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
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 {

Check warning on line 56 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L56

Added line #L56 was not covered by tests
'__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 @@


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]

Check warning on line 139 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L139

Added line #L139 was not covered by tests


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}'])

Check warning on line 152 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L151-L152

Added lines #L151 - L152 were not covered by tests
return opt_complete.get(shell), tuple(options)
else:
return choice_type2fn[opt_complete], tuple(options)

Check warning on line 155 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L155

Added line #L155 was not covered by tests


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 @@

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 @@
# 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([

Check warning on line 240 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L240

Added line #L240 was not covered by tests
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 @@
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 @@
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",

Check warning on line 294 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L294

Added line #L294 was not covered by tests
choice_type2fn)
compgens.extend([

Check warning on line 296 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L296

Added line #L296 was not covered by tests
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 @@
# 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([

Check warning on line 309 in shtab/__init__.py

View check run for this annotation

Codecov / codecov/patch

shtab/__init__.py#L309

Added line #L309 was not covered by tests
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 @@
${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 @@
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 @@
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 @@
# 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