Repository: pallets/click (python)

Issue:
Completion for commands in nested groups is broken in Click 8.2.0
## Summary

In Click 8.2.0, completion for commands in nested groups doesn't work as expected anymore. Instead of completing command options and/or arguments, completion infinitely repeats the command names for the group.

## Example program

Inspired by the example in #2847 

Save the following as `bin/nested` in a virtualenv

```python
#!/usr/bin/env python

# nested

import click


def _complete_arg(ctx, _, incomplete):
    values = [
        "foo",
        "bar",
        "foobar",
        "baz",
    ]
    return [v for v in values if v.startswith(incomplete)]

@click.group
def main():
    ...

@main.group
def subgroup():
    ...

@subgroup.command
@click.option("--name", help="Name of something.")
@click.argument("arg", shell_complete=_complete_arg)
def compile(name: str | None, arg: str) -> None:
    """Test argument completion in nested groups."""
    click.echo(f"{name=} {arg=}")


if __name__ == "__main__":
    main.main()
```

Run `eval "$(_NESTED_COMPLETE=bash_source nested)"` to setup completion (for bash)

## Completion behavior

|Prefix|8.2.0 completion suggestions (unexpected behavior)|8.1.8 completion suggestions (expected behavior)|
|-|-|-|
|`nested`|`subgroup`|`subgroup`|
|`nested subgroup`|`compile`|`compile`|
|`nested subgroup compile`|`compile`|`foo     bar     foobar  baz`|
|`nested subgroup compile f`|none|`foo     foobar`|
|`nested subgroup compile --`|`--help`|`--name  --help`|
|`nested subgroup compile --name=n`|`compile`|`foo     bar     foobar  baz`|

## Environment

- Python version: Python 3.12.3
- Click version: 8.2.0

## Initial debugging

Reverting 0ef55fda47bb3d0dece21197d81d8d72a4250e73 for `src/click/shell_completion.py` restores the 8.1.8 completion behavior.

Looking at the change, I believe that the bug is actually caused by a simple statement reordering . IMO https://github.com/pallets/click/blob/af73ce4793bf38edf11a16e959ef0d8a0dbf6c1f/src/click/shell_completion.py#L567-L571 should be the following to preserve the 8.1.8 behavior:

```python
                    with cmd.make_context(
                        name, args, parent=ctx, resilient_parsing=True
                    ) as sub_ctx:
                        ctx = sub_ctx
                        args = ctx._protected_args + ctx.args
```

Note that Click 8.1.8 directly assigns `cmd.make_context()` to `ctx` in the equivalent snippet: https://github.com/pallets/click/blob/934813e4d421071a1b3db3973c02fe2721359a6e/src/click/shell_completion.py#L530-L531

In the same fashion, I believe that lines 589 and 590 should be switched in https://github.com/pallets/click/blob/af73ce4793bf38edf11a16e959ef0d8a0dbf6c1f/src/click/shell_completion.py#L581-L590

Pull request description:
fix shell completion for nested groups
continues #2907 
fixes #2906

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 6cdc25c..45e3dac 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,7 @@ Version 8.2.1
 
 -   Fix flag value handling for flag options with a provided type. :issue:`2894`
     :issue:`2897` :pr:`2930`
+-   Fix shell completion for nested groups. :issue:`2906` :pr:`2907`
 
 Version 8.2.0
 -------------
diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py
index cdb5822..6c39d5e 100644
--- a/src/click/shell_completion.py
+++ b/src/click/shell_completion.py
@@ -567,8 +567,8 @@ def _resolve_context(
                     with cmd.make_context(
                         name, args, parent=ctx, resilient_parsing=True
                     ) as sub_ctx:
-                        args = ctx._protected_args + ctx.args
                         ctx = sub_ctx
+                        args = ctx._protected_args + ctx.args
                 else:
                     sub_ctx = ctx
 
@@ -586,8 +586,8 @@ def _resolve_context(
                             allow_interspersed_args=False,
                             resilient_parsing=True,
                         ) as sub_sub_ctx:
-                            args = sub_ctx.args
                             sub_ctx = sub_sub_ctx
+                            args = sub_ctx.args
 
                     ctx = sub_ctx
                     args = [*sub_ctx._protected_args, *sub_ctx.args]


Test patch:
diff --git a/tests/test_shell_completion.py b/tests/test_shell_completion.py
index 3511f07..fb39486 100644
--- a/tests/test_shell_completion.py
+++ b/tests/test_shell_completion.py
@@ -40,6 +40,36 @@ def test_group():
     assert _get_words(cli, [], "-") == ["-a", "--help"]
 
 
+@pytest.mark.parametrize(
+    ("args", "word", "expect"),
+    [
+        ([], "", ["get"]),
+        (["get"], "", ["full"]),
+        (["get", "full"], "", ["data"]),
+        (["get", "full"], "-", ["--verbose", "--help"]),
+        (["get", "full", "data"], "", []),
+        (["get", "full", "data"], "-", ["-a", "--help"]),
+    ],
+)
+def test_nested_group(args: list[str], word: str, expect: list[str]) -> None:
+    cli = Group(
+        "cli",
+        commands=[
+            Group(
+                "get",
+                commands=[
+                    Group(
+                        "full",
+                        params=[Option(["--verbose"])],
+                        commands=[Command("data", params=[Option(["-a"])])],
+                    )
+                ],
+            )
+        ],
+    )
+    assert _get_words(cli, args, word) == expect
+
+
 def test_group_command_same_option():
     cli = Group(
         "cli", params=[Option(["-a"])], commands=[Command("x", params=[Option(["-a"])])]