Repository: clap-rs/clap (rust)

Issue:
completion: `last(true)` + subcommands causes 'doubled rest argument' error in Zsh
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the [open](https://github.com/clap-rs/clap/issues) and [rejected](https://github.com/clap-rs/clap/issues?q=is%3Aissue+label%3AS-wont-fix+is%3Aclosed) issues

### Rust Version

`rustc 1.71.0-nightly (f9a6b7158 2023-05-05)`

### Clap Version

4.2.7

### Minimal reproducible code

```rs
use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::generate;

#[derive(Parser)]
struct Opt {
    #[arg(last(true))]
    pub free_args: Vec<String>,
    #[command(subcommand)]
    command: SubOpt,
}

#[derive(Subcommand)]
enum SubOpt {
    Foo,
    Bar,
}

fn main() {
    let mut cmd = Opt::command();
    let name = cmd.get_name().to_string();
    generate(clap_complete::shells::Zsh, &mut cmd, name, &mut std::io::stdout());
}
```

### Steps to reproduce the bug with the above code

Redirect the output to a file, use `source poc.zsh` to load it, then enter `<app-name>` and hit tab.

### Actual Behaviour

`_arguments:comparguments:327: doubled rest argument definition: *::: :->poc`

### Expected Behaviour

An error should not occur and completion should work as normal.

### Additional Context

I'm unsure if this is related to #4652, but it prevents completion from working *at all*

### Debug Output

_No response_

Pull request:
fix: (complete, zsh) Don't emit catchall when we have subcommands
Fixes #4898

I'm not entirely sure this is correct, but it fixes the linked case and doesn't regress any existing tests.

Patch:
diff --git a/clap_complete/src/shells/zsh.rs b/clap_complete/src/shells/zsh.rs
index 71c586d8..65d7af65 100644
--- a/clap_complete/src/shells/zsh.rs
+++ b/clap_complete/src/shells/zsh.rs
@@ -624,7 +624,9 @@ fn write_positionals_of(p: &Command) -> String {
         }
 
         let cardinality_value;
-        let cardinality = if is_multi_valued {
+        // If we have any subcommands, we'll emit a catch-all argument, so we shouldn't
+        // emit one here.
+        let cardinality = if is_multi_valued && !p.has_subcommands() {
             match arg.get_value_terminator() {
                 Some(terminator) => {
                     cardinality_value = format!("*{}:", escape_value(terminator));


Test patch:
diff --git a/clap_complete/tests/bash.rs b/clap_complete/tests/bash.rs
index a331da84..b672e1cf 100644
--- a/clap_complete/tests/bash.rs
+++ b/clap_complete/tests/bash.rs
@@ -123,3 +123,15 @@ fn two_multi_valued_arguments() {
         name,
     );
 }
+
+#[test]
+fn subcommand_last() {
+    let name = "my-app";
+    let cmd = common::subcommand_last(name);
+    common::assert_matches_path(
+        "tests/snapshots/subcommand_last.bash",
+        clap_complete::shells::Bash,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/common.rs b/clap_complete/tests/common.rs
index eef2e71a..1eb530f1 100644
--- a/clap_complete/tests/common.rs
+++ b/clap_complete/tests/common.rs
@@ -275,6 +275,12 @@ pub fn two_multi_valued_arguments_command(name: &'static str) -> clap::Command {
         )
 }
 
+pub fn subcommand_last(name: &'static str) -> clap::Command {
+    clap::Command::new(name)
+        .arg(clap::Arg::new("free").last(true))
+        .subcommands([clap::Command::new("foo"), clap::Command::new("bar")])
+}
+
 pub fn assert_matches_path(
     expected_path: impl AsRef<std::path::Path>,
     gen: impl clap_complete::Generator,
diff --git a/clap_complete/tests/elvish.rs b/clap_complete/tests/elvish.rs
index c2a82fc9..24e1e956 100644
--- a/clap_complete/tests/elvish.rs
+++ b/clap_complete/tests/elvish.rs
@@ -107,3 +107,15 @@ fn two_multi_valued_arguments() {
         name,
     );
 }
+
+#[test]
+fn subcommand_last() {
+    let name = "my-app";
+    let cmd = common::subcommand_last(name);
+    common::assert_matches_path(
+        "tests/snapshots/subcommand_last.elvish",
+        clap_complete::shells::Elvish,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/fish.rs b/clap_complete/tests/fish.rs
index b5bfae15..e36d8f96 100644
--- a/clap_complete/tests/fish.rs
+++ b/clap_complete/tests/fish.rs
@@ -107,3 +107,15 @@ fn two_multi_valued_arguments() {
         name,
     );
 }
+
+#[test]
+fn subcommand_last() {
+    let name = "my-app";
+    let cmd = common::subcommand_last(name);
+    common::assert_matches_path(
+        "tests/snapshots/subcommand_last.fish",
+        clap_complete::shells::Fish,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/powershell.rs b/clap_complete/tests/powershell.rs
index 2de4a05e..4f4b0a63 100644
--- a/clap_complete/tests/powershell.rs
+++ b/clap_complete/tests/powershell.rs
@@ -107,3 +107,15 @@ fn two_multi_valued_arguments() {
         name,
     );
 }
+
+#[test]
+fn subcommand_last() {
+    let name = "my-app";
+    let cmd = common::subcommand_last(name);
+    common::assert_matches_path(
+        "tests/snapshots/subcommand_last.ps1",
+        clap_complete::shells::PowerShell,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/snapshots/subcommand_last.bash b/clap_complete/tests/snapshots/subcommand_last.bash
new file mode 100644
index 00000000..f3ca85b9
--- /dev/null
+++ b/clap_complete/tests/snapshots/subcommand_last.bash
@@ -0,0 +1,140 @@
+_my-app() {
+    local i cur prev opts cmd
+    COMPREPLY=()
+    cur="${COMP_WORDS[COMP_CWORD]}"
+    prev="${COMP_WORDS[COMP_CWORD-1]}"
+    cmd=""
+    opts=""
+
+    for i in ${COMP_WORDS[@]}
+    do
+        case "${cmd},${i}" in
+            ",$1")
+                cmd="my__app"
+                ;;
+            my__app,bar)
+                cmd="my__app__bar"
+                ;;
+            my__app,foo)
+                cmd="my__app__foo"
+                ;;
+            my__app,help)
+                cmd="my__app__help"
+                ;;
+            my__app__help,bar)
+                cmd="my__app__help__bar"
+                ;;
+            my__app__help,foo)
+                cmd="my__app__help__foo"
+                ;;
+            my__app__help,help)
+                cmd="my__app__help__help"
+                ;;
+            *)
+                ;;
+        esac
+    done
+
+    case "${cmd}" in
+        my__app)
+            opts="-h --help [free] foo bar help"
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+        my__app__bar)
+            opts="-h --help"
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+        my__app__foo)
+            opts="-h --help"
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+        my__app__help)
+            opts="foo bar help"
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+        my__app__help__bar)
+            opts=""
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+        my__app__help__foo)
+            opts=""
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+        my__app__help__help)
+            opts=""
+            if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
+                COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+                return 0
+            fi
+            case "${prev}" in
+                *)
+                    COMPREPLY=()
+                    ;;
+            esac
+            COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
+            return 0
+            ;;
+    esac
+}
+
+complete -F _my-app -o bashdefault -o default my-app
diff --git a/clap_complete/tests/snapshots/subcommand_last.elvish b/clap_complete/tests/snapshots/subcommand_last.elvish
new file mode 100644
index 00000000..9a93f09b
--- /dev/null
+++ b/clap_complete/tests/snapshots/subcommand_last.elvish
@@ -0,0 +1,48 @@
+
+use builtin;
+use str;
+
+set edit:completion:arg-completer[my-app] = {|@words|
+    fn spaces {|n|
+        builtin:repeat $n ' ' | str:join ''
+    }
+    fn cand {|text desc|
+        edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
+    }
+    var command = 'my-app'
+    for word $words[1..-1] {
+        if (str:has-prefix $word '-') {
+            break
+        }
+        set command = $command';'$word
+    }
+    var completions = [
+        &'my-app'= {
+            cand -h 'Print help'
+            cand --help 'Print help'
+            cand foo 'foo'
+            cand bar 'bar'
+            cand help 'Print this message or the help of the given subcommand(s)'
+        }
+        &'my-app;foo'= {
+            cand -h 'Print help'
+            cand --help 'Print help'
+        }
+        &'my-app;bar'= {
+            cand -h 'Print help'
+            cand --help 'Print help'
+        }
+        &'my-app;help'= {
+            cand foo 'foo'
+            cand bar 'bar'
+            cand help 'Print this message or the help of the given subcommand(s)'
+        }
+        &'my-app;help;foo'= {
+        }
+        &'my-app;help;bar'= {
+        }
+        &'my-app;help;help'= {
+        }
+    ]
+    $completions[$command]
+}
diff --git a/clap_complete/tests/snapshots/subcommand_last.fish b/clap_complete/tests/snapshots/subcommand_last.fish
new file mode 100644
index 00000000..5226a932
--- /dev/null
+++ b/clap_complete/tests/snapshots/subcommand_last.fish
@@ -0,0 +1,9 @@
+complete -c my-app -n "__fish_use_subcommand" -s h -l help -d 'Print help'
+complete -c my-app -n "__fish_use_subcommand" -f -a "foo"
+complete -c my-app -n "__fish_use_subcommand" -f -a "bar"
+complete -c my-app -n "__fish_use_subcommand" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
+complete -c my-app -n "__fish_seen_subcommand_from foo" -s h -l help -d 'Print help'
+complete -c my-app -n "__fish_seen_subcommand_from bar" -s h -l help -d 'Print help'
+complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from foo; and not __fish_seen_subcommand_from bar; and not __fish_seen_subcommand_from help" -f -a "foo"
+complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from foo; and not __fish_seen_subcommand_from bar; and not __fish_seen_subcommand_from help" -f -a "bar"
+complete -c my-app -n "__fish_seen_subcommand_from help; and not __fish_seen_subcommand_from foo; and not __fish_seen_subcommand_from bar; and not __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
diff --git a/clap_complete/tests/snapshots/subcommand_last.ps1 b/clap_complete/tests/snapshots/subcommand_last.ps1
new file mode 100644
index 00000000..71c4cd8e
--- /dev/null
+++ b/clap_complete/tests/snapshots/subcommand_last.ps1
@@ -0,0 +1,60 @@
+
+using namespace System.Management.Automation
+using namespace System.Management.Automation.Language
+
+Register-ArgumentCompleter -Native -CommandName 'my-app' -ScriptBlock {
+    param($wordToComplete, $commandAst, $cursorPosition)
+
+    $commandElements = $commandAst.CommandElements
+    $command = @(
+        'my-app'
+        for ($i = 1; $i -lt $commandElements.Count; $i++) {
+            $element = $commandElements[$i]
+            if ($element -isnot [StringConstantExpressionAst] -or
+                $element.StringConstantType -ne [StringConstantType]::BareWord -or
+                $element.Value.StartsWith('-') -or
+                $element.Value -eq $wordToComplete) {
+                break
+        }
+        $element.Value
+    }) -join ';'
+
+    $completions = @(switch ($command) {
+        'my-app' {
+            [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
+            [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
+            [CompletionResult]::new('foo', 'foo', [CompletionResultType]::ParameterValue, 'foo')
+            [CompletionResult]::new('bar', 'bar', [CompletionResultType]::ParameterValue, 'bar')
+            [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)')
+            break
+        }
+        'my-app;foo' {
+            [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
+            [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
+            break
+        }
+        'my-app;bar' {
+            [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
+            [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
+            break
+        }
+        'my-app;help' {
+            [CompletionResult]::new('foo', 'foo', [CompletionResultType]::ParameterValue, 'foo')
+            [CompletionResult]::new('bar', 'bar', [CompletionResultType]::ParameterValue, 'bar')
+            [CompletionResult]::new('help', 'help', [CompletionResultType]::ParameterValue, 'Print this message or the help of the given subcommand(s)')
+            break
+        }
+        'my-app;help;foo' {
+            break
+        }
+        'my-app;help;bar' {
+            break
+        }
+        'my-app;help;help' {
+            break
+        }
+    })
+
+    $completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
+        Sort-Object -Property ListItemText
+}
diff --git a/clap_complete/tests/snapshots/subcommand_last.zsh b/clap_complete/tests/snapshots/subcommand_last.zsh
new file mode 100644
index 00000000..1dc63fe5
--- /dev/null
+++ b/clap_complete/tests/snapshots/subcommand_last.zsh
@@ -0,0 +1,123 @@
+#compdef my-app
+
+autoload -U is-at-least
+
+_my-app() {
+    typeset -A opt_args
+    typeset -a _arguments_options
+    local ret=1
+
+    if is-at-least 5.2; then
+        _arguments_options=(-s -S -C)
+    else
+        _arguments_options=(-s -C)
+    fi
+
+    local context curcontext="$curcontext" state line
+    _arguments "${_arguments_options[@]}" \
+'-h[Print help]' \
+'--help[Print help]' \
+'::free:' \
+":: :_my-app_commands" \
+"*::: :->my-app" \
+&& ret=0
+    case $state in
+    (my-app)
+        words=($line[2] "${words[@]}")
+        (( CURRENT += 1 ))
+        curcontext="${curcontext%:*:*}:my-app-command-$line[2]:"
+        case $line[2] in
+            (foo)
+_arguments "${_arguments_options[@]}" \
+'-h[Print help]' \
+'--help[Print help]' \
+&& ret=0
+;;
+(bar)
+_arguments "${_arguments_options[@]}" \
+'-h[Print help]' \
+'--help[Print help]' \
+&& ret=0
+;;
+(help)
+_arguments "${_arguments_options[@]}" \
+":: :_my-app__help_commands" \
+"*::: :->help" \
+&& ret=0
+
+    case $state in
+    (help)
+        words=($line[1] "${words[@]}")
+        (( CURRENT += 1 ))
+        curcontext="${curcontext%:*:*}:my-app-help-command-$line[1]:"
+        case $line[1] in
+            (foo)
+_arguments "${_arguments_options[@]}" \
+&& ret=0
+;;
+(bar)
+_arguments "${_arguments_options[@]}" \
+&& ret=0
+;;
+(help)
+_arguments "${_arguments_options[@]}" \
+&& ret=0
+;;
+        esac
+    ;;
+esac
+;;
+        esac
+    ;;
+esac
+}
+
+(( $+functions[_my-app_commands] )) ||
+_my-app_commands() {
+    local commands; commands=(
+'foo:' \
+'bar:' \
+'help:Print this message or the help of the given subcommand(s)' \
+    )
+    _describe -t commands 'my-app commands' commands "$@"
+}
+(( $+functions[_my-app__bar_commands] )) ||
+_my-app__bar_commands() {
+    local commands; commands=()
+    _describe -t commands 'my-app bar commands' commands "$@"
+}
+(( $+functions[_my-app__help__bar_commands] )) ||
+_my-app__help__bar_commands() {
+    local commands; commands=()
+    _describe -t commands 'my-app help bar commands' commands "$@"
+}
+(( $+functions[_my-app__foo_commands] )) ||
+_my-app__foo_commands() {
+    local commands; commands=()
+    _describe -t commands 'my-app foo commands' commands "$@"
+}
+(( $+functions[_my-app__help__foo_commands] )) ||
+_my-app__help__foo_commands() {
+    local commands; commands=()
+    _describe -t commands 'my-app help foo commands' commands "$@"
+}
+(( $+functions[_my-app__help_commands] )) ||
+_my-app__help_commands() {
+    local commands; commands=(
+'foo:' \
+'bar:' \
+'help:Print this message or the help of the given subcommand(s)' \
+    )
+    _describe -t commands 'my-app help commands' commands "$@"
+}
+(( $+functions[_my-app__help__help_commands] )) ||
+_my-app__help__help_commands() {
+    local commands; commands=()
+    _describe -t commands 'my-app help help commands' commands "$@"
+}
+
+if [ "$funcstack[1]" = "_my-app" ]; then
+    _my-app "$@"
+else
+    compdef _my-app my-app
+fi
diff --git a/clap_complete/tests/zsh.rs b/clap_complete/tests/zsh.rs
index f90a7b80..7a3603b5 100644
--- a/clap_complete/tests/zsh.rs
+++ b/clap_complete/tests/zsh.rs
@@ -107,3 +107,15 @@ fn two_multi_valued_arguments() {
         name,
     );
 }
+
+#[test]
+fn subcommand_last() {
+    let name = "my-app";
+    let cmd = common::subcommand_last(name);
+    common::assert_matches_path(
+        "tests/snapshots/subcommand_last.zsh",
+        clap_complete::shells::Zsh,
+        cmd,
+        name,
+    );
+}