codekingpro/portable-devtools
115k
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 