Repository: clap-rs/clap (language: rust)

Issue:
clap_generate: zsh broken with two multi length arguments
### Please complete the following tasks

- [X] I have searched the [discussions](https://github.com/clap-rs/clap/discussions)
- [X] I have searched the existing issues

### Rust Version

rustc 1.58.0-nightly (8b09ba6a5 2021-11-09)

### Clap Version

3.0.0-beta.5

### Minimal reproducible code

```rust
use clap::{App, IntoApp, Parser};
use clap_generate::{generate, Shell};
use std::io::stdout;

#[derive(Parser)]
pub struct Args {
    pub targets: Vec<String>,
    #[clap(required = true, raw = true)]
    pub files: Vec<String>,
}

fn main() {
    let mut app: App = Args::into_app();
    generate(Shell::Zsh, &mut app, "bug", &mut stdout());
}
```

### Steps to reproduce the bug with the above code

With the completion installed:

```
% command <tab>
_arguments:comparguments:325: doubled rest argument definition: *::file -- Files to search for:
```


### Actual Behaviour

So I have a program that has a usage like this: `program: <targets>... -- <files>...`

The zsh completion seems to really not like this and spits out an error when tab is hit:

`_arguments:comparguments:325: doubled rest argument definition: *::file -- Files to search for:`

The completion generated is:

```sh
#compdef bug

autoload -U is-at-least

_bug() {
    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 information]' \
'--help[Print help information]' \
'*::targets:' \
'*::files:' \
&& ret=0
}

(( $+functions[_bug_commands] )) ||
_bug_commands() {
    local commands; commands=()
    _describe -t commands 'bug commands' commands "$@"
}

_bug "$@"%
```

### Expected Behaviour

Don't be broken

### Additional Context

_No response_

### Debug Output

_No response_


Discussion / hints:
Thanks for reporting this!

One challenge with completions is we have to effectively re-implement clap's argument parsing for each shell we support.  Example of other issues that look like they stem from this:
- https://github.com/clap-rs/clap/issues/2729
- https://github.com/clap-rs/clap/issues/2750
- https://github.com/clap-rs/clap/issues/2145
- https://github.com/clap-rs/clap/issues/1822
- https://github.com/clap-rs/clap/issues/1764

This is making me think that https://github.com/clap-rs/clap/issues/1232 is even more important so we can share parsing logic between different shells *and* clap and more easily test it.
To be more explicitly clear, it's not the parsing logic we need to share but rather the parsing requirements.
The same issue arises with a more common pattern too (see https://github.com/drahnr/clap-completion-zoink/blob/84a3eb106309d066e80645f60a3065525d18c5a4 for a full working example):

```
#[derive(Debug, PartialEq, Eq, clap::Parser)]
#[clap(rename_all = "kebab-case")]
struct Common {
    pub paths: Vec<PathBuf>,
}

#[derive(Debug, PartialEq, Eq, clap::Subcommand)]
#[clap(rename_all = "kebab-case")]
enum Sub {
    Comp {
        #[clap(long)]
        shell: Shell,
    },
// imagine more subcommands here
}

#[derive(clap::Parser, Debug)]
#[clap(author, version, about, long_about = None)]
#[clap(rename_all = "kebab-case")]
struct Args {
    #[clap(flatten)]
    pub common: Common,

    #[clap(subcommand)]
    pub command: Sub,
}
```

On tab completion results in 

```
./target/debug/clap-completion-zoink 
_arguments:comparguments:325: doubled rest argument definition: *::: :->clap-completion-zoink
``` 

```
#compdef clap-completion-zoink

autoload -U is-at-least

_clap-completion-zoink() {
    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 information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
'*::paths:' \
":: :_clap-completion-zoink_commands" \
"*::: :->clap-completion-zoink" \
&& ret=0
    case $state in
    (clap-completion-zoink)
        words=($line[2] "${words[@]}")
        (( CURRENT += 1 ))
        curcontext="${curcontext%:*:*}:clap-completion-zoink-command-$line[2]:"
        case $line[2] in
            (comp)
_arguments "${_arguments_options[@]}" \
'--shell=[]:SHELL: ' \
'-h[Print help information]' \
'--help[Print help information]' \
&& ret=0
;;
(help)
_arguments "${_arguments_options[@]}" \
'*::subcommand -- The subcommand whose help message to display:' \
&& ret=0
;;
        esac
    ;;
esac
}

(( $+functions[_clap-completion-zoink_commands] )) ||
_clap-completion-zoink_commands() {
    local commands; commands=(
'comp:' \
'help:Print this message or the help of the given subcommand(s)' \
    )
    _describe -t commands 'clap-completion-zoink commands' commands "$@"
}
(( $+functions[_clap-completion-zoink__comp_commands] )) ||
_clap-completion-zoink__comp_commands() {
    local commands; commands=()
    _describe -t commands 'clap-completion-zoink comp commands' commands "$@"
}
(( $+functions[_clap-completion-zoink__help_commands] )) ||
_clap-completion-zoink__help_commands() {
    local commands; commands=()
    _describe -t commands 'clap-completion-zoink help commands' commands "$@"
}

_clap-completion-zoink "$@"
```
I meet the same bug.

clap version:
- clap-4.0.22
- clap_complete-4.0.5

The `Cli`: [cli.rs](https://github.com/TD-Sky/conceal/blob/zsh-complete/src/cli.rs)

The code to generate completions: [build.rs](https://github.com/TD-Sky/conceal/blob/zsh-complete/build.rs)

The zsh completion script: [_cnc.rs](https://github.com/TD-Sky/conceal/blob/zsh-complete/completions/_cnc)

## Error

```zsh
_arguments:comparguments:327: doubled rest argument definition: *::: :->cnc
```
I believe https://github.com/clap-rs/clap/pull/4612 may resolve this issue. I ran into this while creating https://github.com/davvid/garden so I figured we might as well fix it (rather than [workaround it](https://davvid.github.io/garden/commands.html#zsh)).

Kindly test it out and let me know if it works for y'all as well.

Fix patch:
diff --git a/clap_complete/src/shells/zsh.rs b/clap_complete/src/shells/zsh.rs
index 15a9ed126dc..04f0a1a9cf0 100644
--- a/clap_complete/src/shells/zsh.rs
+++ b/clap_complete/src/shells/zsh.rs
@@ -622,18 +622,46 @@ fn write_positionals_of(p: &Command) -> String {
 
     let mut ret = vec![];
 
+    // Completions for commands that end with two Vec arguments require special care.
+    // - You can have two Vec args separated with a custom value terminator.
+    // - You can have two Vec args with the second one set to last (raw sets last)
+    //   which will require a '--' separator to be used before the second argument
+    //   on the command-line.
+    //
+    // We use the '-S' _arguments option to disable completion after '--'. Thus, the
+    // completion for the second argument in scenario (B) does not need to be emitted
+    // because it is implicitly handled by the '-S' option.
+    // We only need to emit the first catch-all.
+    //
+    // Have we already emitted a catch-all multi-valued positional argument
+    // without a custom value terminator?
+    let mut catch_all_emitted = false;
+
     for arg in p.get_positionals() {
         debug!("write_positionals_of:iter: arg={}", arg.get_id());
 
         let num_args = arg.get_num_args().expect("built");
+        let is_multi_valued = num_args.max_values() > 1;
+
+        if catch_all_emitted && (arg.is_last_set() || is_multi_valued) {
+            // This is the final argument and it also takes multiple arguments.
+            // We've already emitted a catch-all positional argument so we don't need
+            // to emit anything for this argument because it is implicitly handled by
+            // the use of the '-S' _arguments option.
+            continue;
+        }
+
         let cardinality_value;
-        let cardinality = if num_args.max_values() > 1 {
+        let cardinality = if is_multi_valued {
             match arg.get_value_terminator() {
                 Some(terminator) => {
                     cardinality_value = format!("*{}:", escape_value(terminator));
                     cardinality_value.as_str()
                 }
-                None => "*:",
+                None => {
+                    catch_all_emitted = true;
+                    "*:"
+                }
             }
         } else if !arg.is_required_set() {
             ":"


Test patch:
diff --git a/clap_complete/tests/bash.rs b/clap_complete/tests/bash.rs
index d01651f26cd..a331da84a5b 100644
--- a/clap_complete/tests/bash.rs
+++ b/clap_complete/tests/bash.rs
@@ -111,3 +111,15 @@ fn register_minimal() {
         .action_env("SNAPSHOTS")
         .matches_path("tests/snapshots/register_minimal.bash", buf);
 }
+
+#[test]
+fn two_multi_valued_arguments() {
+    let name = "my-app";
+    let cmd = common::two_multi_valued_arguments_command(name);
+    common::assert_matches_path(
+        "tests/snapshots/two_multi_valued_arguments.bash",
+        clap_complete::shells::Bash,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/common.rs b/clap_complete/tests/common.rs
index 3f8cb437764..eef2e71a0bb 100644
--- a/clap_complete/tests/common.rs
+++ b/clap_complete/tests/common.rs
@@ -261,6 +261,20 @@ pub fn value_terminator_command(name: &'static str) -> clap::Command {
     )
 }
 
+pub fn two_multi_valued_arguments_command(name: &'static str) -> clap::Command {
+    clap::Command::new(name)
+        .arg(
+            clap::Arg::new("first")
+                .help("first multi-valued argument")
+                .num_args(1..),
+        )
+        .arg(
+            clap::Arg::new("second")
+                .help("second multi-valued argument")
+                .raw(true),
+        )
+}
+
 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 26075a67ac1..c2a82fc97c6 100644
--- a/clap_complete/tests/elvish.rs
+++ b/clap_complete/tests/elvish.rs
@@ -95,3 +95,15 @@ fn value_terminator() {
         name,
     );
 }
+
+#[test]
+fn two_multi_valued_arguments() {
+    let name = "my-app";
+    let cmd = common::two_multi_valued_arguments_command(name);
+    common::assert_matches_path(
+        "tests/snapshots/two_multi_valued_arguments.elvish",
+        clap_complete::shells::Elvish,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/fish.rs b/clap_complete/tests/fish.rs
index 9393ed699aa..b5bfae15087 100644
--- a/clap_complete/tests/fish.rs
+++ b/clap_complete/tests/fish.rs
@@ -95,3 +95,15 @@ fn value_terminator() {
         name,
     );
 }
+
+#[test]
+fn two_multi_valued_arguments() {
+    let name = "my-app";
+    let cmd = common::two_multi_valued_arguments_command(name);
+    common::assert_matches_path(
+        "tests/snapshots/two_multi_valued_arguments.fish",
+        clap_complete::shells::Fish,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/powershell.rs b/clap_complete/tests/powershell.rs
index 1f18618b7d2..2de4a05efd6 100644
--- a/clap_complete/tests/powershell.rs
+++ b/clap_complete/tests/powershell.rs
@@ -95,3 +95,15 @@ fn value_terminator() {
         name,
     );
 }
+
+#[test]
+fn two_multi_valued_arguments() {
+    let name = "my-app";
+    let cmd = common::two_multi_valued_arguments_command(name);
+    common::assert_matches_path(
+        "tests/snapshots/two_multi_valued_arguments.ps1",
+        clap_complete::shells::PowerShell,
+        cmd,
+        name,
+    );
+}
diff --git a/clap_complete/tests/snapshots/two_multi_valued_arguments.bash b/clap_complete/tests/snapshots/two_multi_valued_arguments.bash
new file mode 100644
index 00000000000..22afa8be8ac
--- /dev/null
+++ b/clap_complete/tests/snapshots/two_multi_valued_arguments.bash
@@ -0,0 +1,38 @@
+_my-app() {
+    local i cur prev opts cmds
+    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"
+                ;;
+            *)
+                ;;
+        esac
+    done
+
+    case "${cmd}" in
+        my__app)
+            opts="-h --help [first]... [second]..."
+            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
+            ;;
+    esac
+}
+
+complete -F _my-app -o bashdefault -o default my-app
diff --git a/clap_complete/tests/snapshots/two_multi_valued_arguments.elvish b/clap_complete/tests/snapshots/two_multi_valued_arguments.elvish
new file mode 100644
index 00000000000..89b26e0005a
--- /dev/null
+++ b/clap_complete/tests/snapshots/two_multi_valued_arguments.elvish
@@ -0,0 +1,26 @@
+
+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'
+        }
+    ]
+    $completions[$command]
+}
diff --git a/clap_complete/tests/snapshots/two_multi_valued_arguments.fish b/clap_complete/tests/snapshots/two_multi_valued_arguments.fish
new file mode 100644
index 00000000000..a33c671acf5
--- /dev/null
+++ b/clap_complete/tests/snapshots/two_multi_valued_arguments.fish
@@ -0,0 +1,1 @@
+complete -c my-app -s h -l help -d 'Print help'
diff --git a/clap_complete/tests/snapshots/two_multi_valued_arguments.ps1 b/clap_complete/tests/snapshots/two_multi_valued_arguments.ps1
new file mode 100644
index 00000000000..30262afe031
--- /dev/null
+++ b/clap_complete/tests/snapshots/two_multi_valued_arguments.ps1
@@ -0,0 +1,32 @@
+
+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')
+            break
+        }
+    })
+
+    $completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
+        Sort-Object -Property ListItemText
+}
diff --git a/clap_complete/tests/snapshots/two_multi_valued_arguments.zsh b/clap_complete/tests/snapshots/two_multi_valued_arguments.zsh
new file mode 100644
index 00000000000..e0d49622b53
--- /dev/null
+++ b/clap_complete/tests/snapshots/two_multi_valued_arguments.zsh
@@ -0,0 +1,30 @@
+#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]' \
+'*::first -- first multi-valued argument:' \
+&& ret=0
+}
+
+(( $+functions[_my-app_commands] )) ||
+_my-app_commands() {
+    local commands; commands=()
+    _describe -t commands 'my-app commands' commands "$@"
+}
+
+_my-app "$@"
diff --git a/clap_complete/tests/zsh.rs b/clap_complete/tests/zsh.rs
index 57040bde465..f90a7b80ce0 100644
--- a/clap_complete/tests/zsh.rs
+++ b/clap_complete/tests/zsh.rs
@@ -95,3 +95,15 @@ fn value_terminator() {
         name,
     );
 }
+
+#[test]
+fn two_multi_valued_arguments() {
+    let name = "my-app";
+    let cmd = common::two_multi_valued_arguments_command(name);
+    common::assert_matches_path(
+        "tests/snapshots/two_multi_valued_arguments.zsh",
+        clap_complete::shells::Zsh,
+        cmd,
+        name,
+    );
+}