diff --git a/CHANGES b/CHANGES index f06bb3124..45990272a 100644 --- a/CHANGES +++ b/CHANGES @@ -8,7 +8,7 @@ Version 7.0 (upcoming release with new features, release date to be decided) - Added support for dynamic bash completion from a user-supplied callback. - See #755 + See #755. - Added support for bash completion of type=click.Choice for Options and Arguments. See #535. - The user is now presented with the available choices if prompt=True and @@ -22,6 +22,8 @@ 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 + chained commands. See #774. Version 6.8 ----------- diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py index 10a9dc203..a49fe7d95 100644 --- a/click/_bashcomplete.py +++ b/click/_bashcomplete.py @@ -42,14 +42,18 @@ def resolve_ctx(cli, prog_name, args): :return: the final context/command parsed """ ctx = cli.make_context(prog_name, args, resilient_parsing=True) - while ctx.protected_args + ctx.args and isinstance(ctx.command, MultiCommand): - a = ctx.protected_args + ctx.args - cmd = ctx.command.get_command(ctx, a[0]) + args_remaining = ctx.protected_args + ctx.args + while ctx is not None and args_remaining: + if isinstance(ctx.command, MultiCommand): + cmd = ctx.command.get_command(ctx, args_remaining[0]) if cmd is None: return None - ctx = cmd.make_context(a[0], a[1:], parent=ctx, resilient_parsing=True) - return ctx + ctx = cmd.make_context(args_remaining[0], args_remaining[1:], parent=ctx, resilient_parsing=True) + args_remaining = ctx.protected_args + ctx.args + else: + ctx = ctx.parent + return ctx def start_of_option(param_str): """ @@ -164,6 +168,11 @@ def get_choices(cli, prog_name, args, incomplete): # 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) + for item in choices: if item.startswith(incomplete): yield item diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py index f8e91b479..268e046e8 100644 --- a/tests/test_bashcomplete.py +++ b/tests/test_bashcomplete.py @@ -112,6 +112,32 @@ def csub(csub_opt, color): assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], 'b')) == ['blue'] +def test_chaining(): + @click.group('cli', chain=True) + @click.option('--cli-opt') + def cli(cli_opt): + pass + + @cli.command('asub') + @click.option('--asub-opt') + def asub(asub_opt): + pass + + @cli.command('bsub') + @click.option('--bsub-opt') + @click.argument('arg', type=click.Choice(['arg1', 'arg2'])) + 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'] + assert list(get_choices(cli, 'lol', ['asub', '--asub-opt', '5', 'bsub'], '-')) == ['--bsub-opt'] + assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-opt'] + + def test_argument_choice(): @click.command() @click.argument('arg1', required=False, type=click.Choice(['arg11', 'arg12']))