Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
shell_completion.py668 linesDownload Raw Back to click
1from __future__ import annotations2 3import collections.abc as cabc4import os5import re6import typing as t7from gettext import gettext as _8 9from .core import Argument10from .core import Command11from .core import Context12from .core import Group13from .core import Option14from .core import Parameter15from .core import ParameterSource16from .utils import echo17 18 19def shell_complete(20    cli: Command,21    ctx_args: cabc.MutableMapping[str, t.Any],22    prog_name: str,23    complete_var: str,24    instruction: str,25) -> int:26    """Perform shell completion for the given CLI program.27 28    :param cli: Command being called.29    :param ctx_args: Extra arguments to pass to30        ``cli.make_context``.31    :param prog_name: Name of the executable in the shell.32    :param complete_var: Name of the environment variable that holds33        the completion instruction.34    :param instruction: Value of ``complete_var`` with the completion35        instruction and shell, in the form ``instruction_shell``.36    :return: Status code to exit with.37    """38    shell, _, instruction = instruction.partition("_")39    comp_cls = get_completion_class(shell)40 41    if comp_cls is None:42        return 143 44    comp = comp_cls(cli, ctx_args, prog_name, complete_var)45 46    if instruction == "source":47        echo(comp.source())48        return 049 50    if instruction == "complete":51        echo(comp.complete())52        return 053 54    return 155 56 57class CompletionItem:58    """Represents a completion value and metadata about the value. The59    default metadata is ``type`` to indicate special shell handling,60    and ``help`` if a shell supports showing a help string next to the61    value.62 63    Arbitrary parameters can be passed when creating the object, and64    accessed using ``item.attr``. If an attribute wasn't passed,65    accessing it returns ``None``.66 67    :param value: The completion suggestion.68    :param type: Tells the shell script to provide special completion69        support for the type. Click uses ``"dir"`` and ``"file"``.70    :param help: String shown next to the value if supported.71    :param kwargs: Arbitrary metadata. The built-in implementations72        don't use this, but custom type completions paired with custom73        shell support could use it.74    """75 76    __slots__ = ("value", "type", "help", "_info")77 78    def __init__(79        self,80        value: t.Any,81        type: str = "plain",82        help: str | None = None,83        **kwargs: t.Any,84    ) -> None:85        self.value: t.Any = value86        self.type: str = type87        self.help: str | None = help88        self._info = kwargs89 90    def __getattr__(self, name: str) -> t.Any:91        return self._info.get(name)92 93 94# Only Bash >= 4.4 has the nosort option.95_SOURCE_BASH = """\96%(complete_func)s() {97    local IFS=$'\\n'98    local response99 100    response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \101%(complete_var)s=bash_complete $1)102 103    for completion in $response; do104        IFS=',' read type value <<< "$completion"105 106        if [[ $type == 'dir' ]]; then107            COMPREPLY=()108            compopt -o dirnames109        elif [[ $type == 'file' ]]; then110            COMPREPLY=()111            compopt -o default112        elif [[ $type == 'plain' ]]; then113            COMPREPLY+=($value)114        fi115    done116 117    return 0118}119 120%(complete_func)s_setup() {121    complete -o nosort -F %(complete_func)s %(prog_name)s122}123 124%(complete_func)s_setup;125"""126 127# See ZshComplete.format_completion below, and issue #2703, before128# changing this script.129#130# (TL;DR: _describe is picky about the format, but this Zsh script snippet131# is already widely deployed.  So freeze this script, and use clever-ish132# handling of colons in ZshComplet.format_completion.)133_SOURCE_ZSH = """\134#compdef %(prog_name)s135 136%(complete_func)s() {137    local -a completions138    local -a completions_with_descriptions139    local -a response140    (( ! $+commands[%(prog_name)s] )) && return 1141 142    response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \143%(complete_var)s=zsh_complete %(prog_name)s)}")144 145    for type key descr in ${response}; do146        if [[ "$type" == "plain" ]]; then147            if [[ "$descr" == "_" ]]; then148                completions+=("$key")149            else150                completions_with_descriptions+=("$key":"$descr")151            fi152        elif [[ "$type" == "dir" ]]; then153            _path_files -/154        elif [[ "$type" == "file" ]]; then155            _path_files -f156        fi157    done158 159    if [ -n "$completions_with_descriptions" ]; then160        _describe -V unsorted completions_with_descriptions -U161    fi162 163    if [ -n "$completions" ]; then164        compadd -U -V unsorted -a completions165    fi166}167 168if [[ $zsh_eval_context[-1] == loadautofunc ]]; then169    # autoload from fpath, call function directly170    %(complete_func)s "$@"171else172    # eval/source/. command, register function for later173    compdef %(complete_func)s %(prog_name)s174fi175"""176 177_SOURCE_FISH = """\178function %(complete_func)s;179    set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \180COMP_CWORD=(commandline -t) %(prog_name)s);181 182    for completion in $response;183        set -l metadata (string split "," $completion);184 185        if test $metadata[1] = "dir";186            __fish_complete_directories $metadata[2];187        else if test $metadata[1] = "file";188            __fish_complete_path $metadata[2];189        else if test $metadata[1] = "plain";190            echo $metadata[2];191        end;192    end;193end;194 195complete --no-files --command %(prog_name)s --arguments \196"(%(complete_func)s)";197"""198 199 200class ShellComplete:201    """Base class for providing shell completion support. A subclass for202    a given shell will override attributes and methods to implement the203    completion instructions (``source`` and ``complete``).204 205    :param cli: Command being called.206    :param prog_name: Name of the executable in the shell.207    :param complete_var: Name of the environment variable that holds208        the completion instruction.209 210    .. versionadded:: 8.0211    """212 213    name: t.ClassVar[str]214    """Name to register the shell as with :func:`add_completion_class`.215    This is used in completion instructions (``{name}_source`` and216    ``{name}_complete``).217    """218 219    source_template: t.ClassVar[str]220    """Completion script template formatted by :meth:`source`. This must221    be provided by subclasses.222    """223 224    def __init__(225        self,226        cli: Command,227        ctx_args: cabc.MutableMapping[str, t.Any],228        prog_name: str,229        complete_var: str,230    ) -> None:231        self.cli = cli232        self.ctx_args = ctx_args233        self.prog_name = prog_name234        self.complete_var = complete_var235 236    @property237    def func_name(self) -> str:238        """The name of the shell function defined by the completion239        script.240        """241        safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII)242        return f"_{safe_name}_completion"243 244    def source_vars(self) -> dict[str, t.Any]:245        """Vars for formatting :attr:`source_template`.246 247        By default this provides ``complete_func``, ``complete_var``,248        and ``prog_name``.249        """250        return {251            "complete_func": self.func_name,252            "complete_var": self.complete_var,253            "prog_name": self.prog_name,254        }255 256    def source(self) -> str:257        """Produce the shell script that defines the completion258        function. By default this ``%``-style formats259        :attr:`source_template` with the dict returned by260        :meth:`source_vars`.261        """262        return self.source_template % self.source_vars()263 264    def get_completion_args(self) -> tuple[list[str], str]:265        """Use the env vars defined by the shell script to return a266        tuple of ``args, incomplete``. This must be implemented by267        subclasses.268        """269        raise NotImplementedError270 271    def get_completions(self, args: list[str], incomplete: str) -> list[CompletionItem]:272        """Determine the context and last complete command or parameter273        from the complete args. Call that object's ``shell_complete``274        method to get the completions for the incomplete value.275 276        :param args: List of complete args before the incomplete value.277        :param incomplete: Value being completed. May be empty.278        """279        ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)280        obj, incomplete = _resolve_incomplete(ctx, args, incomplete)281        return obj.shell_complete(ctx, incomplete)282 283    def format_completion(self, item: CompletionItem) -> str:284        """Format a completion item into the form recognized by the285        shell script. This must be implemented by subclasses.286 287        :param item: Completion item to format.288        """289        raise NotImplementedError290 291    def complete(self) -> str:292        """Produce the completion data to send back to the shell.293 294        By default this calls :meth:`get_completion_args`, gets the295        completions, then calls :meth:`format_completion` for each296        completion.297        """298        args, incomplete = self.get_completion_args()299        completions = self.get_completions(args, incomplete)300        out = [self.format_completion(item) for item in completions]301        return "\n".join(out)302 303 304class BashComplete(ShellComplete):305    """Shell completion for Bash."""306 307    name = "bash"308    source_template = _SOURCE_BASH309 310    @staticmethod311    def _check_version() -> None:312        import shutil313        import subprocess314 315        bash_exe = shutil.which("bash")316 317        if bash_exe is None:318            match = None319        else:320            output = subprocess.run(321                [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'],322                stdout=subprocess.PIPE,323            )324            match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())325 326        if match is not None:327            major, minor = match.groups()328 329            if major < "4" or major == "4" and minor < "4":330                echo(331                    _(332                        "Shell completion is not supported for Bash"333                        " versions older than 4.4."334                    ),335                    err=True,336                )337        else:338            echo(339                _("Couldn't detect Bash version, shell completion is not supported."),340                err=True,341            )342 343    def source(self) -> str:344        self._check_version()345        return super().source()346 347    def get_completion_args(self) -> tuple[list[str], str]:348        cwords = split_arg_string(os.environ["COMP_WORDS"])349        cword = int(os.environ["COMP_CWORD"])350        args = cwords[1:cword]351 352        try:353            incomplete = cwords[cword]354        except IndexError:355            incomplete = ""356 357        return args, incomplete358 359    def format_completion(self, item: CompletionItem) -> str:360        return f"{item.type},{item.value}"361 362 363class ZshComplete(ShellComplete):364    """Shell completion for Zsh."""365 366    name = "zsh"367    source_template = _SOURCE_ZSH368 369    def get_completion_args(self) -> tuple[list[str], str]:370        cwords = split_arg_string(os.environ["COMP_WORDS"])371        cword = int(os.environ["COMP_CWORD"])372        args = cwords[1:cword]373 374        try:375            incomplete = cwords[cword]376        except IndexError:377            incomplete = ""378 379        return args, incomplete380 381    def format_completion(self, item: CompletionItem) -> str:382        help_ = item.help or "_"383        # The zsh completion script uses `_describe` on items with help384        # texts (which splits the item help from the item value at the385        # first unescaped colon) and `compadd` on items without help386        # text (which uses the item value as-is and does not support387        # colon escaping).  So escape colons in the item value if and388        # only if the item help is not the sentinel "_" value, as used389        # by the completion script.390        #391        # (The zsh completion script is potentially widely deployed, and392        # thus harder to fix than this method.)393        #394        # See issue #1812 and issue #2703 for further context.395        value = item.value.replace(":", r"\:") if help_ != "_" else item.value396        return f"{item.type}\n{value}\n{help_}"397 398 399class FishComplete(ShellComplete):400    """Shell completion for Fish."""401 402    name = "fish"403    source_template = _SOURCE_FISH404 405    def get_completion_args(self) -> tuple[list[str], str]:406        cwords = split_arg_string(os.environ["COMP_WORDS"])407        incomplete = os.environ["COMP_CWORD"]408        if incomplete:409            incomplete = split_arg_string(incomplete)[0]410        args = cwords[1:]411 412        # Fish stores the partial word in both COMP_WORDS and413        # COMP_CWORD, remove it from complete args.414        if incomplete and args and args[-1] == incomplete:415            args.pop()416 417        return args, incomplete418 419    def format_completion(self, item: CompletionItem) -> str:420        if item.help:421            return f"{item.type},{item.value}\t{item.help}"422 423        return f"{item.type},{item.value}"424 425 426ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]")427 428 429_available_shells: dict[str, type[ShellComplete]] = {430    "bash": BashComplete,431    "fish": FishComplete,432    "zsh": ZshComplete,433}434 435 436def add_completion_class(437    cls: ShellCompleteType, name: str | None = None438) -> ShellCompleteType:439    """Register a :class:`ShellComplete` subclass under the given name.440    The name will be provided by the completion instruction environment441    variable during completion.442 443    :param cls: The completion class that will handle completion for the444        shell.445    :param name: Name to register the class under. Defaults to the446        class's ``name`` attribute.447    """448    if name is None:449        name = cls.name450 451    _available_shells[name] = cls452 453    return cls454 455 456def get_completion_class(shell: str) -> type[ShellComplete] | None:457    """Look up a registered :class:`ShellComplete` subclass by the name458    provided by the completion instruction environment variable. If the459    name isn't registered, returns ``None``.460 461    :param shell: Name the class is registered under.462    """463    return _available_shells.get(shell)464 465 466def split_arg_string(string: str) -> list[str]:467    """Split an argument string as with :func:`shlex.split`, but don't468    fail if the string is incomplete. Ignores a missing closing quote or469    incomplete escape sequence and uses the partial token as-is.470 471    .. code-block:: python472 473        split_arg_string("example 'my file")474        ["example", "my file"]475 476        split_arg_string("example my\\")477        ["example", "my"]478 479    :param string: String to split.480 481    .. versionchanged:: 8.2482        Moved to ``shell_completion`` from ``parser``.483    """484    import shlex485 486    lex = shlex.shlex(string, posix=True)487    lex.whitespace_split = True488    lex.commenters = ""489    out = []490 491    try:492        for token in lex:493            out.append(token)494    except ValueError:495        # Raised when end-of-string is reached in an invalid state. Use496        # the partial token as-is. The quote or escape character is in497        # lex.state, not lex.token.498        out.append(lex.token)499 500    return out501 502 503def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:504    """Determine if the given parameter is an argument that can still505    accept values.506 507    :param ctx: Invocation context for the command represented by the508        parsed complete args.509    :param param: Argument object being checked.510    """511    if not isinstance(param, Argument):512        return False513 514    assert param.name is not None515    # Will be None if expose_value is False.516    value = ctx.params.get(param.name)517    return (518        param.nargs == -1519        or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE520        or (521            param.nargs > 1522            and isinstance(value, (tuple, list))523            and len(value) < param.nargs524        )525    )526 527 528def _start_of_option(ctx: Context, value: str) -> bool:529    """Check if the value looks like the start of an option."""530    if not value:531        return False532 533    c = value[0]534    return c in ctx._opt_prefixes535 536 537def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool:538    """Determine if the given parameter is an option that needs a value.539 540    :param args: List of complete args before the incomplete value.541    :param param: Option object being checked.542    """543    if not isinstance(param, Option):544        return False545 546    if param.is_flag or param.count:547        return False548 549    last_option = None550 551    for index, arg in enumerate(reversed(args)):552        if index + 1 > param.nargs:553            break554 555        if _start_of_option(ctx, arg):556            last_option = arg557            break558 559    return last_option is not None and last_option in param.opts560 561 562def _resolve_context(563    cli: Command,564    ctx_args: cabc.MutableMapping[str, t.Any],565    prog_name: str,566    args: list[str],567) -> Context:568    """Produce the context hierarchy starting with the command and569    traversing the complete arguments. This only follows the commands,570    it doesn't trigger input prompts or callbacks.571 572    :param cli: Command being called.573    :param prog_name: Name of the executable in the shell.574    :param args: List of complete args before the incomplete value.575    """576    ctx_args["resilient_parsing"] = True577    with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx:578        args = ctx._protected_args + ctx.args579 580        while args:581            command = ctx.command582 583            if isinstance(command, Group):584                if not command.chain:585                    name, cmd, args = command.resolve_command(ctx, args)586 587                    if cmd is None:588                        return ctx589 590                    with cmd.make_context(591                        name, args, parent=ctx, resilient_parsing=True592                    ) as sub_ctx:593                        ctx = sub_ctx594                        args = ctx._protected_args + ctx.args595                else:596                    sub_ctx = ctx597 598                    while args:599                        name, cmd, args = command.resolve_command(ctx, args)600 601                        if cmd is None:602                            return ctx603 604                        with cmd.make_context(605                            name,606                            args,607                            parent=ctx,608                            allow_extra_args=True,609                            allow_interspersed_args=False,610                            resilient_parsing=True,611                        ) as sub_sub_ctx:612                            sub_ctx = sub_sub_ctx613                            args = sub_ctx.args614 615                    ctx = sub_ctx616                    args = [*sub_ctx._protected_args, *sub_ctx.args]617            else:618                break619 620    return ctx621 622 623def _resolve_incomplete(624    ctx: Context, args: list[str], incomplete: str625) -> tuple[Command | Parameter, str]:626    """Find the Click object that will handle the completion of the627    incomplete value. Return the object and the incomplete value.628 629    :param ctx: Invocation context for the command represented by630        the parsed complete args.631    :param args: List of complete args before the incomplete value.632    :param incomplete: Value being completed. May be empty.633    """634    # Different shells treat an "=" between a long option name and635    # value differently. Might keep the value joined, return the "="636    # as a separate item, or return the split name and value. Always637    # split and discard the "=" to make completion easier.638    if incomplete == "=":639        incomplete = ""640    elif "=" in incomplete and _start_of_option(ctx, incomplete):641        name, _, incomplete = incomplete.partition("=")642        args.append(name)643 644    # The "--" marker tells Click to stop treating values as options645    # even if they start with the option character. If it hasn't been646    # given and the incomplete arg looks like an option, the current647    # command will provide option name completions.648    if "--" not in args and _start_of_option(ctx, incomplete):649        return ctx.command, incomplete650 651    params = ctx.command.get_params(ctx)652 653    # If the last complete arg is an option name with an incomplete654    # value, the option will provide value completions.655    for param in params:656        if _is_incomplete_option(ctx, args, param):657            return param, incomplete658 659    # It's not an option name or value. The first argument without a660    # parsed value will provide value completions.661    for param in params:662        if _is_incomplete_argument(ctx, param):663            return param, incomplete664 665    # There were no unparsed arguments, the command may be a group that666    # will provide command name completions.667    return ctx.command, incomplete668 
codekingpro/portable-devtools · Team Ai