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