Skip to content

Commit f8fbb0c

Browse files
Implement file globbing support
This is tested locally in tcsh and bash, zsh is untested. There should not be any regressions, the test suite passes.
1 parent e5541c8 commit f8fbb0c

File tree

1 file changed

+73
-22
lines changed

1 file changed

+73
-22
lines changed

shtab/__init__.py

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
from functools import total_ordering
1919
from itertools import starmap
2020
from string import Template
21-
from typing import Any, Dict, List, Sequence
21+
from typing import Any, Dict, List, Mapping
2222
from typing import Optional as Opt
23-
from typing import Union
23+
from typing import Sequence, Tuple, Union
2424

2525
# version detector. Precedence: installed dist, git, 'UNKNOWN'
2626
try:
@@ -32,7 +32,7 @@
3232
__version__ = get_version(root="..", relative_to=__file__)
3333
except (ImportError, LookupError):
3434
__version__ = "UNKNOWN"
35-
__all__ = ["complete", "add_argument_to", "SUPPORTED_SHELLS", "FILE", "DIRECTORY", "DIR"]
35+
__all__ = ["complete", "add_argument_to", "SUPPORTED_SHELLS", "FILE", "DIRECTORY", "DIR", "fglob"]
3636
log = logging.getLogger(__name__)
3737

3838
SUPPORTED_SHELLS: List[str] = []
@@ -51,6 +51,15 @@
5151
)
5252

5353

54+
def fglob(fglob: str):
55+
'''Glob files'''
56+
return {
57+
'__glob__': fglob,
58+
'bash': '_shtab_compgen_files', # Uses `__glob__` internally
59+
'zsh': f"_files -g '{fglob}'",
60+
'tcsh': f'f:{fglob}',}
61+
62+
5463
class _ShtabPrintCompletionAction(Action):
5564
pass
5665

@@ -124,8 +133,26 @@ class Required:
124133

125134

126135
def complete2pattern(opt_complete, shell: str, choice_type2fn) -> str:
127-
return (opt_complete.get(shell, "")
128-
if isinstance(opt_complete, dict) else choice_type2fn[opt_complete])
136+
if isinstance(opt_complete, dict):
137+
return opt_complete.get(shell, "")
138+
else:
139+
return choice_type2fn[opt_complete]
140+
141+
142+
def bash_complete2compgen(
143+
opt_complete: Mapping[str, str],
144+
shell: str,
145+
choice_type2fn: Mapping[str, str],
146+
) -> Tuple[str, Tuple[str]]:
147+
# Same inputs as `complete2pattern`
148+
options = []
149+
if isinstance(opt_complete, dict):
150+
if '__glob__' in opt_complete:
151+
option_glob = opt_complete['__glob__']
152+
options.extend(['-X', f'!{option_glob}'])
153+
return opt_complete.get(shell), tuple(options)
154+
else:
155+
return choice_type2fn[opt_complete], tuple(options)
129156

130157

131158
def bash_listify(lst: Sequence[str]) -> str:
@@ -194,8 +221,11 @@ def recurse(parser, prefix):
194221

195222
if hasattr(positional, "complete"):
196223
# shtab `.complete = ...` functions
197-
comp_pattern = complete2pattern(positional.complete, "bash", choice_type2fn)
198-
compgens.append(f"{prefix}_pos_{i}_COMPGEN={comp_pattern}")
224+
comp_gen, comp_genopts = bash_complete2compgen(positional.complete, "bash",
225+
choice_type2fn)
226+
compgens.extend([
227+
f"{prefix}_pos_{i}_COMPGEN={comp_gen}",
228+
f"{prefix}_pos_{i}_COMPGEN_options={bash_listify(comp_genopts)}",])
199229

200230
if positional.choices:
201231
# choices (including subparsers & shtab `.complete` functions)
@@ -207,7 +237,9 @@ def recurse(parser, prefix):
207237
# append special completion type to `compgens`
208238
# NOTE: overrides `.complete` attribute
209239
log.debug(f"Choice.{choice.type}:{prefix}:{positional.dest}")
210-
compgens.append(f"{prefix}_pos_{i}_COMPGEN={choice_type2fn[choice.type]}")
240+
compgens.extend([
241+
f"{prefix}_pos_{i}_COMPGEN={choice_type2fn[choice.type]}",
242+
f"{prefix}_pos_{i}_COMPGEN_options=()",])
211243
elif isinstance(positional.choices, dict):
212244
# subparser, so append to list of subparsers & recurse
213245
log.debug("subcommand:%s", choice)
@@ -237,7 +269,8 @@ def recurse(parser, prefix):
237269
this_positional_choices.append(str(choice))
238270

239271
if this_positional_choices:
240-
choices.append(f"{prefix}_pos_{i}_choices={bash_listify(this_positional_choices)}")
272+
choices.append(
273+
f"{prefix}_pos_{i}_choices={bash_listify(this_positional_choices)}")
241274

242275
# skip default `nargs` values
243276
if positional.nargs not in (None, "1", "?"):
@@ -258,9 +291,12 @@ def recurse(parser, prefix):
258291
for option_string in optional.option_strings:
259292
if hasattr(optional, "complete"):
260293
# shtab `.complete = ...` functions
261-
comp_pattern_str = complete2pattern(optional.complete, "bash", choice_type2fn)
262-
compgens.append(
263-
f"{prefix}_{wordify(option_string)}_COMPGEN={comp_pattern_str}")
294+
comp_gen, comp_genopts = bash_complete2compgen(optional.complete, "bash",
295+
choice_type2fn)
296+
compgens.extend([
297+
f"{prefix}_{wordify(option_string)}_COMPGEN={comp_gen}",
298+
f"{prefix}_{wordify(option_string)}_COMPGEN_options={bash_listify(comp_genopts)}",
299+
])
264300

265301
if optional.choices:
266302
# choices (including shtab `.complete` functions)
@@ -270,15 +306,17 @@ def recurse(parser, prefix):
270306
# NOTE: overrides `.complete` attribute
271307
if isinstance(choice, Choice):
272308
log.debug(f"Choice.{choice.type}:{prefix}:{optional.dest}")
273-
func_str = choice_type2fn[choice.type]
274-
compgens.append(
275-
f"{prefix}_{wordify(option_string)}_COMPGEN={func_str}")
309+
compgens.extend([
310+
f"{prefix}_{wordify(option_string)}_COMPGEN={choice_type2fn[choice.type]}",
311+
f"{prefix}_{wordify(option_string)}_COMPGEN_options=()",])
276312
else:
277313
# simple choice
278314
this_optional_choices.append(str(choice))
279315

280316
if this_optional_choices:
281-
choices.append(f"{prefix}_{wordify(option_string)}_choices={bash_listify(this_optional_choices)}")
317+
choices.append(
318+
f"{prefix}_{wordify(option_string)}_choices={bash_listify(this_optional_choices)}"
319+
)
282320

283321
# Check for nargs.
284322
if optional.nargs is not None and optional.nargs != 1:
@@ -328,7 +366,9 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
328366
${preamble}
329367
# $1=COMP_WORDS[1]
330368
_shtab_compgen_files() {
331-
compgen -f -- $1 # files
369+
local cur="$1"
370+
shift
371+
compgen -f "$@" -- "$cur" # files
332372
}
333373
334374
# $1=COMP_WORDS[1]
@@ -363,6 +403,13 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
363403
local current_action_compgen_var=${current_action}_COMPGEN
364404
current_action_compgen="${!current_action_compgen_var-}"
365405
406+
if [ -z "$current_action_compgen" ]; then
407+
current_action_compgen_options=()
408+
else
409+
local current_action_compgen_options_var="${current_action}_COMPGEN_options[@]"
410+
current_action_compgen_options=("${!current_action_compgen_options_var}")
411+
fi
412+
366413
local current_action_choices_var="${current_action}_choices[@]"
367414
current_action_choices="${!current_action_choices_var-}"
368415
@@ -393,6 +440,7 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
393440
local current_action_args_start_index
394441
local current_action_choices
395442
local current_action_compgen
443+
local -a current_action_compgen_options
396444
local current_action_is_positional
397445
local current_action_nargs
398446
local current_option_strings
@@ -450,11 +498,14 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
450498
# handle redirection operators
451499
COMPREPLY=( $(compgen -f -- "${completing_word}") )
452500
else
453-
# use choices & compgen
454-
local IFS=$'\\n' # items may contain spaces, so delimit using newline
455-
COMPREPLY=( $([ -n "${current_action_compgen}" ] \\
456-
&& "${current_action_compgen}" "${completing_word}") )
457-
unset IFS
501+
COMPREPLY=()
502+
# use compgen
503+
if [ -n "${current_action_compgen}" ]; then
504+
local IFS=$'\\n' # items may contain spaces, so delimit using newline
505+
COMPREPLY+=( $("${current_action_compgen}" "${current_action_compgen_options[@]}" "${completing_word}") )
506+
unset IFS
507+
fi
508+
# use choices
458509
COMPREPLY+=( $(compgen -W "${current_action_choices[*]}" -- "${completing_word}") )
459510
fi
460511

0 commit comments

Comments
 (0)