Repository: pallets/click

Issue:
Bash auto-complete of options not working properly in command chains
Example:

    $ cli cmd1 cmd2 --<TAB><TAB>

Auto-completes options of `cmd1`, not `cmd2`.

Observed in Python version 3.5.2 and click version 6.6.


Discussion:
@Gig77 try with latest develop. I believe that this has been addressed.
Not working for me in 7.0 development version. `cmd1` has no arguments, only options, and `cmd1` and `cmd2` are both valid subcommands of `cli`.
@Gig77 please post a snippet of your test case and I will take a look.
This works for me on 5f2ff4faf627dc9d0925cffb7d9b8f3162fbdaad (head of master):


    import click

    @click.group()
    def cli():
        pass

    @click.group()
    @click.option('-n', is_flag=True)
    def cmd1(n):
        pass

    @cmd1.command()
    @click.option('-b', is_flag=True)
    def subcmd(b):
        pass

    cli.add_command(cmd1)

Thanks for looking into it! In chain mode it did not work for me:

```
@click.group(chain=True)
def cli():
	pass

@cli.command()
@click.option('-n', is_flag=True)
def cmd1(n):
	pass

@cli.command()
@click.option('-b', is_flag=True)
def cmd2(b):
	pass
```
I see the issue and will take a more in depth look this weekend.

On Tue, Apr 25, 2017 at 5:57 PM Christian Frech <notifications@github.com>
wrote:

> Thanks for looking into it! In chain mode it did not work for me:
>
> @click.group(chain=True)
> def cli():
> 	pass
>
> @cli.command()
> @click.option('-n', is_flag=True)
> def cmd1(n):
> 	pass
>
> @cli.command()
> @click.option('-b', is_flag=True)
> def subcmd(b):
> 	pass
>
> cli.add_command(cmd1)
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/pallets/click/issues/754#issuecomment-297207215>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AMEUpYKOI8Asw2-WMr627kEyun0JIl9aks5rzpZ6gaJpZM4MpbtA>
> .
>


Resolving patch:
diff --git a/click/_bashcomplete.py b/click/_bashcomplete.py
--- 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


Test patch:
diff --git a/tests/test_bashcomplete.py b/tests/test_bashcomplete.py
--- 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']))