Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix overzealous completion when required options/arguments are being completed #806

Merged
merged 9 commits into from
Oct 10, 2017
5 changes: 4 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ Version 7.0
- ``launch`` now works properly under Cygwin. See #650.
- `CliRunner.invoke` now may receive `args` as a string representing
a Unix shell command. See #664.
- Fix bug that caused bashcompletion to give inproper completions on
- Fix bug that caused bashcompletion to give improper completions on
chained commands. See #774.
- 't' and 'f' are now converted to True and False.
- Fix bug that caused bashcompletion to give improper completions on
chained commands when a required option/argument was being completed.
See #790.

Version 6.8
-----------
Expand Down
55 changes: 32 additions & 23 deletions click/_bashcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def resolve_ctx(cli, prog_name, args):

return ctx


def start_of_option(param_str):
"""
:param param_str: param_str to check
Expand All @@ -72,6 +73,8 @@ def is_incomplete_option(all_args, cmd_param):
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
Expand All @@ -91,6 +94,8 @@ def is_incomplete_argument(current_params, cmd_param):
: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
Expand Down Expand Up @@ -118,6 +123,7 @@ def get_user_autocompletions(ctx, args, incomplete, cmd_param):
else:
return []


def get_choices(cli, prog_name, args, incomplete):
"""
:param cli: command definition
Expand All @@ -144,35 +150,38 @@ def get_choices(cli, prog_name, args, incomplete):
choices = []
found_param = False
if start_of_option(incomplete):
# completions for options
# completions for partial options
for param in ctx.command.params:
if isinstance(param, Option):
choices.extend([param_opt for param_opt in param.opts + param.secondary_opts
if param_opt not in all_args or param.multiple])
found_param = True
if not found_param:
# completion for option values by choices
for cmd_param in ctx.command.params:
if isinstance(cmd_param, Option) and is_incomplete_option(all_args, cmd_param):
choices.extend(get_user_autocompletions(ctx, all_args, incomplete, cmd_param))
found_param = True
break
if not found_param:
# completion for argument values by choices
for cmd_param in ctx.command.params:
if isinstance(cmd_param, Argument) and is_incomplete_argument(ctx.params, cmd_param):
choices.extend(get_user_autocompletions(ctx, all_args, incomplete, cmd_param))
else:
# completion for option values from user supplied values
for param in ctx.command.params:
if is_incomplete_option(all_args, param):
choices.extend(get_user_autocompletions(ctx, all_args, incomplete, param))
found_param = True
break

if not found_param and isinstance(ctx.command, MultiCommand):
# completion for any subcommands
choices.extend(ctx.command.list_commands(ctx))

if not start_of_option(incomplete) and ctx.parent is not None and isinstance(ctx.parent.command, MultiCommand) and ctx.parent.command.chain:
# completion for chained commands
remaining_comands = set(ctx.parent.command.list_commands(ctx.parent))-set(ctx.parent.protected_args)
choices.extend(remaining_comands)
# completion for argument values from user supplied values
if not found_param:
for param in ctx.command.params:
if is_incomplete_argument(ctx.params, param):
choices.extend(get_user_autocompletions(ctx, all_args, incomplete, param))
# Stop looking for other completions only if this argument is required
found_param = param.required
break

if not found_param and isinstance(ctx.command, MultiCommand):
# completion for any subcommands
choices.extend(ctx.command.list_commands(ctx))

if not found_param:
# Walk up the context list and add any other completion possibilities from chained commands
while ctx.parent is not None:
ctx = ctx.parent
if isinstance(ctx.command, MultiCommand) and ctx.command.chain:
remaining_commands = set(ctx.command.list_commands(ctx))-set(ctx.protected_args)
choices.extend(remaining_commands)

for item in choices:
if item.startswith(incomplete):
Expand Down
9 changes: 5 additions & 4 deletions tests/test_bashcomplete.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,16 @@ def asub(asub_opt):

@cli.command('bsub')
@click.option('--bsub-opt')
@click.argument('arg', type=click.Choice(['arg1', 'arg2']))
@click.argument('arg', type=click.Choice(['arg1', 'arg2']), required=True)
def bsub(bsub_opt, arg):
pass

assert list(get_choices(cli, 'lol', [], '-')) == ['--cli-opt']
assert list(get_choices(cli, 'lol', [], '')) == ['asub', 'bsub']
assert list(get_choices(cli, 'lol', ['asub'], '-')) == ['--asub-opt']
assert list(get_choices(cli, 'lol', ['asub'], '')) == ['bsub']
assert list(get_choices(cli, 'lol', ['bsub'], '')) == ['arg1', 'arg2', 'asub']

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

This comment was marked as off-topic.

assert list(get_choices(cli, 'lol', ['bsub'], '')) == ['arg1', 'arg2']
assert list(get_choices(cli, 'lol', ['asub', '--asub-opt'], '')) == []
assert list(get_choices(cli, 'lol', ['asub', '--asub-opt', '5', 'bsub'], '-')) == ['--bsub-opt']
assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-opt']

Expand Down Expand Up @@ -255,10 +256,10 @@ def sub(sub_opt):
def bsub(bsub_opt):
pass

assert list(get_choices(cli, 'lol', ['sub'], '')) == ['subarg1', 'subarg2']
assert list(get_choices(cli, 'lol', ['sub'], '')) == ['subarg1', 'subarg2', 'bsub']
assert list(get_choices(cli, 'lol', ['sub', '--sub-opt'], '')) == ['subopt1', 'subopt2']
assert list(get_choices(cli, 'lol', ['sub', '--sub-opt', 'subopt1'], '')) == \
['subarg1', 'subarg2']
['subarg1', 'subarg2', 'bsub']
assert list(get_choices(cli, 'lol',
['sub', '--sub-opt', 'subopt1', 'subarg1', 'bsub'], '-')) == ['--bsub-opt']
assert list(get_choices(cli, 'lol',
Expand Down