Repository: clap-rs/clap (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_

Pull request:
clap_complete: fix support for two multi-length arguments in zsh
Properly handle two consecutive multi-valued arguments.

Currently the zsh completion generates an error:

    $ my-app <TAB>
    _arguments:comparguments:325: doubled rest argument definition:
    *::second -- second set of of multi-length arguments:

Special-case multi-valued arguments to avoid emitting duplicate catch-all arguments.

Closes #3022
Signed-off-by: David Aguilar <davvid@gmail.com>

Patch:
diff --git a/clap_complete/src/shells/zsh.rs b/clap_complete/src/shells/zsh.rs
index 15a9ed12..04f0a1a9 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 d01651f2..a331da84 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 3f8cb437..eef2e71a 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 26075a67..c2a82fc9 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 9393ed69..b5bfae15 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 1f18618b..2de4a05e 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 00000000..22afa8be
--- /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 00000000..89b26e00
--- /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 00000000..a33c671a
--- /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 00000000..30262afe
--- /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 00000000..e0d49622
--- /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 57040bde..f90a7b80 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,
+    );
+}