Skip to content

Commit

Permalink
bump version, merge branch 'tcsh'
Browse files Browse the repository at this point in the history
  • Loading branch information
casperdcl committed Nov 16, 2021
2 parents b6661ce + 7cdadca commit 25e3e44
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 7 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Most of the magic lives in [`shtab/__init__.py`](./shtab/__init__.py).
- `complete()` - primary API, calls shell-specific versions
- `complete_bash()`
- `complete_zsh()`
- `complete_tcsh()`
- ...
- `add_argument_to()` - convenience function for library integration
- `Optional()`, `Required()`, `Choice()` - legacy helpers for advanced completion (e.g. dirs, files, `*.txt`)
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Features

- ``bash``
- ``zsh``
- ``tcsh``

- Supports

Expand Down Expand Up @@ -313,7 +314,6 @@ Please do open issues & pull requests! Some ideas:

- support ``fish``
- support ``powershell``
- support ``tcsh``

See
`CONTRIBUTING.md <https://github.com/iterative/shtab/tree/master/CONTRIBUTING.md>`_
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- Outputs tab completion scripts for
- `bash`
- `zsh`
- `tcsh`
- Supports
- [argparse](https://docs.python.org/library/argparse)
- [docopt](https://pypi.org/project/docopt) (via [argopt](https://pypi.org/project/argopt))
Expand Down
25 changes: 23 additions & 2 deletions docs/use.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ There are two ways of using `shtab`:
- end-users execute `shtab your_cli_app.your_parser_object`
- [Library Usage](#library-usage): as a library integrated into your CLI application
- adds a couple of lines to your application
- argument mode: end-users execute `your_cli_app --print-completion {bash,zsh}`
- subparser mode: end-users execute `your_cli_app completion {bash,zsh}`
- argument mode: end-users execute `your_cli_app --print-completion {bash,zsh,tcsh}`
- subparser mode: end-users execute `your_cli_app completion {bash,zsh,tcsh}`

## CLI Usage

Expand Down Expand Up @@ -77,6 +77,27 @@ Below are various examples of enabling `shtab`'s own tab completion scripts.
shtab --shell=zsh shtab.main.get_main_parser > ~/.zsh/completions/_shtab
```

=== "tcsh"

```sh
shtab --shell=tcsh shtab.main.get_main_parser --error-unimportable \
| sudo tee /etc/profile.d/shtab.completion.csh
```

=== "Eager tcsh"

There are a few options:

```sh
# Install locally
echo 'shtab --shell=tcsh shtab.main.get_main_parser | source /dev/stdin' \
>> ~/.cshrc

# Install system-wide
echo 'shtab --shell=tcsh shtab.main.get_main_parser | source /dev/stdin' \
| sudo tee /etc/profile.d/eager-completion.csh
```

!!! tip
See the [examples/](https://github.com/iterative/shtab/tree/master/examples)
folder for more.
Expand Down
8 changes: 6 additions & 2 deletions examples/customcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

import shtab # for completion magic

TXT_FILE = {"bash": "_shtab_greeter_compgen_TXTFiles", "zsh": "_files -g '(*.txt|*.TXT)'"}
TXT_FILE = {
"bash": "_shtab_greeter_compgen_TXTFiles", "zsh": "_files -g '(*.txt|*.TXT)'",
"tcsh": "f:*.txt"}
PREAMBLE = {
"bash": """
# $1=COMP_WORDS[1]
Expand All @@ -17,7 +19,7 @@
compgen -f -X '!*?.txt' -- $1
compgen -f -X '!*?.TXT' -- $1
}
""", "zsh": ""}
""", "zsh": "", "tcsh": ""}


def process(args):
Expand Down Expand Up @@ -48,6 +50,8 @@ def get_main_parser():
).complete = shtab.DIRECTORY
# directory tab completion builtin shortcut

main_parser.add_argument('suffix', choices=['json', 'csv'], default='json',
help="Output format")
parser.set_defaults(func=process)
return main_parser

Expand Down
108 changes: 106 additions & 2 deletions shtab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
_StoreConstAction,
_VersionAction,
)
from collections import defaultdict
from functools import total_ordering
from string import Template

Expand All @@ -32,8 +33,8 @@
SUPPORTED_SHELLS = []
_SUPPORTED_COMPLETERS = {}
CHOICE_FUNCTIONS = {
"file": {"bash": "_shtab_compgen_files", "zsh": "_files"},
"directory": {"bash": "_shtab_compgen_dirs", "zsh": "_files -/"}}
"file": {"bash": "_shtab_compgen_files", "zsh": "_files", "tcsh": "f"},
"directory": {"bash": "_shtab_compgen_dirs", "zsh": "_files -/", "tcsh": "d"}}
FILE = CHOICE_FUNCTIONS["file"]
DIRECTORY = DIR = CHOICE_FUNCTIONS["directory"]
FLAG_OPTION = (
Expand Down Expand Up @@ -580,6 +581,109 @@ def format_positional(opt):
)


@mark_completer("tcsh")
def complete_tcsh(parser, root_prefix=None, preamble="", choice_functions=None):
"""
Return tcsh syntax autocompletion script.
See `complete` for arguments.
"""
optionals_single = set()
optionals_double = set()
specials = []
index_choices = defaultdict(dict)

choice_type2fn = {k: v["tcsh"] for k, v in CHOICE_FUNCTIONS.items()}
if choice_functions:
choice_type2fn.update(choice_functions)

def get_specials(arg, arg_type, arg_sel):
if arg.choices:
choice_strs = ' '.join(map(str, arg.choices))
yield "'{}/{}/({})/'".format(
arg_type,
arg_sel,
choice_strs,
)
elif hasattr(arg, "complete"):
complete_fn = complete2pattern(arg.complete, 'tcsh', choice_type2fn)
if complete_fn:
yield "'{}/{}/{}/'".format(
arg_type,
arg_sel,
complete_fn,
)

def recurse_parser(cparser, positional_idx, requirements=None):
log_prefix = '| ' * positional_idx
log.debug('%sParser @ %d', log_prefix, positional_idx)
if requirements:
log.debug('%s- Requires: %s', log_prefix, ' '.join(requirements))
else:
requirements = []

for optional in cparser._get_optional_actions():
log.debug('%s| Optional: %s', log_prefix, optional.dest)
# Mingle all optional arguments for all subparsers
for optional_str in optional.option_strings:
log.debug('%s| | %s', log_prefix, optional_str)
if optional_str.startswith('--'):
optionals_double.add(optional_str[2:])
elif optional_str.startswith('-'):
optionals_single.add(optional_str[1:])
specials.extend(get_specials(optional, 'n', optional_str))

for positional in cparser._get_positional_actions():
if positional.help != SUPPRESS:
positional_idx += 1
log.debug('%s| Positional #%d: %s', log_prefix, positional_idx, positional.dest)
index_choices[positional_idx][tuple(requirements)] = positional
if not requirements and isinstance(positional.choices, dict):
for subcmd, subparser in positional.choices.items():
log.debug('%s| | SubParser: %s', log_prefix, subcmd)
recurse_parser(subparser, positional_idx, requirements + [subcmd])

recurse_parser(parser, 0)

for idx, ndict in index_choices.items():
if len(ndict) == 1:
# Single choice, no requirements
arg = list(ndict.values())[0]
specials.extend(get_specials(arg, 'p', str(idx)))
else:
# Multiple requirements
nlist = []
for nn, arg in ndict.items():
checks = [
'[ "$cmd[{}]" == "{}" ]'.format(iidx, n) for iidx, n in enumerate(nn, start=2)]
if arg.choices:
nlist.append('( {}echo "{}" || false )'.format(
' && '.join(checks + ['']), # Append the separator
'\\n'.join(arg.choices),
))

# Ugly hack
specials.append("'p@{}@`set cmd=($COMMAND_LINE); {}`@'".format(
str(idx), ' || '.join(nlist)))

return Template("""\
#!/usr/bin/env tcsh
# AUTOMATICALLY GENERATED by `shtab`
${preamble}
complete ${prog} \\
'c/--/(${optionals_double_str})/' \\
'c/-/(${optionals_single_str} -)/' \\
${optionals_special_str} \\
'p/*/()/'""").safe_substitute(
preamble=("\n# Custom Preamble\n" + preamble +
"\n# End Custom Preamble\n" if preamble else ""), root_prefix=root_prefix,
prog=parser.prog, optionals_double_str=' '.join(optionals_double),
optionals_single_str=' '.join(optionals_single),
optionals_special_str=' \\\n '.join(specials))


def complete(parser, shell="bash", root_prefix=None, preamble="", choice_functions=None):
"""
parser : argparse.ArgumentParser
Expand Down
3 changes: 3 additions & 0 deletions shtab/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ def get_main_parser():
action="store_true",
help="raise errors if `parser` is not found in $PYTHONPATH",
)
parser.add_argument("--verbose", dest="loglevel", action="store_const", default=logging.INFO,
const=logging.DEBUG, help="Log debug information")
return parser


def main(argv=None):
parser = get_main_parser()
args = parser.parse_args(argv)
logging.basicConfig(level=args.loglevel)
log.debug(args)

module, other_parser = args.parser.rsplit(".", 1)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_shtab.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ def test_prog_scripts(shell, caplog, capsys):
assert script_py == ["complete -o filenames -F _shtab_shtab script.py"]
elif shell == "zsh":
assert script_py == ["#compdef script.py", "_describe 'script.py commands' _commands"]
elif shell == "tcsh":
assert script_py == ["complete script.py \\"]
else:
raise NotImplementedError(shell)

Expand Down

0 comments on commit 25e3e44

Please sign in to comment.