Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
shell_completion.py597 linesDownload Raw Back to click
1import os2import re3import typing as t4from gettext import gettext as _5 6from .core import Argument7from .core import BaseCommand8from .core import Context9from .core import MultiCommand10from .core import Option11from .core import Parameter12from .core import ParameterSource13from .parser import split_arg_string14from .utils import echo15 16 17def shell_complete(18    cli: BaseCommand,19    ctx_args: t.MutableMapping[str, t.Any],20    prog_name: str,21    complete_var: str,22    instruction: str,23) -> int:24    """Perform shell completion for the given CLI program.25 26    :param cli: Command being called.27    :param ctx_args: Extra arguments to pass to28        ``cli.make_context``.29    :param prog_name: Name of the executable in the shell.30    :param complete_var: Name of the environment variable that holds31        the completion instruction.32    :param instruction: Value of ``complete_var`` with the completion33        instruction and shell, in the form ``instruction_shell``.34    :return: Status code to exit with.35    """36    shell, _, instruction = instruction.partition("_")37    comp_cls = get_completion_class(shell)38 39    if comp_cls is None:40        return 141 42    comp = comp_cls(cli, ctx_args, prog_name, complete_var)43 44    if instruction == "source":45        echo(comp.source())46        return 047 48    if instruction == "complete":49        echo(comp.complete())50        return 051 52    return 153 54 55class CompletionItem:56    """Represents a completion value and metadata about the value. The57    default metadata is ``type`` to indicate special shell handling,58    and ``help`` if a shell supports showing a help string next to the59    value.60 61    Arbitrary parameters can be passed when creating the object, and62    accessed using ``item.attr``. If an attribute wasn't passed,63    accessing it returns ``None``.64 65    :param value: The completion suggestion.66    :param type: Tells the shell script to provide special completion67        support for the type. Click uses ``"dir"`` and ``"file"``.68    :param help: String shown next to the value if supported.69    :param kwargs: Arbitrary metadata. The built-in implementations70        don't use this, but custom type completions paired with custom71        shell support could use it.72    """73 74    __slots__ = ("value", "type", "help", "_info")75 76    def __init__(77        self,78        value: t.Any,79        type: str = "plain",80        help: t.Optional[str] = None,81        **kwargs: t.Any,82    ) -> None:83        self.value: t.Any = value84        self.type: str = type85        self.help: t.Optional[str] = help86        self._info = kwargs87 88    def __getattr__(self, name: str) -> t.Any:89        return self._info.get(name)90 91 92# Only Bash >= 4.4 has the nosort option.93_SOURCE_BASH = """\94%(complete_func)s() {95    local IFS=$'\\n'96    local response97 98    response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \99%(complete_var)s=bash_complete $1)100 101    for completion in $response; do102        IFS=',' read type value <<< "$completion"103 104        if [[ $type == 'dir' ]]; then105            COMPREPLY=()106            compopt -o dirnames107        elif [[ $type == 'file' ]]; then108            COMPREPLY=()109            compopt -o default110        elif [[ $type == 'plain' ]]; then111            COMPREPLY+=($value)112        fi113    done114 115    return 0116}117 118%(complete_func)s_setup() {119    complete -o nosort -F %(complete_func)s %(prog_name)s120}121 122%(complete_func)s_setup;123"""124 125_SOURCE_ZSH = """\126#compdef %(prog_name)s127 128%(complete_func)s() {129    local -a completions130    local -a completions_with_descriptions131    local -a response132    (( ! $+commands[%(prog_name)s] )) && return 1133 134    response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \135%(complete_var)s=zsh_complete %(prog_name)s)}")136 137    for type key descr in ${response}; do138        if [[ "$type" == "plain" ]]; then139            if [[ "$descr" == "_" ]]; then140                completions+=("$key")141            else142                completions_with_descriptions+=("$key":"$descr")143            fi144        elif [[ "$type" == "dir" ]]; then145            _path_files -/146        elif [[ "$type" == "file" ]]; then147            _path_files -f148        fi149    done150 151    if [ -n "$completions_with_descriptions" ]; then152        _describe -V unsorted completions_with_descriptions -U153    fi154 155    if [ -n "$completions" ]; then156        compadd -U -V unsorted -a completions157    fi158}159 160if [[ $zsh_eval_context[-1] == loadautofunc ]]; then161    # autoload from fpath, call function directly162    %(complete_func)s "$@"163else164    # eval/source/. command, register function for later165    compdef %(complete_func)s %(prog_name)s166fi167"""168 169_SOURCE_FISH = """\170function %(complete_func)s;171    set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \172COMP_CWORD=(commandline -t) %(prog_name)s);173 174    for completion in $response;175        set -l metadata (string split "," $completion);176 177        if test $metadata[1] = "dir";178            __fish_complete_directories $metadata[2];179        else if test $metadata[1] = "file";180            __fish_complete_path $metadata[2];181        else if test $metadata[1] = "plain";182            echo $metadata[2];183        end;184    end;185end;186 187complete --no-files --command %(prog_name)s --arguments \188"(%(complete_func)s)";189"""190 191 192class ShellComplete:193    """Base class for providing shell completion support. A subclass for194    a given shell will override attributes and methods to implement the195    completion instructions (``source`` and ``complete``).196 197    :param cli: Command being called.198    :param prog_name: Name of the executable in the shell.199    :param complete_var: Name of the environment variable that holds200        the completion instruction.201 202    .. versionadded:: 8.0203    """204 205    name: t.ClassVar[str]206    """Name to register the shell as with :func:`add_completion_class`.207    This is used in completion instructions (``{name}_source`` and208    ``{name}_complete``).209    """210 211    source_template: t.ClassVar[str]212    """Completion script template formatted by :meth:`source`. This must213    be provided by subclasses.214    """215 216    def __init__(217        self,218        cli: BaseCommand,219        ctx_args: t.MutableMapping[str, t.Any],220        prog_name: str,221        complete_var: str,222    ) -> None:223        self.cli = cli224        self.ctx_args = ctx_args225        self.prog_name = prog_name226        self.complete_var = complete_var227 228    @property229    def func_name(self) -> str:230        """The name of the shell function defined by the completion231        script.232        """233        safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII)234        return f"_{safe_name}_completion"235 236    def source_vars(self) -> t.Dict[str, t.Any]:237        """Vars for formatting :attr:`source_template`.238 239        By default this provides ``complete_func``, ``complete_var``,240        and ``prog_name``.241        """242        return {243            "complete_func": self.func_name,244            "complete_var": self.complete_var,245            "prog_name": self.prog_name,246        }247 248    def source(self) -> str:249        """Produce the shell script that defines the completion250        function. By default this ``%``-style formats251        :attr:`source_template` with the dict returned by252        :meth:`source_vars`.253        """254        return self.source_template % self.source_vars()255 256    def get_completion_args(self) -> t.Tuple[t.List[str], str]:257        """Use the env vars defined by the shell script to return a258        tuple of ``args, incomplete``. This must be implemented by259        subclasses.260        """261        raise NotImplementedError262 263    def get_completions(264        self, args: t.List[str], incomplete: str265    ) -> t.List[CompletionItem]:266        """Determine the context and last complete command or parameter267        from the complete args. Call that object's ``shell_complete``268        method to get the completions for the incomplete value.269 270        :param args: List of complete args before the incomplete value.271        :param incomplete: Value being completed. May be empty.272        """273        ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args)274        obj, incomplete = _resolve_incomplete(ctx, args, incomplete)275        return obj.shell_complete(ctx, incomplete)276 277    def format_completion(self, item: CompletionItem) -> str:278        """Format a completion item into the form recognized by the279        shell script. This must be implemented by subclasses.280 281        :param item: Completion item to format.282        """283        raise NotImplementedError284 285    def complete(self) -> str:286        """Produce the completion data to send back to the shell.287 288        By default this calls :meth:`get_completion_args`, gets the289        completions, then calls :meth:`format_completion` for each290        completion.291        """292        args, incomplete = self.get_completion_args()293        completions = self.get_completions(args, incomplete)294        out = [self.format_completion(item) for item in completions]295        return "\n".join(out)296 297 298class BashComplete(ShellComplete):299    """Shell completion for Bash."""300 301    name = "bash"302    source_template = _SOURCE_BASH303 304    @staticmethod305    def _check_version() -> None:306        import subprocess307 308        output = subprocess.run(309            ["bash", "-c", 'echo "${BASH_VERSION}"'], stdout=subprocess.PIPE310        )311        match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode())312 313        if match is not None:314            major, minor = match.groups()315 316            if major < "4" or major == "4" and minor < "4":317                echo(318                    _(319                        "Shell completion is not supported for Bash"320                        " versions older than 4.4."321                    ),322                    err=True,323                )324        else:325            echo(326                _("Couldn't detect Bash version, shell completion is not supported."),327                err=True,328            )329 330    def source(self) -> str:331        self._check_version()332        return super().source()333 334    def get_completion_args(self) -> t.Tuple[t.List[str], str]:335        cwords = split_arg_string(os.environ["COMP_WORDS"])336        cword = int(os.environ["COMP_CWORD"])337        args = cwords[1:cword]338 339        try:340            incomplete = cwords[cword]341        except IndexError:342            incomplete = ""343 344        return args, incomplete345 346    def format_completion(self, item: CompletionItem) -> str:347        return f"{item.type},{item.value}"348 349 350class ZshComplete(ShellComplete):351    """Shell completion for Zsh."""352 353    name = "zsh"354    source_template = _SOURCE_ZSH355 356    def get_completion_args(self) -> t.Tuple[t.List[str], str]:357        cwords = split_arg_string(os.environ["COMP_WORDS"])358        cword = int(os.environ["COMP_CWORD"])359        args = cwords[1:cword]360 361        try:362            incomplete = cwords[cword]363        except IndexError:364            incomplete = ""365 366        return args, incomplete367 368    def format_completion(self, item: CompletionItem) -> str:369        return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}"370 371 372class FishComplete(ShellComplete):373    """Shell completion for Fish."""374 375    name = "fish"376    source_template = _SOURCE_FISH377 378    def get_completion_args(self) -> t.Tuple[t.List[str], str]:379        cwords = split_arg_string(os.environ["COMP_WORDS"])380        incomplete = os.environ["COMP_CWORD"]381        args = cwords[1:]382 383        # Fish stores the partial word in both COMP_WORDS and384        # COMP_CWORD, remove it from complete args.385        if incomplete and args and args[-1] == incomplete:386            args.pop()387 388        return args, incomplete389 390    def format_completion(self, item: CompletionItem) -> str:391        if item.help:392            return f"{item.type},{item.value}\t{item.help}"393 394        return f"{item.type},{item.value}"395 396 397ShellCompleteType = t.TypeVar("ShellCompleteType", bound=t.Type[ShellComplete])398 399 400_available_shells: t.Dict[str, t.Type[ShellComplete]] = {401    "bash": BashComplete,402    "fish": FishComplete,403    "zsh": ZshComplete,404}405 406 407def add_completion_class(408    cls: ShellCompleteType, name: t.Optional[str] = None409) -> ShellCompleteType:410    """Register a :class:`ShellComplete` subclass under the given name.411    The name will be provided by the completion instruction environment412    variable during completion.413 414    :param cls: The completion class that will handle completion for the415        shell.416    :param name: Name to register the class under. Defaults to the417        class's ``name`` attribute.418    """419    if name is None:420        name = cls.name421 422    _available_shells[name] = cls423 424    return cls425 426 427def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]:428    """Look up a registered :class:`ShellComplete` subclass by the name429    provided by the completion instruction environment variable. If the430    name isn't registered, returns ``None``.431 432    :param shell: Name the class is registered under.433    """434    return _available_shells.get(shell)435 436 437def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:438    """Determine if the given parameter is an argument that can still439    accept values.440 441    :param ctx: Invocation context for the command represented by the442        parsed complete args.443    :param param: Argument object being checked.444    """445    if not isinstance(param, Argument):446        return False447 448    assert param.name is not None449    # Will be None if expose_value is False.450    value = ctx.params.get(param.name)451    return (452        param.nargs == -1453        or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE454        or (455            param.nargs > 1456            and isinstance(value, (tuple, list))457            and len(value) < param.nargs458        )459    )460 461 462def _start_of_option(ctx: Context, value: str) -> bool:463    """Check if the value looks like the start of an option."""464    if not value:465        return False466 467    c = value[0]468    return c in ctx._opt_prefixes469 470 471def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool:472    """Determine if the given parameter is an option that needs a value.473 474    :param args: List of complete args before the incomplete value.475    :param param: Option object being checked.476    """477    if not isinstance(param, Option):478        return False479 480    if param.is_flag or param.count:481        return False482 483    last_option = None484 485    for index, arg in enumerate(reversed(args)):486        if index + 1 > param.nargs:487            break488 489        if _start_of_option(ctx, arg):490            last_option = arg491 492    return last_option is not None and last_option in param.opts493 494 495def _resolve_context(496    cli: BaseCommand,497    ctx_args: t.MutableMapping[str, t.Any],498    prog_name: str,499    args: t.List[str],500) -> Context:501    """Produce the context hierarchy starting with the command and502    traversing the complete arguments. This only follows the commands,503    it doesn't trigger input prompts or callbacks.504 505    :param cli: Command being called.506    :param prog_name: Name of the executable in the shell.507    :param args: List of complete args before the incomplete value.508    """509    ctx_args["resilient_parsing"] = True510    ctx = cli.make_context(prog_name, args.copy(), **ctx_args)511    args = ctx.protected_args + ctx.args512 513    while args:514        command = ctx.command515 516        if isinstance(command, MultiCommand):517            if not command.chain:518                name, cmd, args = command.resolve_command(ctx, args)519 520                if cmd is None:521                    return ctx522 523                ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)524                args = ctx.protected_args + ctx.args525            else:526                sub_ctx = ctx527 528                while args:529                    name, cmd, args = command.resolve_command(ctx, args)530 531                    if cmd is None:532                        return ctx533 534                    sub_ctx = cmd.make_context(535                        name,536                        args,537                        parent=ctx,538                        allow_extra_args=True,539                        allow_interspersed_args=False,540                        resilient_parsing=True,541                    )542                    args = sub_ctx.args543 544                ctx = sub_ctx545                args = [*sub_ctx.protected_args, *sub_ctx.args]546        else:547            break548 549    return ctx550 551 552def _resolve_incomplete(553    ctx: Context, args: t.List[str], incomplete: str554) -> t.Tuple[t.Union[BaseCommand, Parameter], str]:555    """Find the Click object that will handle the completion of the556    incomplete value. Return the object and the incomplete value.557 558    :param ctx: Invocation context for the command represented by559        the parsed complete args.560    :param args: List of complete args before the incomplete value.561    :param incomplete: Value being completed. May be empty.562    """563    # Different shells treat an "=" between a long option name and564    # value differently. Might keep the value joined, return the "="565    # as a separate item, or return the split name and value. Always566    # split and discard the "=" to make completion easier.567    if incomplete == "=":568        incomplete = ""569    elif "=" in incomplete and _start_of_option(ctx, incomplete):570        name, _, incomplete = incomplete.partition("=")571        args.append(name)572 573    # The "--" marker tells Click to stop treating values as options574    # even if they start with the option character. If it hasn't been575    # given and the incomplete arg looks like an option, the current576    # command will provide option name completions.577    if "--" not in args and _start_of_option(ctx, incomplete):578        return ctx.command, incomplete579 580    params = ctx.command.get_params(ctx)581 582    # If the last complete arg is an option name with an incomplete583    # value, the option will provide value completions.584    for param in params:585        if _is_incomplete_option(ctx, args, param):586            return param, incomplete587 588    # It's not an option name or value. The first argument without a589    # parsed value will provide value completions.590    for param in params:591        if _is_incomplete_argument(ctx, param):592            return param, incomplete593 594    # There were no unparsed arguments, the command may be a group that595    # will provide command name completions.596    return ctx.command, incomplete597 
codekingpro/portable-devtools · Team Ai