| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 | import osimport reimport typing as tfrom gettext import gettext as _from .core import Argumentfrom .core import BaseCommandfrom .core import Contextfrom .core import MultiCommandfrom .core import Optionfrom .core import Parameterfrom .core import ParameterSourcefrom .parser import split_arg_stringfrom .utils import echodef shell_complete(    cli: BaseCommand,    ctx_args: t.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 1class 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: t.Any = value        self.type: str = type        self.help: t.Optional[str] = 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}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)sfi"""_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: BaseCommand,        ctx_args: t.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) -> 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    @staticmethod    def _check_version() -> 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":                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) -> 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}"ShellCompleteType = t.TypeVar("ShellCompleteType", bound=t.Type[ShellComplete])_available_shells: t.Dict[str, t.Type[ShellComplete]] = {    "bash": BashComplete,    "fish": FishComplete,    "zsh": ZshComplete,}def add_completion_class(    cls: ShellCompleteType, name: t.Optional[str] = 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 clsdef 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    # Will be None if expose_value is False.    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_prefixesdef _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.optsdef _resolve_context(    cli: BaseCommand,    ctx_args: t.MutableMapping[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:                sub_ctx = ctx                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 ctxdef _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
 |