Skip to content
Open
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
60 changes: 26 additions & 34 deletions shtab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from collections import defaultdict
from functools import total_ordering
from itertools import starmap
from shlex import join, quote
from string import Template
from typing import Any, Dict, List
from typing import Optional as Opt
Expand Down Expand Up @@ -187,7 +188,7 @@ 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}")
compgens.append(f"{prefix}_pos_{i}_COMPGEN={quote(comp_pattern)}")

if positional.choices:
# choices (including subparsers & shtab `.complete` functions)
Expand All @@ -199,7 +200,8 @@ 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.append(f"{prefix}_pos_{i}_COMPGEN="
f"{quote(choice_type2fn[choice.type])}")
elif isinstance(positional.choices, dict):
# subparser, so append to list of subparsers & recurse
log.debug("subcommand:%s", choice)
Expand Down Expand Up @@ -229,21 +231,18 @@ 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=({join(this_positional_choices)})")

# skip default `nargs` values
if positional.nargs not in (None, "1", "?"):
nargs.append(f"{prefix}_pos_{i}_nargs={positional.nargs}")
nargs.append(f"{prefix}_pos_{i}_nargs={quote(str(positional.nargs))}")

if discovered_subparsers:
subparsers_str = "' '".join(discovered_subparsers)
subparsers.append(f"{prefix}_subparsers=('{subparsers_str}')")
subparsers.append(f"{prefix}_subparsers=({join(discovered_subparsers)})")
log.debug(f"subcommands:{prefix}:{discovered_subparsers}")

# optional arguments
options_strings_str = "' '".join(get_option_strings(parser))
option_strings.append(f"{prefix}_option_strings=('{options_strings_str}')")
option_strings.append(f"{prefix}_option_strings=({join(get_option_strings(parser))})")
for optional in parser._get_optional_actions():
if optional == SUPPRESS:
continue
Expand All @@ -252,8 +251,8 @@ def recurse(parser, prefix):
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}")
compgens.append(f"{prefix}_{wordify(option_string)}_COMPGEN="
f"{join(comp_pattern_str)}")

if optional.choices:
# choices (including shtab `.complete` functions)
Expand All @@ -264,20 +263,20 @@ def recurse(parser, prefix):
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.append(f"{prefix}_{wordify(option_string)}_COMPGEN="
f"{quote(func_str)}")
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}')")
choices.append(f"{prefix}_{wordify(option_string)}_choices="
f"({join(this_optional_choices)})")

# Check for nargs.
if optional.nargs is not None and optional.nargs != 1:
nargs.append(f"{prefix}_{wordify(option_string)}_nargs={optional.nargs}")
nargs.append(f"{prefix}_{wordify(option_string)}_nargs="
f"{quote(str(optional.nargs))}")

# append recursion results
subparsers.extend(sub_subparsers)
Expand Down Expand Up @@ -439,18 +438,17 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):

if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
# optional argument started: use option strings
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
mapfile -t COMPREPLY < <(compgen -W "${current_option_strings[*]}" -- "${completing_word}")
elif [[ "${previous_word}" == ">" || "${previous_word}" == ">>" ||
"${previous_word}" =~ ^[12]">" || "${previous_word}" =~ ^[12]">>" ]]; then
# handle redirection operators
COMPREPLY=( $(compgen -f -- "${completing_word}") )
mapfile -t 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+=( $(compgen -W "${current_action_choices[*]}" -- "${completing_word}") )
[ -n "${current_action_compgen}" ] &&
mapfile -t COMPREPLY < <("${current_action_compgen}" "${completing_word}")
mapfile -t -O "${#COMPREPLY[@]}" COMPREPLY < <(
compgen -W "${current_action_choices[*]}" -- "${completing_word}")
fi

return 0
Expand All @@ -469,11 +467,6 @@ def complete_bash(parser, root_prefix=None, preamble="", choice_functions=None):
)


def escape_zsh(string):
# excessive but safe
return re.sub(r"([^\w\s.,()-])", r"\\\1", str(string))


@mark_completer("zsh")
def complete_zsh(parser, root_prefix=None, preamble="", choice_functions=None):
"""
Expand Down Expand Up @@ -501,7 +494,7 @@ def format_optional(opt, parser):
nargs=('"(- : *)"' if is_opt_end(opt) else '"*"' if is_opt_multiline(opt) else ""),
options=("{{{}}}".format(",".join(opt.option_strings)) if len(opt.option_strings)
> 1 else '"{}"'.format("".join(opt.option_strings))),
help=escape_zsh(get_help(opt) if opt.help else ""),
help=quote(get_help(opt) if opt.help else ""),
dest=opt.dest,
pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) if hasattr(
opt, "complete") else
Expand All @@ -513,7 +506,7 @@ def format_positional(opt, parser):
get_help = parser._get_formatter()._expand_help
return '"{nargs}:{help}:{pattern}"'.format(
nargs={ONE_OR_MORE: "(*)", ZERO_OR_MORE: "(*):", REMAINDER: "(-)*"}.get(opt.nargs, ""),
help=escape_zsh((get_help(opt) if opt.help else opt.dest).strip().split("\n")[0]),
help=quote((get_help(opt) if opt.help else opt.dest).strip().split("\n")[0]),
pattern=complete2pattern(opt.complete, "zsh", choice_type2fn) if hasattr(
opt, "complete") else
(choice_type2fn[opt.choices[0].type] if isinstance(opt.choices[0], Choice) else
Expand Down Expand Up @@ -645,7 +638,7 @@ def command_option(prefix, options):

def command_list(prefix, options):
name = " ".join([prog, *options["paths"]])
commands = "\n ".join(f'"{escape_zsh(cmd)}:{escape_zsh(opt["help"])}"'
commands = "\n ".join(f'{quote(cmd)}:{quote(opt["help"])}'
for cmd, opt in sorted(options["commands"].items()))
return f"""
{prefix}_commands() {{
Expand Down Expand Up @@ -769,8 +762,7 @@ def recurse_parser(cparser, positional_idx, requirements=None):
for nn, arg in ndict.items():
if arg.choices:
checks = [f'[ "$cmd[{iidx}]" == "{n}" ]' for iidx, n in enumerate(nn, start=2)]
choices_str = "' '".join(arg.choices)
checks_str = ' && '.join(checks + [f"echo '{choices_str}'"])
checks_str = ' && '.join(checks + [f"echo {join(arg.choices)}"])
nlist.append(f"( {checks_str} || false )")
# Ugly hack
nlist_str = ' || '.join(nlist)
Expand Down