| from __future__ import annotations
|
|
|
| import collections.abc as cabc
|
| import os
|
| import re
|
| import typing as t
|
| from gettext import gettext as _
|
|
|
| from .core import Argument
|
| from .core import Command
|
| from .core import Context
|
| from .core import Group
|
| from .core import Option
|
| from .core import Parameter
|
| from .core import ParameterSource
|
| from .utils import echo
|
|
|
|
|
| def shell_complete(
|
| cli: Command,
|
| ctx_args: cabc.MutableMapping[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: str | None = None,
|
| **kwargs: t.Any,
|
| ) -> None:
|
| self.value: t.Any = value
|
| self.type: str = type
|
| self.help: str | None = help
|
| self._info = kwargs
|
|
|
| def __getattr__(self, name: str) -> t.Any:
|
| return self._info.get(name)
|
|
|
|
|
|
|
| _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
|
| }
|
|
|
| if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
|
| # autoload from fpath, call function directly
|
| %(complete_func)s "$@"
|
| else
|
| # eval/source/. command, register function for later
|
| compdef %(complete_func)s %(prog_name)s
|
| fi
|
| """
|
|
|
| _SOURCE_FISH = """\
|
| function %(complete_func)s;
|
| set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \
|
| COMP_CWORD=(commandline -t) %(prog_name)s);
|
|
|
| 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: Command,
|
| ctx_args: cabc.MutableMapping[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
|
|
|
| @property
|
| 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("-", "_"), flags=re.ASCII)
|
| return f"_{safe_name}_completion"
|
|
|
| def source_vars(self) -> 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) -> tuple[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: list[str], incomplete: str) -> 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
|
|
|
| @staticmethod
|
| def _check_version() -> None:
|
| import shutil
|
| import subprocess
|
|
|
| bash_exe = shutil.which("bash")
|
|
|
| if bash_exe is None:
|
| match = None
|
| else:
|
| output = subprocess.run(
|
| [bash_exe, "--norc", "-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":
|
| echo(
|
| _(
|
| "Shell completion is not supported for Bash"
|
| " versions older than 4.4."
|
| ),
|
| err=True,
|
| )
|
| else:
|
| echo(
|
| _("Couldn't detect Bash version, shell completion is not supported."),
|
| err=True,
|
| )
|
|
|
| def source(self) -> str:
|
| self._check_version()
|
| return super().source()
|
|
|
| def get_completion_args(self) -> tuple[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) -> tuple[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:
|
| help_ = item.help or "_"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| value = item.value.replace(":", r"\:") if help_ != "_" else item.value
|
| return f"{item.type}\n{value}\n{help_}"
|
|
|
|
|
| class FishComplete(ShellComplete):
|
| """Shell completion for Fish."""
|
|
|
| name = "fish"
|
| source_template = _SOURCE_FISH
|
|
|
| def get_completion_args(self) -> tuple[list[str], str]:
|
| cwords = split_arg_string(os.environ["COMP_WORDS"])
|
| incomplete = os.environ["COMP_CWORD"]
|
| if incomplete:
|
| incomplete = split_arg_string(incomplete)[0]
|
| args = cwords[1:]
|
|
|
|
|
|
|
| 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}"
|
|
|
|
|
| ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]")
|
|
|
|
|
| _available_shells: dict[str, type[ShellComplete]] = {
|
| "bash": BashComplete,
|
| "fish": FishComplete,
|
| "zsh": ZshComplete,
|
| }
|
|
|
|
|
| def add_completion_class(
|
| cls: ShellCompleteType, name: str | None = None
|
| ) -> ShellCompleteType:
|
| """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
|
|
|
| return cls
|
|
|
|
|
| def get_completion_class(shell: str) -> type[ShellComplete] | None:
|
| """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 split_arg_string(string: str) -> list[str]:
|
| """Split an argument string as with :func:`shlex.split`, but don't
|
| fail if the string is incomplete. Ignores a missing closing quote or
|
| incomplete escape sequence and uses the partial token as-is.
|
|
|
| .. code-block:: python
|
|
|
| split_arg_string("example 'my file")
|
| ["example", "my file"]
|
|
|
| split_arg_string("example my\\")
|
| ["example", "my"]
|
|
|
| :param string: String to split.
|
|
|
| .. versionchanged:: 8.2
|
| Moved to ``shell_completion`` from ``parser``.
|
| """
|
| import shlex
|
|
|
| lex = shlex.shlex(string, posix=True)
|
| lex.whitespace_split = True
|
| lex.commenters = ""
|
| out = []
|
|
|
| try:
|
| for token in lex:
|
| out.append(token)
|
| except ValueError:
|
|
|
|
|
|
|
| out.append(lex.token)
|
|
|
| return out
|
|
|
|
|
| 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.get(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: 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
|
| break
|
|
|
| return last_option is not None and last_option in param.opts
|
|
|
|
|
| def _resolve_context(
|
| cli: Command,
|
| ctx_args: cabc.MutableMapping[str, t.Any],
|
| prog_name: str,
|
| args: 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
|
| with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx:
|
| args = ctx._protected_args + ctx.args
|
|
|
| while args:
|
| command = ctx.command
|
|
|
| if isinstance(command, Group):
|
| if not command.chain:
|
| name, cmd, args = command.resolve_command(ctx, args)
|
|
|
| if cmd is None:
|
| return ctx
|
|
|
| with cmd.make_context(
|
| name, args, parent=ctx, resilient_parsing=True
|
| ) as sub_ctx:
|
| ctx = sub_ctx
|
| args = ctx._protected_args + ctx.args
|
| else:
|
| sub_ctx = ctx
|
|
|
| while args:
|
| name, cmd, args = command.resolve_command(ctx, args)
|
|
|
| if cmd is None:
|
| return ctx
|
|
|
| with cmd.make_context(
|
| name,
|
| args,
|
| parent=ctx,
|
| allow_extra_args=True,
|
| allow_interspersed_args=False,
|
| resilient_parsing=True,
|
| ) as sub_sub_ctx:
|
| sub_ctx = sub_sub_ctx
|
| 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: list[str], incomplete: str
|
| ) -> tuple[Command | 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.
|
| """
|
|
|
|
|
|
|
|
|
| if incomplete == "=":
|
| incomplete = ""
|
| elif "=" in incomplete and _start_of_option(ctx, incomplete):
|
| name, _, incomplete = incomplete.partition("=")
|
| args.append(name)
|
|
|
|
|
|
|
|
|
|
|
| if "--" not in args and _start_of_option(ctx, incomplete):
|
| return ctx.command, incomplete
|
|
|
| params = ctx.command.get_params(ctx)
|
|
|
|
|
|
|
| for param in params:
|
| if _is_incomplete_option(ctx, args, param):
|
| return param, incomplete
|
|
|
|
|
|
|
| for param in params:
|
| if _is_incomplete_argument(ctx, param):
|
| return param, incomplete
|
|
|
|
|
|
|
| return ctx.command, incomplete
|
|
|