Repository: apple/swift-argument-parser (swift)

Issue:
Zsh completion script only shows a completion for the first time in an array.
If a command has either an option or argument with an array type and `completion` set to anything, the completions will only be used for the first item typed into the terminal. Any following items wont get any completions.

In the case of an option with parsing set to one of the single value options, the option and value will only be suggested the first time. Afterwards the option will no longer be given as a completion.

**ArgumentParser version:** 1.2.2
**Swift version:**
Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
Target: arm64-apple-macosx13.0

### Checklist
- [x] If possible, I've reproduced the issue using the `main` branch of this package
- [x] I've searched for [existing GitHub issues](https://github.com/apple/swift-argument-parser/issues)

### Steps to Reproduce

```
import ArgumentParser

@main
struct PhoneticAlphabet: ParsableCommand {
    @Option(completion: .list(words))
    var option: [String]
    
    @Argument(completion: .list(words))
    var argument: [String]
    
    func run() throws {
        print("options:", option)
        print("arguments:", argument)
    }
}

private extension PhoneticAlphabet {
    static var words: [String] {
        ["alfa", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel", "india", "juliett", "kilo", "lima", "mike", "november", "oscar", "papa", "quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey", "xray", "yankee", "zulu"]
    }
}
```

1. Build the above command
2. Install the zsh completions for it using `{Command name} --generate-completion-script zsh` and putting the completions in the correct location for your terminal setup. For me it's `~/.oh-my-zsh/completions`.
3. Start typing the command using completions for the option
    1. Type `{Command name} --o` then press tab to complete the option name
    2. Type `a` then tab to complete the word `alfa`
    3. Type `--o` then press tab again
4. Or, start typing the command using completions for the argument
    1. Type `{Command name} a` then press tab to complete the word `alfa`
    2. Type `a` then press tab again

### Expected behavior
For step 3, it should complete `--option` again since it can be passed multiple times to the command to build the `option` array.

For step 4, it should complete `alfa` again since it be passed multiple strings to build the `argument` array.

### Actual behavior
After pressing tab the expected word is not completed. Either nothing is completed or possibly a different flag/option/argument is completed depending on what letter is typed, ie. it might complete `--help` depending on the terminal's completion settings.

Pull request description:
Some commands allow an option to be repeated to provide multiple values. For example, ssh allows the -L flag to be repeated to establish multiple port forwardings.

ArgumentParser supports this style when the Option value is an Array and the parsingStrategy is ArrayParsingStrategy.singleValue or .unconditionalSingleValue.

Without this patch, ArgumentParser generates a zsh completion script that does not handle repeatable options correctly. The generated script suppresses completion of any option after that option's first use, even if the option is repeatable.

With this patch, ArgumentParser generates a zsh completion script that allows a repeatable option to be completed each time it is used.

The relevant zsh _arguments syntax is documented here: https://zsh.sourceforge.io/Doc/Release/Completion-System.html#Completion-Functions

Specifically, a repeatable option's optspec needs to start with '*'. Furthermore, a repeatable argument must not list itself or its synonyms in its own parenthesized suppression list. (This is not clearly documented.)

fixes #564

### Checklist
- [x] I've added at least one test that validates that my change is working, if appropriate
- [x] I've followed the code style of the rest of the project
- [x] I've read the [Contribution Guidelines](https://github.com/apple/swift-argument-parser/blob/main/CONTRIBUTING.md)
- [x] I've updated the documentation if necessary

Patch:
diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift
index dea09615e..96143bd02 100644
--- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift
+++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift
@@ -137,7 +137,7 @@ extension ArgumentDefinition {
   func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? {
     guard help.visibility.base == .default else { return nil }
     
-    var inputs: String
+    let inputs: String
     switch update {
     case .unary:
       inputs = ":\(valueName):\(zshActionString(commands))"
@@ -150,13 +150,15 @@ extension ArgumentDefinition {
     case 0:
       line = ""
     case 1:
+      let star = isRepeatableOption ? "*" : ""
       line = """
-      \(names[0].synopsisString)\(zshCompletionAbstract)
+      \(star)\(names[0].synopsisString)\(zshCompletionAbstract)
       """
     default:
       let synopses = names.map { $0.synopsisString }
+      let suppression = isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))"
       line = """
-      (\(synopses.joined(separator: " ")))'\
+      \(suppression)'\
       {\(synopses.joined(separator: ","))}\
       '\(zshCompletionAbstract)
       """
@@ -165,6 +167,19 @@ extension ArgumentDefinition {
     return "'\(line)\(inputs)'"
   }
 
+  /// - returns: `true` if I'm an option and can be tab-completed multiple times in one command line. For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings.
+  private var isRepeatableOption: Bool {
+    guard
+      case .named(_) = kind,
+      help.options.contains(.isRepeating)
+    else { return false }
+
+    switch parsingStrategy {
+    case .default, .unconditional: return true
+    default: return false
+    }
+  }
+
   /// Returns the zsh "action" for an argument completion string.
   func zshActionString(_ commands: [ParsableCommand.Type]) -> String {
     switch completion.kind {


Test patch:
diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
index f9b3458d4..b9aa7cd72 100644
--- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
+++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
@@ -29,7 +29,7 @@ extension CompletionScriptTests {
     }
   }
     
-  enum Kind: String, ExpressibleByArgument, CaseIterable {
+  enum Kind: String, ExpressibleByArgument, EnumerableFlag {
     case one, two, three = "custom-three"
   }
   
@@ -48,6 +48,11 @@ extension CompletionScriptTests {
     @Option(completion: .list(["a", "b", "c"])) var path3: Path
     
     @Flag(help: .hidden) var verbose = false
+    @Flag var allowedKinds: [Kind] = []
+    @Flag var kindCounter: Int
+    
+    @Option() var rep1: [String]
+    @Option(name: [.short, .long]) var rep2: [String]
 
    struct SubCommand: ParsableCommand {
      static var configuration = CommandConfiguration(
@@ -164,6 +169,12 @@ _base-test() {
         '--path1:path1:_files'
         '--path2:path2:_files'
         '--path3:path3:(a b c)'
+        '--one'
+        '--two'
+        '--three'
+        '*--kind-counter'
+        '*--rep1:rep1:'
+        '*'{-r,--rep2}':rep2:'
         '(-h --help)'{-h,--help}'[Show help information.]'
         '(-): :->command'
         '(-)*:: :->arg'
@@ -231,7 +242,7 @@ _base_test() {
     cur="${COMP_WORDS[COMP_CWORD]}"
     prev="${COMP_WORDS[COMP_CWORD-1]}"
     COMPREPLY=()
-    opts="--name --kind --other-kind --path1 --path2 --path3 -h --help sub-command help"
+    opts="--name --kind --other-kind --path1 --path2 --path3 --one --two --three --kind-counter --rep1 -r --rep2 -h --help sub-command help"
     if [[ $COMP_CWORD == "1" ]]; then
         COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
         return
@@ -269,6 +280,14 @@ _base_test() {
             COMPREPLY=( $(compgen -W "a b c" -- "$cur") )
             return
         ;;
+        --rep1)
+
+            return
+        ;;
+        -r|--rep2)
+
+            return
+        ;;
     esac
     case ${COMP_WORDS[1]} in
         (sub-command)
@@ -370,12 +389,18 @@ function _swift_base-test_using_command
 end
 
 complete -c base-test -n '_swift_base-test_using_command "base-test sub-command"' -s h -l help -d 'Show help information.'
-complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l name -d 'The user\\\'s name.'
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l name -d 'The user\\'s name.'
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l kind -r -f -k -a 'one two custom-three'
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l other-kind -r -f -k -a '1 2 3'
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l path1 -r -f -a '(for i in *.{}; echo $i;end)'
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l path2 -r -f -a '(for i in *.{}; echo $i;end)'
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l path3 -r -f -k -a 'a b c'
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l one
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l two
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l three
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l kind-counter
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -l rep1
+complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -s r -l rep2
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -s h -l help -d 'Show help information.'
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -f -a 'sub-command' -d ''
 complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command help"' -f -a 'help' -d 'Show subcommand help information.'