Spaces:
Runtime error
Runtime error
import os | |
import re | |
import typing as t | |
from gettext import gettext as _ | |
from .core import Argument | |
from .core import BaseCommand | |
from .core import Context | |
from .core import MultiCommand | |
from .core import Option | |
from .core import Parameter | |
from .core import ParameterSource | |
from .parser import split_arg_string | |
from .utils import echo | |
def shell_complete( | |
cli: BaseCommand, | |
ctx_args: t.Dict[str, t.Any], | |
prog_name: str, | |
complete_var: str, | |
instruction: str, | |
) -> int: | |
"""Perform shell completion for the given CLI program. | |
:param cli: Command being called. | |
:param ctx_args: Extra arguments to pass to | |
``cli.make_context``. | |
:param prog_name: Name of the executable in the shell. | |
:param complete_var: Name of the environment variable that holds | |
the completion instruction. | |
:param instruction: Value of ``complete_var`` with the completion | |
instruction and shell, in the form ``instruction_shell``. | |
:return: Status code to exit with. | |
""" | |
shell, _, instruction = instruction.partition("_") | |
comp_cls = get_completion_class(shell) | |
if comp_cls is None: | |
return 1 | |
comp = comp_cls(cli, ctx_args, prog_name, complete_var) | |
if instruction == "source": | |
echo(comp.source()) | |
return 0 | |
if instruction == "complete": | |
echo(comp.complete()) | |
return 0 | |
return 1 | |
class CompletionItem: | |
"""Represents a completion value and metadata about the value. The | |
default metadata is ``type`` to indicate special shell handling, | |
and ``help`` if a shell supports showing a help string next to the | |
value. | |
Arbitrary parameters can be passed when creating the object, and | |
accessed using ``item.attr``. If an attribute wasn't passed, | |
accessing it returns ``None``. | |
:param value: The completion suggestion. | |
:param type: Tells the shell script to provide special completion | |
support for the type. Click uses ``"dir"`` and ``"file"``. | |
:param help: String shown next to the value if supported. | |
:param kwargs: Arbitrary metadata. The built-in implementations | |
don't use this, but custom type completions paired with custom | |
shell support could use it. | |
""" | |
__slots__ = ("value", "type", "help", "_info") | |
def __init__( | |
self, | |
value: t.Any, | |
type: str = "plain", | |
help: t.Optional[str] = None, | |
**kwargs: t.Any, | |
) -> None: | |
self.value = value | |
self.type = type | |
self.help = help | |
self._info = kwargs | |
def __getattr__(self, name: str) -> t.Any: | |
return self._info.get(name) | |
# Only Bash >= 4.4 has the nosort option. | |
_SOURCE_BASH = """\ | |
%(complete_func)s() { | |
local IFS=$'\\n' | |
local response | |
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ | |
%(complete_var)s=bash_complete $1) | |
for completion in $response; do | |
IFS=',' read type value <<< "$completion" | |
if [[ $type == 'dir' ]]; then | |
COMPREPLY=() | |
compopt -o dirnames | |
elif [[ $type == 'file' ]]; then | |
COMPREPLY=() | |
compopt -o default | |
elif [[ $type == 'plain' ]]; then | |
COMPREPLY+=($value) | |
fi | |
done | |
return 0 | |
} | |
%(complete_func)s_setup() { | |
complete -o nosort -F %(complete_func)s %(prog_name)s | |
} | |
%(complete_func)s_setup; | |
""" | |
_SOURCE_ZSH = """\ | |
#compdef %(prog_name)s | |
%(complete_func)s() { | |
local -a completions | |
local -a completions_with_descriptions | |
local -a response | |
(( ! $+commands[%(prog_name)s] )) && return 1 | |
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ | |
%(complete_var)s=zsh_complete %(prog_name)s)}") | |
for type key descr in ${response}; do | |
if [[ "$type" == "plain" ]]; then | |
if [[ "$descr" == "_" ]]; then | |
completions+=("$key") | |
else | |
completions_with_descriptions+=("$key":"$descr") | |
fi | |
elif [[ "$type" == "dir" ]]; then | |
_path_files -/ | |
elif [[ "$type" == "file" ]]; then | |
_path_files -f | |
fi | |
done | |
if [ -n "$completions_with_descriptions" ]; then | |
_describe -V unsorted completions_with_descriptions -U | |
fi | |
if [ -n "$completions" ]; then | |
compadd -U -V unsorted -a completions | |
fi | |
} | |
compdef %(complete_func)s %(prog_name)s; | |
""" | |
_SOURCE_FISH = """\ | |
function %(complete_func)s; | |
set -l response; | |
for value in (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ | |
COMP_CWORD=(commandline -t) %(prog_name)s); | |
set response $response $value; | |
end; | |
for completion in $response; | |
set -l metadata (string split "," $completion); | |
if test $metadata[1] = "dir"; | |
__fish_complete_directories $metadata[2]; | |
else if test $metadata[1] = "file"; | |
__fish_complete_path $metadata[2]; | |
else if test $metadata[1] = "plain"; | |
echo $metadata[2]; | |
end; | |
end; | |
end; | |
complete --no-files --command %(prog_name)s --arguments \ | |
"(%(complete_func)s)"; | |
""" | |
class ShellComplete: | |
"""Base class for providing shell completion support. A subclass for | |
a given shell will override attributes and methods to implement the | |
completion instructions (``source`` and ``complete``). | |
:param cli: Command being called. | |
:param prog_name: Name of the executable in the shell. | |
:param complete_var: Name of the environment variable that holds | |
the completion instruction. | |
.. versionadded:: 8.0 | |
""" | |
name: t.ClassVar[str] | |
"""Name to register the shell as with :func:`add_completion_class`. | |
This is used in completion instructions (``{name}_source`` and | |
``{name}_complete``). | |
""" | |
source_template: t.ClassVar[str] | |
"""Completion script template formatted by :meth:`source`. This must | |
be provided by subclasses. | |
""" | |
def __init__( | |
self, | |
cli: BaseCommand, | |
ctx_args: t.Dict[str, t.Any], | |
prog_name: str, | |
complete_var: str, | |
) -> None: | |
self.cli = cli | |
self.ctx_args = ctx_args | |
self.prog_name = prog_name | |
self.complete_var = complete_var | |
def func_name(self) -> str: | |
"""The name of the shell function defined by the completion | |
script. | |
""" | |
safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII) | |
return f"_{safe_name}_completion" | |
def source_vars(self) -> t.Dict[str, t.Any]: | |
"""Vars for formatting :attr:`source_template`. | |
By default this provides ``complete_func``, ``complete_var``, | |
and ``prog_name``. | |
""" | |
return { | |
"complete_func": self.func_name, | |
"complete_var": self.complete_var, | |
"prog_name": self.prog_name, | |
} | |
def source(self) -> str: | |
"""Produce the shell script that defines the completion | |
function. By default this ``%``-style formats | |
:attr:`source_template` with the dict returned by | |
:meth:`source_vars`. | |
""" | |
return self.source_template % self.source_vars() | |
def get_completion_args(self) -> t.Tuple[t.List[str], str]: | |
"""Use the env vars defined by the shell script to return a | |
tuple of ``args, incomplete``. This must be implemented by | |
subclasses. | |
""" | |
raise NotImplementedError | |
def get_completions( | |
self, args: t.List[str], incomplete: str | |
) -> t.List[CompletionItem]: | |
"""Determine the context and last complete command or parameter | |
from the complete args. Call that object's ``shell_complete`` | |
method to get the completions for the incomplete value. | |
:param args: List of complete args before the incomplete value. | |
:param incomplete: Value being completed. May be empty. | |
""" | |
ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) | |
obj, incomplete = _resolve_incomplete(ctx, args, incomplete) | |
return obj.shell_complete(ctx, incomplete) | |
def format_completion(self, item: CompletionItem) -> str: | |
"""Format a completion item into the form recognized by the | |
shell script. This must be implemented by subclasses. | |
:param item: Completion item to format. | |
""" | |
raise NotImplementedError | |
def complete(self) -> str: | |
"""Produce the completion data to send back to the shell. | |
By default this calls :meth:`get_completion_args`, gets the | |
completions, then calls :meth:`format_completion` for each | |
completion. | |
""" | |
args, incomplete = self.get_completion_args() | |
completions = self.get_completions(args, incomplete) | |
out = [self.format_completion(item) for item in completions] | |
return "\n".join(out) | |
class BashComplete(ShellComplete): | |
"""Shell completion for Bash.""" | |
name = "bash" | |
source_template = _SOURCE_BASH | |
def _check_version(self) -> None: | |
import subprocess | |
output = subprocess.run( | |
["bash", "-c", "echo ${BASH_VERSION}"], stdout=subprocess.PIPE | |
) | |
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) | |
if match is not None: | |
major, minor = match.groups() | |
if major < "4" or major == "4" and minor < "4": | |
raise RuntimeError( | |
_( | |
"Shell completion is not supported for Bash" | |
" versions older than 4.4." | |
) | |
) | |
else: | |
raise RuntimeError( | |
_("Couldn't detect Bash version, shell completion is not supported.") | |
) | |
def source(self) -> str: | |
self._check_version() | |
return super().source() | |
def get_completion_args(self) -> t.Tuple[t.List[str], str]: | |
cwords = split_arg_string(os.environ["COMP_WORDS"]) | |
cword = int(os.environ["COMP_CWORD"]) | |
args = cwords[1:cword] | |
try: | |
incomplete = cwords[cword] | |
except IndexError: | |
incomplete = "" | |
return args, incomplete | |
def format_completion(self, item: CompletionItem) -> str: | |
return f"{item.type},{item.value}" | |
class ZshComplete(ShellComplete): | |
"""Shell completion for Zsh.""" | |
name = "zsh" | |
source_template = _SOURCE_ZSH | |
def get_completion_args(self) -> t.Tuple[t.List[str], str]: | |
cwords = split_arg_string(os.environ["COMP_WORDS"]) | |
cword = int(os.environ["COMP_CWORD"]) | |
args = cwords[1:cword] | |
try: | |
incomplete = cwords[cword] | |
except IndexError: | |
incomplete = "" | |
return args, incomplete | |
def format_completion(self, item: CompletionItem) -> str: | |
return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" | |
class FishComplete(ShellComplete): | |
"""Shell completion for Fish.""" | |
name = "fish" | |
source_template = _SOURCE_FISH | |
def get_completion_args(self) -> t.Tuple[t.List[str], str]: | |
cwords = split_arg_string(os.environ["COMP_WORDS"]) | |
incomplete = os.environ["COMP_CWORD"] | |
args = cwords[1:] | |
# Fish stores the partial word in both COMP_WORDS and | |
# COMP_CWORD, remove it from complete args. | |
if incomplete and args and args[-1] == incomplete: | |
args.pop() | |
return args, incomplete | |
def format_completion(self, item: CompletionItem) -> str: | |
if item.help: | |
return f"{item.type},{item.value}\t{item.help}" | |
return f"{item.type},{item.value}" | |
_available_shells: t.Dict[str, t.Type[ShellComplete]] = { | |
"bash": BashComplete, | |
"fish": FishComplete, | |
"zsh": ZshComplete, | |
} | |
def add_completion_class( | |
cls: t.Type[ShellComplete], name: t.Optional[str] = None | |
) -> None: | |
"""Register a :class:`ShellComplete` subclass under the given name. | |
The name will be provided by the completion instruction environment | |
variable during completion. | |
:param cls: The completion class that will handle completion for the | |
shell. | |
:param name: Name to register the class under. Defaults to the | |
class's ``name`` attribute. | |
""" | |
if name is None: | |
name = cls.name | |
_available_shells[name] = cls | |
def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]: | |
"""Look up a registered :class:`ShellComplete` subclass by the name | |
provided by the completion instruction environment variable. If the | |
name isn't registered, returns ``None``. | |
:param shell: Name the class is registered under. | |
""" | |
return _available_shells.get(shell) | |
def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: | |
"""Determine if the given parameter is an argument that can still | |
accept values. | |
:param ctx: Invocation context for the command represented by the | |
parsed complete args. | |
:param param: Argument object being checked. | |
""" | |
if not isinstance(param, Argument): | |
return False | |
assert param.name is not None | |
value = ctx.params[param.name] | |
return ( | |
param.nargs == -1 | |
or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE | |
or ( | |
param.nargs > 1 | |
and isinstance(value, (tuple, list)) | |
and len(value) < param.nargs | |
) | |
) | |
def _start_of_option(ctx: Context, value: str) -> bool: | |
"""Check if the value looks like the start of an option.""" | |
if not value: | |
return False | |
c = value[0] | |
return c in ctx._opt_prefixes | |
def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool: | |
"""Determine if the given parameter is an option that needs a value. | |
:param args: List of complete args before the incomplete value. | |
:param param: Option object being checked. | |
""" | |
if not isinstance(param, Option): | |
return False | |
if param.is_flag or param.count: | |
return False | |
last_option = None | |
for index, arg in enumerate(reversed(args)): | |
if index + 1 > param.nargs: | |
break | |
if _start_of_option(ctx, arg): | |
last_option = arg | |
return last_option is not None and last_option in param.opts | |
def _resolve_context( | |
cli: BaseCommand, ctx_args: t.Dict[str, t.Any], prog_name: str, args: t.List[str] | |
) -> Context: | |
"""Produce the context hierarchy starting with the command and | |
traversing the complete arguments. This only follows the commands, | |
it doesn't trigger input prompts or callbacks. | |
:param cli: Command being called. | |
:param prog_name: Name of the executable in the shell. | |
:param args: List of complete args before the incomplete value. | |
""" | |
ctx_args["resilient_parsing"] = True | |
ctx = cli.make_context(prog_name, args.copy(), **ctx_args) | |
args = ctx.protected_args + ctx.args | |
while args: | |
command = ctx.command | |
if isinstance(command, MultiCommand): | |
if not command.chain: | |
name, cmd, args = command.resolve_command(ctx, args) | |
if cmd is None: | |
return ctx | |
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True) | |
args = ctx.protected_args + ctx.args | |
else: | |
while args: | |
name, cmd, args = command.resolve_command(ctx, args) | |
if cmd is None: | |
return ctx | |
sub_ctx = cmd.make_context( | |
name, | |
args, | |
parent=ctx, | |
allow_extra_args=True, | |
allow_interspersed_args=False, | |
resilient_parsing=True, | |
) | |
args = sub_ctx.args | |
ctx = sub_ctx | |
args = [*sub_ctx.protected_args, *sub_ctx.args] | |
else: | |
break | |
return ctx | |
def _resolve_incomplete( | |
ctx: Context, args: t.List[str], incomplete: str | |
) -> t.Tuple[t.Union[BaseCommand, Parameter], str]: | |
"""Find the Click object that will handle the completion of the | |
incomplete value. Return the object and the incomplete value. | |
:param ctx: Invocation context for the command represented by | |
the parsed complete args. | |
:param args: List of complete args before the incomplete value. | |
:param incomplete: Value being completed. May be empty. | |
""" | |
# Different shells treat an "=" between a long option name and | |
# value differently. Might keep the value joined, return the "=" | |
# as a separate item, or return the split name and value. Always | |
# split and discard the "=" to make completion easier. | |
if incomplete == "=": | |
incomplete = "" | |
elif "=" in incomplete and _start_of_option(ctx, incomplete): | |
name, _, incomplete = incomplete.partition("=") | |
args.append(name) | |
# The "--" marker tells Click to stop treating values as options | |
# even if they start with the option character. If it hasn't been | |
# given and the incomplete arg looks like an option, the current | |
# command will provide option name completions. | |
if "--" not in args and _start_of_option(ctx, incomplete): | |
return ctx.command, incomplete | |
params = ctx.command.get_params(ctx) | |
# If the last complete arg is an option name with an incomplete | |
# value, the option will provide value completions. | |
for param in params: | |
if _is_incomplete_option(ctx, args, param): | |
return param, incomplete | |
# It's not an option name or value. The first argument without a | |
# parsed value will provide value completions. | |
for param in params: | |
if _is_incomplete_argument(ctx, param): | |
return param, incomplete | |
# There were no unparsed arguments, the command may be a group that | |
# will provide command name completions. | |
return ctx.command, incomplete | |