Skip to content

Commit 25e3e44

Browse files
committed
bump version, merge branch 'tcsh'
2 parents b6661ce + 7cdadca commit 25e3e44

File tree

8 files changed

+143
-7
lines changed

8 files changed

+143
-7
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Most of the magic lives in [`shtab/__init__.py`](./shtab/__init__.py).
2020
- `complete()` - primary API, calls shell-specific versions
2121
- `complete_bash()`
2222
- `complete_zsh()`
23+
- `complete_tcsh()`
2324
- ...
2425
- `add_argument_to()` - convenience function for library integration
2526
- `Optional()`, `Required()`, `Choice()` - legacy helpers for advanced completion (e.g. dirs, files, `*.txt`)

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Features
2020

2121
- ``bash``
2222
- ``zsh``
23+
- ``tcsh``
2324

2425
- Supports
2526

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

314315
- support ``fish``
315316
- support ``powershell``
316-
- support ``tcsh``
317317

318318
See
319319
`CONTRIBUTING.md <https://github.com/iterative/shtab/tree/master/CONTRIBUTING.md>`_

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- Outputs tab completion scripts for
1717
- `bash`
1818
- `zsh`
19+
- `tcsh`
1920
- Supports
2021
- [argparse](https://docs.python.org/library/argparse)
2122
- [docopt](https://pypi.org/project/docopt) (via [argopt](https://pypi.org/project/argopt))

docs/use.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ There are two ways of using `shtab`:
77
- end-users execute `shtab your_cli_app.your_parser_object`
88
- [Library Usage](#library-usage): as a library integrated into your CLI application
99
- adds a couple of lines to your application
10-
- argument mode: end-users execute `your_cli_app --print-completion {bash,zsh}`
11-
- subparser mode: end-users execute `your_cli_app completion {bash,zsh}`
10+
- argument mode: end-users execute `your_cli_app --print-completion {bash,zsh,tcsh}`
11+
- subparser mode: end-users execute `your_cli_app completion {bash,zsh,tcsh}`
1212

1313
## CLI Usage
1414

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

80+
=== "tcsh"
81+
82+
```sh
83+
shtab --shell=tcsh shtab.main.get_main_parser --error-unimportable \
84+
| sudo tee /etc/profile.d/shtab.completion.csh
85+
```
86+
87+
=== "Eager tcsh"
88+
89+
There are a few options:
90+
91+
```sh
92+
# Install locally
93+
echo 'shtab --shell=tcsh shtab.main.get_main_parser | source /dev/stdin' \
94+
>> ~/.cshrc
95+
96+
# Install system-wide
97+
echo 'shtab --shell=tcsh shtab.main.get_main_parser | source /dev/stdin' \
98+
| sudo tee /etc/profile.d/eager-completion.csh
99+
```
100+
80101
!!! tip
81102
See the [examples/](https://github.com/iterative/shtab/tree/master/examples)
82103
folder for more.

examples/customcomplete.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
import shtab # for completion magic
1010

11-
TXT_FILE = {"bash": "_shtab_greeter_compgen_TXTFiles", "zsh": "_files -g '(*.txt|*.TXT)'"}
11+
TXT_FILE = {
12+
"bash": "_shtab_greeter_compgen_TXTFiles", "zsh": "_files -g '(*.txt|*.TXT)'",
13+
"tcsh": "f:*.txt"}
1214
PREAMBLE = {
1315
"bash": """
1416
# $1=COMP_WORDS[1]
@@ -17,7 +19,7 @@
1719
compgen -f -X '!*?.txt' -- $1
1820
compgen -f -X '!*?.TXT' -- $1
1921
}
20-
""", "zsh": ""}
22+
""", "zsh": "", "tcsh": ""}
2123

2224

2325
def process(args):
@@ -48,6 +50,8 @@ def get_main_parser():
4850
).complete = shtab.DIRECTORY
4951
# directory tab completion builtin shortcut
5052

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

shtab/__init__.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
_StoreConstAction,
1414
_VersionAction,
1515
)
16+
from collections import defaultdict
1617
from functools import total_ordering
1718
from string import Template
1819

@@ -32,8 +33,8 @@
3233
SUPPORTED_SHELLS = []
3334
_SUPPORTED_COMPLETERS = {}
3435
CHOICE_FUNCTIONS = {
35-
"file": {"bash": "_shtab_compgen_files", "zsh": "_files"},
36-
"directory": {"bash": "_shtab_compgen_dirs", "zsh": "_files -/"}}
36+
"file": {"bash": "_shtab_compgen_files", "zsh": "_files", "tcsh": "f"},
37+
"directory": {"bash": "_shtab_compgen_dirs", "zsh": "_files -/", "tcsh": "d"}}
3738
FILE = CHOICE_FUNCTIONS["file"]
3839
DIRECTORY = DIR = CHOICE_FUNCTIONS["directory"]
3940
FLAG_OPTION = (
@@ -580,6 +581,109 @@ def format_positional(opt):
580581
)
581582

582583

584+
@mark_completer("tcsh")
585+
def complete_tcsh(parser, root_prefix=None, preamble="", choice_functions=None):
586+
"""
587+
Return tcsh syntax autocompletion script.
588+
589+
See `complete` for arguments.
590+
"""
591+
optionals_single = set()
592+
optionals_double = set()
593+
specials = []
594+
index_choices = defaultdict(dict)
595+
596+
choice_type2fn = {k: v["tcsh"] for k, v in CHOICE_FUNCTIONS.items()}
597+
if choice_functions:
598+
choice_type2fn.update(choice_functions)
599+
600+
def get_specials(arg, arg_type, arg_sel):
601+
if arg.choices:
602+
choice_strs = ' '.join(map(str, arg.choices))
603+
yield "'{}/{}/({})/'".format(
604+
arg_type,
605+
arg_sel,
606+
choice_strs,
607+
)
608+
elif hasattr(arg, "complete"):
609+
complete_fn = complete2pattern(arg.complete, 'tcsh', choice_type2fn)
610+
if complete_fn:
611+
yield "'{}/{}/{}/'".format(
612+
arg_type,
613+
arg_sel,
614+
complete_fn,
615+
)
616+
617+
def recurse_parser(cparser, positional_idx, requirements=None):
618+
log_prefix = '| ' * positional_idx
619+
log.debug('%sParser @ %d', log_prefix, positional_idx)
620+
if requirements:
621+
log.debug('%s- Requires: %s', log_prefix, ' '.join(requirements))
622+
else:
623+
requirements = []
624+
625+
for optional in cparser._get_optional_actions():
626+
log.debug('%s| Optional: %s', log_prefix, optional.dest)
627+
# Mingle all optional arguments for all subparsers
628+
for optional_str in optional.option_strings:
629+
log.debug('%s| | %s', log_prefix, optional_str)
630+
if optional_str.startswith('--'):
631+
optionals_double.add(optional_str[2:])
632+
elif optional_str.startswith('-'):
633+
optionals_single.add(optional_str[1:])
634+
specials.extend(get_specials(optional, 'n', optional_str))
635+
636+
for positional in cparser._get_positional_actions():
637+
if positional.help != SUPPRESS:
638+
positional_idx += 1
639+
log.debug('%s| Positional #%d: %s', log_prefix, positional_idx, positional.dest)
640+
index_choices[positional_idx][tuple(requirements)] = positional
641+
if not requirements and isinstance(positional.choices, dict):
642+
for subcmd, subparser in positional.choices.items():
643+
log.debug('%s| | SubParser: %s', log_prefix, subcmd)
644+
recurse_parser(subparser, positional_idx, requirements + [subcmd])
645+
646+
recurse_parser(parser, 0)
647+
648+
for idx, ndict in index_choices.items():
649+
if len(ndict) == 1:
650+
# Single choice, no requirements
651+
arg = list(ndict.values())[0]
652+
specials.extend(get_specials(arg, 'p', str(idx)))
653+
else:
654+
# Multiple requirements
655+
nlist = []
656+
for nn, arg in ndict.items():
657+
checks = [
658+
'[ "$cmd[{}]" == "{}" ]'.format(iidx, n) for iidx, n in enumerate(nn, start=2)]
659+
if arg.choices:
660+
nlist.append('( {}echo "{}" || false )'.format(
661+
' && '.join(checks + ['']), # Append the separator
662+
'\\n'.join(arg.choices),
663+
))
664+
665+
# Ugly hack
666+
specials.append("'p@{}@`set cmd=($COMMAND_LINE); {}`@'".format(
667+
str(idx), ' || '.join(nlist)))
668+
669+
return Template("""\
670+
#!/usr/bin/env tcsh
671+
# AUTOMATICALLY GENERATED by `shtab`
672+
673+
${preamble}
674+
675+
complete ${prog} \\
676+
'c/--/(${optionals_double_str})/' \\
677+
'c/-/(${optionals_single_str} -)/' \\
678+
${optionals_special_str} \\
679+
'p/*/()/'""").safe_substitute(
680+
preamble=("\n# Custom Preamble\n" + preamble +
681+
"\n# End Custom Preamble\n" if preamble else ""), root_prefix=root_prefix,
682+
prog=parser.prog, optionals_double_str=' '.join(optionals_double),
683+
optionals_single_str=' '.join(optionals_single),
684+
optionals_special_str=' \\\n '.join(specials))
685+
686+
583687
def complete(parser, shell="bash", root_prefix=None, preamble="", choice_functions=None):
584688
"""
585689
parser : argparse.ArgumentParser

shtab/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,15 @@ def get_main_parser():
2626
action="store_true",
2727
help="raise errors if `parser` is not found in $PYTHONPATH",
2828
)
29+
parser.add_argument("--verbose", dest="loglevel", action="store_const", default=logging.INFO,
30+
const=logging.DEBUG, help="Log debug information")
2931
return parser
3032

3133

3234
def main(argv=None):
3335
parser = get_main_parser()
3436
args = parser.parse_args(argv)
37+
logging.basicConfig(level=args.loglevel)
3538
log.debug(args)
3639

3740
module, other_parser = args.parser.rsplit(".", 1)

tests/test_shtab.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ def test_prog_scripts(shell, caplog, capsys):
9191
assert script_py == ["complete -o filenames -F _shtab_shtab script.py"]
9292
elif shell == "zsh":
9393
assert script_py == ["#compdef script.py", "_describe 'script.py commands' _commands"]
94+
elif shell == "tcsh":
95+
assert script_py == ["complete script.py \\"]
9496
else:
9597
raise NotImplementedError(shell)
9698

0 commit comments

Comments
 (0)