Source code for xonsh.completers.click

"""Tab-completion for click-based aliases registered via
:func:`xonsh.aliases.Aliases.register_click_command`.

The entry point is :func:`complete_click`, bound per-alias through
``functools.partial(complete_click, click_cmd)`` and attached to the
alias wrapper as ``xonsh_complete``. The stock alias completer
(``xonsh.completers._aliases.complete_aliases``) then picks it up from
``alias.func.xonsh_complete`` during normal tab completion.

The ``click`` package is imported lazily — this module is only
exercised once a click alias has been registered, which itself requires
click to be installed.
"""

from xonsh.completers.tools import RichCompletion


[docs] def option_all_opts(param): """Every flag string declared on a ``click.Option`` — primary + secondary. ``secondary_opts`` holds the off-switch of flag pairs like ``--verbose/--quiet``. """ return list(param.opts) + list(param.secondary_opts)
[docs] def option_takes_value(param): """``True`` if the option consumes a separate argument as its value. Flags (``--verbose``) and count options (``-vvv``) do not. """ return not (getattr(param, "is_flag", False) or getattr(param, "count", False))
[docs] def resolve_subcommand(root_cmd, args): """Walk ``click.Group`` → sub-command chain consuming positional tokens. Returns ``(current_cmd, remaining_args)`` where ``remaining_args`` is the slice of ``args`` belonging to ``current_cmd`` (i.e. everything after the last recognised sub-command name). Options are skipped when they appear before the sub-command name — the few that take a separate value consume the next token as well. An unknown sub-command stops the walk, so the completer can still offer ``--`` options on the outermost unresolved command. """ import click current = root_cmd remaining = list(args) while isinstance(current, click.Group) and remaining: i = 0 # Skip leading options belonging to the group. while i < len(remaining) and remaining[i].startswith("-"): opt = remaining[i] i += 1 if "=" not in opt and i < len(remaining): for p in current.params: if isinstance(p, click.Option) and opt in option_all_opts(p): if option_takes_value(p): i += 1 break if i >= len(remaining): break sub_name = remaining[i] sub = current.get_command(click.Context(current), sub_name) if sub is None: # Unknown sub-command — stop here so caller can still complete # against the current group's options. break current = sub remaining = remaining[i + 1 :] return current, remaining
[docs] def previous_option_waiting_value(current_cmd, args_after_cmd): """If the last token is an option expecting a value, return that ``click.Option``; otherwise ``None``. Used to drive Choice completion after things like ``--color <TAB>``. """ import click if not args_after_cmd: return None prev = args_after_cmd[-1] if not prev.startswith("-") or "=" in prev: return None for param in current_cmd.params: if isinstance(param, click.Option) and prev in option_all_opts(param): if option_takes_value(param): return param return None return None
[docs] def positional_index(current_cmd, args_after_cmd): """Count how many positional arguments have been supplied to ``current_cmd`` so far, skipping options and their values. Used to pick which ``click.Argument`` slot we're completing into. """ import click idx = 0 j = 0 while j < len(args_after_cmd): arg = args_after_cmd[j] if arg.startswith("-"): if "=" in arg: j += 1 continue takes_value = False for param in current_cmd.params: if isinstance(param, click.Option) and arg in option_all_opts(param): takes_value = option_takes_value(param) break j += 2 if takes_value else 1 else: idx += 1 j += 1 return idx
[docs] def complete_click(click_cmd, command, alias=None, **_): """Tab-completer for click-based aliases. Yields completions for, in priority order: 1. Option values when the preceding token is an option with a ``click.Choice`` type (``--color <TAB>`` → ``red green blue``). 2. Option flags when the prefix starts with ``-`` (``--nam<TAB>`` → ``--name``). 3. Sub-command names when the current command is a ``click.Group``. 4. Positional arguments with a ``click.Choice`` type. Bound per-alias via :func:`functools.partial` in :func:`xonsh.aliases._click_command_alias`, so each wrapper captures its own ``click.Command`` instance. """ import click # Descend click.Group chain to find the command we're completing into. args_after_cmd = [a.value for a in command.args[1:]] current_cmd, args_after_cmd = resolve_subcommand(click_cmd, args_after_cmd) prefix = command.prefix # 1. Value for an option that takes one (only Choice is predictable). pending = previous_option_waiting_value(current_cmd, args_after_cmd) if pending is not None: if isinstance(pending.type, click.Choice): return { RichCompletion(choice, append_space=True) for choice in pending.type.choices if choice.startswith(prefix) } # Opaque type (str, int, path, ...) — no suggestions from us; let # other completers (path, etc.) try. return None # 2. Option flag. if prefix.startswith("-"): results = set() for param in current_cmd.params: if isinstance(param, click.Option): for opt in option_all_opts(param): if opt.startswith(prefix): results.add( RichCompletion( opt, description=(param.help or "").strip(), ) ) if getattr(current_cmd, "add_help_option", False) and "--help".startswith( prefix ): results.add( RichCompletion("--help", description="Show this message and exit.") ) return results # 3. Sub-command name for a click.Group. if isinstance(current_cmd, click.Group): ctx = click.Context(current_cmd) results = set() for sub_name in current_cmd.list_commands(ctx): if sub_name.startswith(prefix): sub = current_cmd.get_command(ctx, sub_name) desc = ( getattr(sub, "short_help", None) or getattr(sub, "help", None) or "" ).strip() results.add( RichCompletion(sub_name, description=desc, append_space=True) ) return results # 4. Positional arg with a Choice type. click_arguments = [p for p in current_cmd.params if isinstance(p, click.Argument)] pos_idx = positional_index(current_cmd, args_after_cmd) if pos_idx < len(click_arguments): arg_param = click_arguments[pos_idx] if isinstance(arg_param.type, click.Choice): return { RichCompletion(choice, append_space=True) for choice in arg_param.type.choices if choice.startswith(prefix) } return None