|
import copy |
|
import os |
|
import re |
|
|
|
from .core import Argument |
|
from .core import MultiCommand |
|
from .core import Option |
|
from .parser import split_arg_string |
|
from .types import Choice |
|
from .utils import echo |
|
|
|
try: |
|
from collections import abc |
|
except ImportError: |
|
import collections as abc |
|
|
|
WORDBREAK = "=" |
|
|
|
|
|
COMPLETION_SCRIPT_BASH = """ |
|
%(complete_func)s() { |
|
local IFS=$'\n' |
|
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\ |
|
COMP_CWORD=$COMP_CWORD \\ |
|
%(autocomplete_var)s=complete $1 ) ) |
|
return 0 |
|
} |
|
|
|
%(complete_func)setup() { |
|
local COMPLETION_OPTIONS="" |
|
local BASH_VERSION_ARR=(${BASH_VERSION//./ }) |
|
# Only BASH version 4.4 and later have the nosort option. |
|
if [ ${BASH_VERSION_ARR[0]} -gt 4 ] || ([ ${BASH_VERSION_ARR[0]} -eq 4 ] \ |
|
&& [ ${BASH_VERSION_ARR[1]} -ge 4 ]); then |
|
COMPLETION_OPTIONS="-o nosort" |
|
fi |
|
|
|
complete $COMPLETION_OPTIONS -F %(complete_func)s %(script_names)s |
|
} |
|
|
|
%(complete_func)setup |
|
""" |
|
|
|
COMPLETION_SCRIPT_ZSH = """ |
|
#compdef %(script_names)s |
|
|
|
%(complete_func)s() { |
|
local -a completions |
|
local -a completions_with_descriptions |
|
local -a response |
|
(( ! $+commands[%(script_names)s] )) && return 1 |
|
|
|
response=("${(@f)$( env COMP_WORDS=\"${words[*]}\" \\ |
|
COMP_CWORD=$((CURRENT-1)) \\ |
|
%(autocomplete_var)s=\"complete_zsh\" \\ |
|
%(script_names)s )}") |
|
|
|
for key descr in ${(kv)response}; do |
|
if [[ "$descr" == "_" ]]; then |
|
completions+=("$key") |
|
else |
|
completions_with_descriptions+=("$key":"$descr") |
|
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 |
|
compstate[insert]="automenu" |
|
} |
|
|
|
compdef %(complete_func)s %(script_names)s |
|
""" |
|
|
|
COMPLETION_SCRIPT_FISH = ( |
|
"complete --no-files --command %(script_names)s --arguments" |
|
' "(env %(autocomplete_var)s=complete_fish' |
|
" COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t)" |
|
' %(script_names)s)"' |
|
) |
|
|
|
_completion_scripts = { |
|
"bash": COMPLETION_SCRIPT_BASH, |
|
"zsh": COMPLETION_SCRIPT_ZSH, |
|
"fish": COMPLETION_SCRIPT_FISH, |
|
} |
|
|
|
_invalid_ident_char_re = re.compile(r"[^a-zA-Z0-9_]") |
|
|
|
|
|
def get_completion_script(prog_name, complete_var, shell): |
|
cf_name = _invalid_ident_char_re.sub("", prog_name.replace("-", "_")) |
|
script = _completion_scripts.get(shell, COMPLETION_SCRIPT_BASH) |
|
return ( |
|
script |
|
% { |
|
"complete_func": "_{}_completion".format(cf_name), |
|
"script_names": prog_name, |
|
"autocomplete_var": complete_var, |
|
} |
|
).strip() + ";" |
|
|
|
|
|
def resolve_ctx(cli, prog_name, args): |
|
"""Parse into a hierarchy of contexts. Contexts are connected |
|
through the parent variable. |
|
|
|
:param cli: command definition |
|
:param prog_name: the program that is running |
|
:param args: full list of args |
|
:return: the final context/command parsed |
|
""" |
|
ctx = cli.make_context(prog_name, args, resilient_parsing=True) |
|
args = ctx.protected_args + ctx.args |
|
while args: |
|
if isinstance(ctx.command, MultiCommand): |
|
if not ctx.command.chain: |
|
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) |
|
if cmd is None: |
|
return ctx |
|
ctx = cmd.make_context( |
|
cmd_name, args, parent=ctx, resilient_parsing=True |
|
) |
|
args = ctx.protected_args + ctx.args |
|
else: |
|
|
|
while args: |
|
cmd_name, cmd, args = ctx.command.resolve_command(ctx, args) |
|
if cmd is None: |
|
return ctx |
|
sub_ctx = cmd.make_context( |
|
cmd_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 start_of_option(param_str): |
|
""" |
|
:param param_str: param_str to check |
|
:return: whether or not this is the start of an option declaration |
|
(i.e. starts "-" or "--") |
|
""" |
|
return param_str and param_str[:1] == "-" |
|
|
|
|
|
def is_incomplete_option(all_args, cmd_param): |
|
""" |
|
:param all_args: the full original list of args supplied |
|
:param cmd_param: the current command paramter |
|
:return: whether or not the last option declaration (i.e. starts |
|
"-" or "--") is incomplete and corresponds to this cmd_param. In |
|
other words whether this cmd_param option can still accept |
|
values |
|
""" |
|
if not isinstance(cmd_param, Option): |
|
return False |
|
if cmd_param.is_flag: |
|
return False |
|
last_option = None |
|
for index, arg_str in enumerate( |
|
reversed([arg for arg in all_args if arg != WORDBREAK]) |
|
): |
|
if index + 1 > cmd_param.nargs: |
|
break |
|
if start_of_option(arg_str): |
|
last_option = arg_str |
|
|
|
return True if last_option and last_option in cmd_param.opts else False |
|
|
|
|
|
def is_incomplete_argument(current_params, cmd_param): |
|
""" |
|
:param current_params: the current params and values for this |
|
argument as already entered |
|
:param cmd_param: the current command parameter |
|
:return: whether or not the last argument is incomplete and |
|
corresponds to this cmd_param. In other words whether or not the |
|
this cmd_param argument can still accept values |
|
""" |
|
if not isinstance(cmd_param, Argument): |
|
return False |
|
current_param_values = current_params[cmd_param.name] |
|
if current_param_values is None: |
|
return True |
|
if cmd_param.nargs == -1: |
|
return True |
|
if ( |
|
isinstance(current_param_values, abc.Iterable) |
|
and cmd_param.nargs > 1 |
|
and len(current_param_values) < cmd_param.nargs |
|
): |
|
return True |
|
return False |
|
|
|
|
|
def get_user_autocompletions(ctx, args, incomplete, cmd_param): |
|
""" |
|
:param ctx: context associated with the parsed command |
|
:param args: full list of args |
|
:param incomplete: the incomplete text to autocomplete |
|
:param cmd_param: command definition |
|
:return: all the possible user-specified completions for the param |
|
""" |
|
results = [] |
|
if isinstance(cmd_param.type, Choice): |
|
|
|
results = [ |
|
(c, None) for c in cmd_param.type.choices if str(c).startswith(incomplete) |
|
] |
|
elif cmd_param.autocompletion is not None: |
|
dynamic_completions = cmd_param.autocompletion( |
|
ctx=ctx, args=args, incomplete=incomplete |
|
) |
|
results = [ |
|
c if isinstance(c, tuple) else (c, None) for c in dynamic_completions |
|
] |
|
return results |
|
|
|
|
|
def get_visible_commands_starting_with(ctx, starts_with): |
|
""" |
|
:param ctx: context associated with the parsed command |
|
:starts_with: string that visible commands must start with. |
|
:return: all visible (not hidden) commands that start with starts_with. |
|
""" |
|
for c in ctx.command.list_commands(ctx): |
|
if c.startswith(starts_with): |
|
command = ctx.command.get_command(ctx, c) |
|
if not command.hidden: |
|
yield command |
|
|
|
|
|
def add_subcommand_completions(ctx, incomplete, completions_out): |
|
|
|
if isinstance(ctx.command, MultiCommand): |
|
completions_out.extend( |
|
[ |
|
(c.name, c.get_short_help_str()) |
|
for c in get_visible_commands_starting_with(ctx, incomplete) |
|
] |
|
) |
|
|
|
|
|
|
|
while ctx.parent is not None: |
|
ctx = ctx.parent |
|
if isinstance(ctx.command, MultiCommand) and ctx.command.chain: |
|
remaining_commands = [ |
|
c |
|
for c in get_visible_commands_starting_with(ctx, incomplete) |
|
if c.name not in ctx.protected_args |
|
] |
|
completions_out.extend( |
|
[(c.name, c.get_short_help_str()) for c in remaining_commands] |
|
) |
|
|
|
|
|
def get_choices(cli, prog_name, args, incomplete): |
|
""" |
|
:param cli: command definition |
|
:param prog_name: the program that is running |
|
:param args: full list of args |
|
:param incomplete: the incomplete text to autocomplete |
|
:return: all the possible completions for the incomplete |
|
""" |
|
all_args = copy.deepcopy(args) |
|
|
|
ctx = resolve_ctx(cli, prog_name, args) |
|
if ctx is None: |
|
return [] |
|
|
|
has_double_dash = "--" in all_args |
|
|
|
|
|
|
|
if start_of_option(incomplete) and WORDBREAK in incomplete: |
|
partition_incomplete = incomplete.partition(WORDBREAK) |
|
all_args.append(partition_incomplete[0]) |
|
incomplete = partition_incomplete[2] |
|
elif incomplete == WORDBREAK: |
|
incomplete = "" |
|
|
|
completions = [] |
|
if not has_double_dash and start_of_option(incomplete): |
|
|
|
for param in ctx.command.params: |
|
if isinstance(param, Option) and not param.hidden: |
|
param_opts = [ |
|
param_opt |
|
for param_opt in param.opts + param.secondary_opts |
|
if param_opt not in all_args or param.multiple |
|
] |
|
completions.extend( |
|
[(o, param.help) for o in param_opts if o.startswith(incomplete)] |
|
) |
|
return completions |
|
|
|
for param in ctx.command.params: |
|
if is_incomplete_option(all_args, param): |
|
return get_user_autocompletions(ctx, all_args, incomplete, param) |
|
|
|
for param in ctx.command.params: |
|
if is_incomplete_argument(ctx.params, param): |
|
return get_user_autocompletions(ctx, all_args, incomplete, param) |
|
|
|
add_subcommand_completions(ctx, incomplete, completions) |
|
|
|
return sorted(completions) |
|
|
|
|
|
def do_complete(cli, prog_name, include_descriptions): |
|
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 = "" |
|
|
|
for item in get_choices(cli, prog_name, args, incomplete): |
|
echo(item[0]) |
|
if include_descriptions: |
|
|
|
|
|
|
|
echo(item[1] if item[1] else "_") |
|
|
|
return True |
|
|
|
|
|
def do_complete_fish(cli, prog_name): |
|
cwords = split_arg_string(os.environ["COMP_WORDS"]) |
|
incomplete = os.environ["COMP_CWORD"] |
|
args = cwords[1:] |
|
|
|
for item in get_choices(cli, prog_name, args, incomplete): |
|
if item[1]: |
|
echo("{arg}\t{desc}".format(arg=item[0], desc=item[1])) |
|
else: |
|
echo(item[0]) |
|
|
|
return True |
|
|
|
|
|
def bashcomplete(cli, prog_name, complete_var, complete_instr): |
|
if "_" in complete_instr: |
|
command, shell = complete_instr.split("_", 1) |
|
else: |
|
command = complete_instr |
|
shell = "bash" |
|
|
|
if command == "source": |
|
echo(get_completion_script(prog_name, complete_var, shell)) |
|
return True |
|
elif command == "complete": |
|
if shell == "fish": |
|
return do_complete_fish(cli, prog_name) |
|
elif shell in {"bash", "zsh"}: |
|
return do_complete(cli, prog_name, shell == "zsh") |
|
|
|
return False |
|
|