diff --git a/CHANGES b/CHANGES index 97d5b3789..2bc5dde6e 100644 --- a/CHANGES +++ b/CHANGES @@ -12,6 +12,7 @@ Version 6.8 #728. - Fix bug in test runner when calling ``sys.exit`` with ``None``. See #739. - Fix crash on Windows console, see #744. +- Fix bashcompletion on chained commands. See #754. Version 6.7 ----------- diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py index d9d26d28b..5210a7ca5 100644 --- a/click/_bashcomplete.py +++ b/click/_bashcomplete.py @@ -30,14 +30,18 @@ def get_completion_script(prog_name, complete_var): def resolve_ctx(cli, prog_name, args): 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 get_choices(cli, prog_name, args, incomplete): ctx = resolve_ctx(cli, prog_name, args) @@ -45,7 +49,8 @@ def get_choices(cli, prog_name, args, incomplete): return choices = [] - if incomplete and not incomplete[:1].isalnum(): + incomplete_is_start_of_option = incomplete and not incomplete[:1].isalnum() + if incomplete_is_start_of_option: for param in ctx.command.params: if not isinstance(param, Option): continue @@ -54,6 +59,11 @@ def get_choices(cli, prog_name, args, incomplete): elif isinstance(ctx.command, MultiCommand): choices.extend(ctx.command.list_commands(ctx)) + if not incomplete_is_start_of_option 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 fea096c10..5b247bfd4 100644 --- a/tests/test_bashcomplete.py +++ b/tests/test_bashcomplete.py @@ -60,3 +60,28 @@ def csub(csub_opt): assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '')) == ['csub'] assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '-')) == ['--csub-opt'] assert list(get_choices(cli, 'lol', ['asub', 'bsub', 'csub'], '')) == [] + + +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') + 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'], '')) == ['asub'] + assert list(get_choices(cli, 'lol', ['asub', '--asub-opt', '5', 'bsub'], '-')) == ['--bsub-opt'] + assert list(get_choices(cli, 'lol', ['asub', 'bsub'], '-')) == ['--bsub-opt']