|
import os |
|
import re |
|
import typing as t |
|
from gettext import gettext as _ |
|
|
|
from .core import Argument |
|
from .core import BaseCommand |
|
from .core import Context |
|
from .core import MultiCommand |
|
from .core import Option |
|
from .core import Parameter |
|
from .core import ParameterSource |
|
from .parser import split_arg_string |
|
from .utils import echo |
|
|
|
|
|
def shell_complete( |
|
cli: BaseCommand, |
|
ctx_args: t.MutableMapping[str, t.Any], |
|
prog_name: str, |
|
complete_var: str, |
|
instruction: str, |
|
) -> int: |
|
"""Perform shell completion for the given CLI program. |
|
|
|
:param cli: Command being called. |
|
:param ctx_args: Extra arguments to pass to |
|
``cli.make_context``. |
|
:param prog_name: Name of the executable in the shell. |
|
:param complete_var: Name of the environment variable that holds |
|
the completion instruction. |
|
:param instruction: Value of ``complete_var`` with the completion |
|
instruction and shell, in the form ``instruction_shell``. |
|
:return: Status code to exit with. |
|
""" |
|
shell, _, instruction = instruction.partition("_") |
|
comp_cls = get_completion_class(shell) |
|
|
|
if comp_cls is None: |
|
return 1 |
|
|
|
comp = comp_cls(cli, ctx_args, prog_name, complete_var) |
|
|
|
if instruction == "source": |
|
echo(comp.source()) |
|
return 0 |
|
|
|
if instruction == "complete": |
|
echo(comp.complete()) |
|
return 0 |
|
|
|
return 1 |
|
|
|
|
|
class CompletionItem: |
|
"""Represents a completion value and metadata about the value. The |
|
default metadata is ``type`` to indicate special shell handling, |
|
and ``help`` if a shell supports showing a help string next to the |
|
value. |
|
|
|
Arbitrary parameters can be passed when creating the object, and |
|
accessed using ``item.attr``. If an attribute wasn't passed, |
|
accessing it returns ``None``. |
|
|
|
:param value: The completion suggestion. |
|
:param type: Tells the shell script to provide special completion |
|
support for the type. Click uses ``"dir"`` and ``"file"``. |
|
:param help: String shown next to the value if supported. |
|
:param kwargs: Arbitrary metadata. The built-in implementations |
|
don't use this, but custom type completions paired with custom |
|
shell support could use it. |
|
""" |
|
|
|
__slots__ = ("value", "type", "help", "_info") |
|
|
|
def __init__( |
|
self, |
|
value: t.Any, |
|
type: str = "plain", |
|
help: t.Optional[str] = None, |
|
**kwargs: t.Any, |
|
) -> None: |
|
self.value: t.Any = value |
|
self.type: str = type |
|
self.help: t.Optional[str] = help |
|
self._info = kwargs |
|
|
|
def __getattr__(self, name: str) -> t.Any: |
|
return self._info.get(name) |
|
|
|
|
|
|
|
_SOURCE_BASH = """\ |
|
%(complete_func)s() { |
|
local IFS=$'\\n' |
|
local response |
|
|
|
response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ |
|
%(complete_var)s=bash_complete $1) |
|
|
|
for completion in $response; do |
|
IFS=',' read type value <<< "$completion" |
|
|
|
if [[ $type == 'dir' ]]; then |
|
COMPREPLY=() |
|
compopt -o dirnames |
|
elif [[ $type == 'file' ]]; then |
|
COMPREPLY=() |
|
compopt -o default |
|
elif [[ $type == 'plain' ]]; then |
|
COMPREPLY+=($value) |
|
fi |
|
done |
|
|
|
return 0 |
|
} |
|
|
|
%(complete_func)s_setup() { |
|
complete -o nosort -F %(complete_func)s %(prog_name)s |
|
} |
|
|
|
%(complete_func)s_setup; |
|
""" |
|
|
|
_SOURCE_ZSH = """\ |
|
#compdef %(prog_name)s |
|
|
|
%(complete_func)s() { |
|
local -a completions |
|
local -a completions_with_descriptions |
|
local -a response |
|
(( ! $+commands[%(prog_name)s] )) && return 1 |
|
|
|
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ |
|
%(complete_var)s=zsh_complete %(prog_name)s)}") |
|
|
|
for type key descr in ${response}; do |
|
if [[ "$type" == "plain" ]]; then |
|
if [[ "$descr" == "_" ]]; then |
|
completions+=("$key") |
|
else |
|
completions_with_descriptions+=("$key":"$descr") |
|
fi |
|
elif [[ "$type" == "dir" ]]; then |
|
_path_files -/ |
|
elif [[ "$type" == "file" ]]; then |
|
_path_files -f |
|
fi |
|
done |
|
|
|
if [ -n "$completions_with_descriptions" ]; then |
|
_describe -V unsorted completions_with_descriptions -U |
|
fi |
|
|
|
if [ -n "$completions" ]; then |
|
compadd -U -V unsorted -a completions |
|
fi |
|
} |
|
|
|
if [[ $zsh_eval_context[-1] == loadautofunc ]]; then |
|
# autoload from fpath, call function directly |
|
%(complete_func)s "$@" |
|
else |
|
# eval/source/. command, register function for later |
|
compdef %(complete_func)s %(prog_name)s |
|
fi |
|
""" |
|
|
|
_SOURCE_FISH = """\ |
|
function %(complete_func)s; |
|
set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ |
|
COMP_CWORD=(commandline -t) %(prog_name)s); |
|
|
|
for completion in $response; |
|
set -l metadata (string split "," $completion); |
|
|
|
if test $metadata[1] = "dir"; |
|
__fish_complete_directories $metadata[2]; |
|
else if test $metadata[1] = "file"; |
|
__fish_complete_path $metadata[2]; |
|
else if test $metadata[1] = "plain"; |
|
echo $metadata[2]; |
|
end; |
|
end; |
|
end; |
|
|
|
complete --no-files --command %(prog_name)s --arguments \ |
|
"(%(complete_func)s)"; |
|
""" |
|
|
|
|
|
class ShellComplete: |
|
"""Base class for providing shell completion support. A subclass for |
|
a given shell will override attributes and methods to implement the |
|
completion instructions (``source`` and ``complete``). |
|
|
|
:param cli: Command being called. |
|
:param prog_name: Name of the executable in the shell. |
|
:param complete_var: Name of the environment variable that holds |
|
the completion instruction. |
|
|
|
.. versionadded:: 8.0 |
|
""" |
|
|
|
name: t.ClassVar[str] |
|
"""Name to register the shell as with :func:`add_completion_class`. |
|
This is used in completion instructions (``{name}_source`` and |
|
``{name}_complete``). |
|
""" |
|
|
|
source_template: t.ClassVar[str] |
|
"""Completion script template formatted by :meth:`source`. This must |
|
be provided by subclasses. |
|
""" |
|
|
|
def __init__( |
|
self, |
|
cli: BaseCommand, |
|
ctx_args: t.MutableMapping[str, t.Any], |
|
prog_name: str, |
|
complete_var: str, |
|
) -> None: |
|
self.cli = cli |
|
self.ctx_args = ctx_args |
|
self.prog_name = prog_name |
|
self.complete_var = complete_var |
|
|
|
@property |
|
def func_name(self) -> str: |
|
"""The name of the shell function defined by the completion |
|
script. |
|
""" |
|
safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII) |
|
return f"_{safe_name}_completion" |
|
|
|
def source_vars(self) -> t.Dict[str, t.Any]: |
|
"""Vars for formatting :attr:`source_template`. |
|
|
|
By default this provides ``complete_func``, ``complete_var``, |
|
and ``prog_name``. |
|
""" |
|
return { |
|
"complete_func": self.func_name, |
|
"complete_var": self.complete_var, |
|
"prog_name": self.prog_name, |
|
} |
|
|
|
def source(self) -> str: |
|
"""Produce the shell script that defines the completion |
|
function. By default this ``%``-style formats |
|
:attr:`source_template` with the dict returned by |
|
:meth:`source_vars`. |
|
""" |
|
return self.source_template % self.source_vars() |
|
|
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]: |
|
"""Use the env vars defined by the shell script to return a |
|
tuple of ``args, incomplete``. This must be implemented by |
|
subclasses. |
|
""" |
|
raise NotImplementedError |
|
|
|
def get_completions( |
|
self, args: t.List[str], incomplete: str |
|
) -> t.List[CompletionItem]: |
|
"""Determine the context and last complete command or parameter |
|
from the complete args. Call that object's ``shell_complete`` |
|
method to get the completions for the incomplete value. |
|
|
|
:param args: List of complete args before the incomplete value. |
|
:param incomplete: Value being completed. May be empty. |
|
""" |
|
ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) |
|
obj, incomplete = _resolve_incomplete(ctx, args, incomplete) |
|
return obj.shell_complete(ctx, incomplete) |
|
|
|
def format_completion(self, item: CompletionItem) -> str: |
|
"""Format a completion item into the form recognized by the |
|
shell script. This must be implemented by subclasses. |
|
|
|
:param item: Completion item to format. |
|
""" |
|
raise NotImplementedError |
|
|
|
def complete(self) -> str: |
|
"""Produce the completion data to send back to the shell. |
|
|
|
By default this calls :meth:`get_completion_args`, gets the |
|
completions, then calls :meth:`format_completion` for each |
|
completion. |
|
""" |
|
args, incomplete = self.get_completion_args() |
|
completions = self.get_completions(args, incomplete) |
|
out = [self.format_completion(item) for item in completions] |
|
return "\n".join(out) |
|
|
|
|
|
class BashComplete(ShellComplete): |
|
"""Shell completion for Bash.""" |
|
|
|
name = "bash" |
|
source_template = _SOURCE_BASH |
|
|
|
@staticmethod |
|
def _check_version() -> None: |
|
import subprocess |
|
|
|
output = subprocess.run( |
|
["bash", "-c", 'echo "${BASH_VERSION}"'], stdout=subprocess.PIPE |
|
) |
|
match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) |
|
|
|
if match is not None: |
|
major, minor = match.groups() |
|
|
|
if major < "4" or major == "4" and minor < "4": |
|
echo( |
|
_( |
|
"Shell completion is not supported for Bash" |
|
" versions older than 4.4." |
|
), |
|
err=True, |
|
) |
|
else: |
|
echo( |
|
_("Couldn't detect Bash version, shell completion is not supported."), |
|
err=True, |
|
) |
|
|
|
def source(self) -> str: |
|
self._check_version() |
|
return super().source() |
|
|
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]: |
|
cwords = split_arg_string(os.environ["COMP_WORDS"]) |
|
cword = int(os.environ["COMP_CWORD"]) |
|
args = cwords[1:cword] |
|
|
|
try: |
|
incomplete = cwords[cword] |
|
except IndexError: |
|
incomplete = "" |
|
|
|
return args, incomplete |
|
|
|
def format_completion(self, item: CompletionItem) -> str: |
|
return f"{item.type},{item.value}" |
|
|
|
|
|
class ZshComplete(ShellComplete): |
|
"""Shell completion for Zsh.""" |
|
|
|
name = "zsh" |
|
source_template = _SOURCE_ZSH |
|
|
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]: |
|
cwords = split_arg_string(os.environ["COMP_WORDS"]) |
|
cword = int(os.environ["COMP_CWORD"]) |
|
args = cwords[1:cword] |
|
|
|
try: |
|
incomplete = cwords[cword] |
|
except IndexError: |
|
incomplete = "" |
|
|
|
return args, incomplete |
|
|
|
def format_completion(self, item: CompletionItem) -> str: |
|
return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" |
|
|
|
|
|
class FishComplete(ShellComplete): |
|
"""Shell completion for Fish.""" |
|
|
|
name = "fish" |
|
source_template = _SOURCE_FISH |
|
|
|
def get_completion_args(self) -> t.Tuple[t.List[str], str]: |
|
cwords = split_arg_string(os.environ["COMP_WORDS"]) |
|
incomplete = os.environ["COMP_CWORD"] |
|
args = cwords[1:] |
|
|
|
|
|
|
|
if incomplete and args and args[-1] == incomplete: |
|
args.pop() |
|
|
|
return args, incomplete |
|
|
|
def format_completion(self, item: CompletionItem) -> str: |
|
if item.help: |
|
return f"{item.type},{item.value}\t{item.help}" |
|
|
|
return f"{item.type},{item.value}" |
|
|
|
|
|
ShellCompleteType = t.TypeVar("ShellCompleteType", bound=t.Type[ShellComplete]) |
|
|
|
|
|
_available_shells: t.Dict[str, t.Type[ShellComplete]] = { |
|
"bash": BashComplete, |
|
"fish": FishComplete, |
|
"zsh": ZshComplete, |
|
} |
|
|
|
|
|
def add_completion_class( |
|
cls: ShellCompleteType, name: t.Optional[str] = None |
|
) -> ShellCompleteType: |
|
"""Register a :class:`ShellComplete` subclass under the given name. |
|
The name will be provided by the completion instruction environment |
|
variable during completion. |
|
|
|
:param cls: The completion class that will handle completion for the |
|
shell. |
|
:param name: Name to register the class under. Defaults to the |
|
class's ``name`` attribute. |
|
""" |
|
if name is None: |
|
name = cls.name |
|
|
|
_available_shells[name] = cls |
|
|
|
return cls |
|
|
|
|
|
def get_completion_class(shell: str) -> t.Optional[t.Type[ShellComplete]]: |
|
"""Look up a registered :class:`ShellComplete` subclass by the name |
|
provided by the completion instruction environment variable. If the |
|
name isn't registered, returns ``None``. |
|
|
|
:param shell: Name the class is registered under. |
|
""" |
|
return _available_shells.get(shell) |
|
|
|
|
|
def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: |
|
"""Determine if the given parameter is an argument that can still |
|
accept values. |
|
|
|
:param ctx: Invocation context for the command represented by the |
|
parsed complete args. |
|
:param param: Argument object being checked. |
|
""" |
|
if not isinstance(param, Argument): |
|
return False |
|
|
|
assert param.name is not None |
|
|
|
value = ctx.params.get(param.name) |
|
return ( |
|
param.nargs == -1 |
|
or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE |
|
or ( |
|
param.nargs > 1 |
|
and isinstance(value, (tuple, list)) |
|
and len(value) < param.nargs |
|
) |
|
) |
|
|
|
|
|
def _start_of_option(ctx: Context, value: str) -> bool: |
|
"""Check if the value looks like the start of an option.""" |
|
if not value: |
|
return False |
|
|
|
c = value[0] |
|
return c in ctx._opt_prefixes |
|
|
|
|
|
def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool: |
|
"""Determine if the given parameter is an option that needs a value. |
|
|
|
:param args: List of complete args before the incomplete value. |
|
:param param: Option object being checked. |
|
""" |
|
if not isinstance(param, Option): |
|
return False |
|
|
|
if param.is_flag or param.count: |
|
return False |
|
|
|
last_option = None |
|
|
|
for index, arg in enumerate(reversed(args)): |
|
if index + 1 > param.nargs: |
|
break |
|
|
|
if _start_of_option(ctx, arg): |
|
last_option = arg |
|
|
|
return last_option is not None and last_option in param.opts |
|
|
|
|
|
def _resolve_context( |
|
cli: BaseCommand, |
|
ctx_args: t.MutableMapping[str, t.Any], |
|
prog_name: str, |
|
args: t.List[str], |
|
) -> Context: |
|
"""Produce the context hierarchy starting with the command and |
|
traversing the complete arguments. This only follows the commands, |
|
it doesn't trigger input prompts or callbacks. |
|
|
|
:param cli: Command being called. |
|
:param prog_name: Name of the executable in the shell. |
|
:param args: List of complete args before the incomplete value. |
|
""" |
|
ctx_args["resilient_parsing"] = True |
|
ctx = cli.make_context(prog_name, args.copy(), **ctx_args) |
|
args = ctx.protected_args + ctx.args |
|
|
|
while args: |
|
command = ctx.command |
|
|
|
if isinstance(command, MultiCommand): |
|
if not command.chain: |
|
name, cmd, args = command.resolve_command(ctx, args) |
|
|
|
if cmd is None: |
|
return ctx |
|
|
|
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True) |
|
args = ctx.protected_args + ctx.args |
|
else: |
|
sub_ctx = ctx |
|
|
|
while args: |
|
name, cmd, args = command.resolve_command(ctx, args) |
|
|
|
if cmd is None: |
|
return ctx |
|
|
|
sub_ctx = cmd.make_context( |
|
name, |
|
args, |
|
parent=ctx, |
|
allow_extra_args=True, |
|
allow_interspersed_args=False, |
|
resilient_parsing=True, |
|
) |
|
args = sub_ctx.args |
|
|
|
ctx = sub_ctx |
|
args = [*sub_ctx.protected_args, *sub_ctx.args] |
|
else: |
|
break |
|
|
|
return ctx |
|
|
|
|
|
def _resolve_incomplete( |
|
ctx: Context, args: t.List[str], incomplete: str |
|
) -> t.Tuple[t.Union[BaseCommand, Parameter], str]: |
|
"""Find the Click object that will handle the completion of the |
|
incomplete value. Return the object and the incomplete value. |
|
|
|
:param ctx: Invocation context for the command represented by |
|
the parsed complete args. |
|
:param args: List of complete args before the incomplete value. |
|
:param incomplete: Value being completed. May be empty. |
|
""" |
|
|
|
|
|
|
|
|
|
if incomplete == "=": |
|
incomplete = "" |
|
elif "=" in incomplete and _start_of_option(ctx, incomplete): |
|
name, _, incomplete = incomplete.partition("=") |
|
args.append(name) |
|
|
|
|
|
|
|
|
|
|
|
if "--" not in args and _start_of_option(ctx, incomplete): |
|
return ctx.command, incomplete |
|
|
|
params = ctx.command.get_params(ctx) |
|
|
|
|
|
|
|
for param in params: |
|
if _is_incomplete_option(ctx, args, param): |
|
return param, incomplete |
|
|
|
|
|
|
|
for param in params: |
|
if _is_incomplete_argument(ctx, param): |
|
return param, incomplete |
|
|
|
|
|
|
|
return ctx.command, incomplete |
|
|