Repository: pallets/click
Pull request #778 (2017-05-04 15:58:33)

Backport fixes for autocompletion on chained commands
Backports #774 to 6.x maintenance branch.



Patch:
diff --git a/CHANGES b/CHANGES
index 97d5b37..2bc5dde 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 d9d26d2..5210a7c 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


Test patch:
diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py
index fea096c..5b247bf 100644
--- a/tests/test_bashcomplete.py
+++ b/tests/test_bashcomplete.py
@@ -60,3 +60,28 @@ def test_long_chain():
     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']