Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/use.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,11 @@ Assuming this code example is installed in `MY_PROG.command.main`, simply run:
folder for more.

Complex projects with subparsers and custom completions for paths matching
certain patterns (e.g. `--file=*.txt`) are fully supported (see
certain patterns (e.g. `--file=*.txt`) are supported using the `fglob` function
(see fully custom examples
[examples/customcomplete.py](https://github.com/iterative/shtab/tree/main/examples/customcomplete.py)
or even
[iterative/dvc:commands/completion.py](https://github.com/iterative/dvc/blob/main/dvc/commands/completion.py)
for example).
[iterative/dvc:commands/completion.py](https://github.com/iterative/dvc/blob/main/dvc/commands/completion.py)).

Add direct support to scripts for a little more configurability:

Expand All @@ -172,6 +172,7 @@ Add direct support to scripts for a little more configurability:
shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic!
# file & directory tab complete
parser.add_argument("file", nargs="?").complete = shtab.FILE
parser.add_argument("markdown", nargs="?").complete = shtab.fglob("*.md")
parser.add_argument("--dir", default=".").complete = shtab.DIRECTORY
return parser

Expand Down
3 changes: 2 additions & 1 deletion examples/pathcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def get_main_parser():
shtab.add_argument_to(parser, ["-s", "--print-completion"]) # magic!

# file & directory tab complete
parser.add_argument("file", nargs="?").complete = shtab.FILE
parser.add_argument("file_all", nargs="?").complete = shtab.FILE
parser.add_argument("file_md", nargs="?").complete = shtab.fglob('*.md')
parser.add_argument("--dir", default=".").complete = shtab.DIRECTORY
return parser

Expand Down
104 changes: 80 additions & 24 deletions shtab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
from functools import total_ordering
from itertools import starmap
from string import Template
from typing import Any, Dict, List
from typing import Any, Dict, List, Mapping
from typing import Optional as Opt
from typing import Union
from typing import Sequence, Tuple, Union

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

SUPPORTED_SHELLS: List[str] = []
Expand All @@ -51,6 +51,15 @@
)


def fglob(fglob: str):
'''Glob files'''
return {
'__glob__': fglob,
'bash': '_shtab_compgen_files', # Uses `__glob__` internally
'zsh': f"_files -g '{fglob}'",
'tcsh': f'f:{fglob}',}


class _ShtabPrintCompletionAction(Action):
pass

Expand Down Expand Up @@ -124,8 +133,34 @@ class Required:


def complete2pattern(opt_complete, shell: str, choice_type2fn) -> str:
return (opt_complete.get(shell, "")
if isinstance(opt_complete, dict) else choice_type2fn[opt_complete])
if isinstance(opt_complete, dict):
return opt_complete.get(shell, "")
else:
return choice_type2fn[opt_complete]


def bash_complete2compgen(
opt_complete: Mapping[str, str],
shell: str,
choice_type2fn: Mapping[str, str],
) -> Tuple[str, Tuple[str]]:
# Same inputs as `complete2pattern`
options = []
if isinstance(opt_complete, dict):
if '__glob__' in opt_complete:
option_glob = opt_complete['__glob__']
options.extend(['-X', f'!{option_glob}'])
return opt_complete.get(shell), tuple(options)
else:
return choice_type2fn[opt_complete], tuple(options)


def bash_listify(lst: Sequence[str]) -> str:
"""Create a bash array from a list of strings"""
if len(lst) == 0:
return '()'
else:
return "('%s')" % "' '".join(lst)


def wordify(string: str) -> str:
Expand Down Expand Up @@ -186,8 +221,11 @@ def recurse(parser, prefix):

if hasattr(positional, "complete"):
# shtab `.complete = ...` functions
comp_pattern = complete2pattern(positional.complete, "bash", choice_type2fn)
compgens.append(f"{prefix}_pos_{i}_COMPGEN={comp_pattern}")
comp_gen, comp_genopts = bash_complete2compgen(positional.complete, "bash",
choice_type2fn)
compgens.extend([
f"{prefix}_pos_{i}_COMPGEN={comp_gen}",
f"{prefix}_pos_{i}_COMPGEN_options={bash_listify(comp_genopts)}",])

if positional.choices:
# choices (including subparsers & shtab `.complete` functions)
Expand All @@ -199,7 +237,9 @@ def recurse(parser, prefix):
# append special completion type to `compgens`
# NOTE: overrides `.complete` attribute
log.debug(f"Choice.{choice.type}:{prefix}:{positional.dest}")
compgens.append(f"{prefix}_pos_{i}_COMPGEN={choice_type2fn[choice.type]}")
compgens.extend([
f"{prefix}_pos_{i}_COMPGEN={choice_type2fn[choice.type]}",
f"{prefix}_pos_{i}_COMPGEN_options=()",])
elif isinstance(positional.choices, dict):
# subparser, so append to list of subparsers & recurse
log.debug("subcommand:%s", choice)
Expand Down Expand Up @@ -229,8 +269,8 @@ def recurse(parser, prefix):
this_positional_choices.append(str(choice))

if this_positional_choices:
choices_str = "' '".join(this_positional_choices)
choices.append(f"{prefix}_pos_{i}_choices=('{choices_str}')")
choices.append(
f"{prefix}_pos_{i}_choices={bash_listify(this_positional_choices)}")

# skip default `nargs` values
if positional.nargs not in (None, "1", "?"):
Expand All @@ -251,9 +291,12 @@ def recurse(parser, prefix):
for option_string in optional.option_strings:
if hasattr(optional, "complete"):
# shtab `.complete = ...` functions
comp_pattern_str = complete2pattern(optional.complete, "bash", choice_type2fn)
compgens.append(
f"{prefix}_{wordify(option_string)}_COMPGEN={comp_pattern_str}")
comp_gen, comp_genopts = bash_complete2compgen(optional.complete, "bash",
choice_type2fn)
compgens.extend([
f"{prefix}_{wordify(option_string)}_COMPGEN={comp_gen}",
f"{prefix}_{wordify(option_string)}_COMPGEN_options={bash_listify(comp_genopts)}",
])

if optional.choices:
# choices (including shtab `.complete` functions)
Expand All @@ -263,17 +306,17 @@ def recurse(parser, prefix):
# NOTE: overrides `.complete` attribute
if isinstance(choice, Choice):
log.debug(f"Choice.{choice.type}:{prefix}:{optional.dest}")
func_str = choice_type2fn[choice.type]
compgens.append(
f"{prefix}_{wordify(option_string)}_COMPGEN={func_str}")
compgens.extend([
f"{prefix}_{wordify(option_string)}_COMPGEN={choice_type2fn[choice.type]}",
f"{prefix}_{wordify(option_string)}_COMPGEN_options=()",])
else:
# simple choice
this_optional_choices.append(str(choice))

if this_optional_choices:
this_choices_str = "' '".join(this_optional_choices)
choices.append(
f"{prefix}_{wordify(option_string)}_choices=('{this_choices_str}')")
f"{prefix}_{wordify(option_string)}_choices={bash_listify(this_optional_choices)}"
)

# Check for nargs.
if optional.nargs is not None and optional.nargs != 1:
Expand Down Expand Up @@ -323,7 +366,9 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
${preamble}
# $1=COMP_WORDS[1]
_shtab_compgen_files() {
compgen -f -- $1 # files
local cur="$1"
shift
compgen -f "$@" -- "$cur" # files
}

# $1=COMP_WORDS[1]
Expand Down Expand Up @@ -358,6 +403,13 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
local current_action_compgen_var=${current_action}_COMPGEN
current_action_compgen="${!current_action_compgen_var-}"

if [ -z "$current_action_compgen" ]; then
current_action_compgen_options=()
else
local current_action_compgen_options_var="${current_action}_COMPGEN_options[@]"
current_action_compgen_options=("${!current_action_compgen_options_var}")
fi

local current_action_choices_var="${current_action}_choices[@]"
current_action_choices="${!current_action_choices_var-}"

Expand Down Expand Up @@ -388,6 +440,7 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
local current_action_args_start_index
local current_action_choices
local current_action_compgen
local -a current_action_compgen_options
local current_action_is_positional
local current_action_nargs
local current_option_strings
Expand Down Expand Up @@ -445,11 +498,14 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
# handle redirection operators
COMPREPLY=( $(compgen -f -- "${completing_word}") )
else
# use choices & compgen
local IFS=$'\\n' # items may contain spaces, so delimit using newline
COMPREPLY=( $([ -n "${current_action_compgen}" ] \\
&& "${current_action_compgen}" "${completing_word}") )
unset IFS
COMPREPLY=()
# use compgen
if [ -n "${current_action_compgen}" ]; then
local IFS=$'\\n' # items may contain spaces, so delimit using newline
COMPREPLY+=( $("${current_action_compgen}" "${current_action_compgen_options[@]}" "${completing_word}") )
unset IFS
fi
# use choices
COMPREPLY+=( $(compgen -W "${current_action_choices[*]}" -- "${completing_word}") )
fi

Expand Down
Loading