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

Issue:
Unclear help with sets of 3 or more mutually exclusive flags
I defined a set of mutually exclusive flags thusly:
```
enum OutputBehaviour: String, CaseIterable { case stats, count, list }

struct Options: ParsableCommand {
  @Flag(help: "Program output")
  var behaviour: OutputBehaviour
}

let options = Options.parseOrExit()
```

The help message that corresponds to these is not very helpful:
```
$ ./options --help
USAGE: options --stats --count --list

OPTIONS:
  --stats/--count         Program output 
  --list                  Program output 
  -h, --help              Show help information.
```

The help generator lists them in pairs whenever there are more than 2.
It's not clear that the three flags (stats, count, list) are a set.
It's not clear that they are mutually exclusive.

Pull request description:
The original logic assumed that flags would only be grouped in pairs.
Fixes https://github.com/apple/swift-argument-parser/issues/50

Still an issue: with a `@Flag` value typed as a `CaseIterable` enum, its default value doesn't appear. The latter is a new issue https://github.com/apple/swift-argument-parser/issues/56

Patch:
diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift
index 1f6a2be05..646751220 100644
--- a/Sources/ArgumentParser/Usage/HelpGenerator.swift
+++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift
@@ -140,15 +140,31 @@ internal struct HelpGenerator {
         let description: String
         
         if i < args.count - 1 && args[i + 1].help.keys == arg.help.keys {
-          // If the next argument has the same keys as this one, output them together
-          let nextArg = args[i + 1]
+          // If the next argument has the same keys as this one, we have a group of arguments to output together
+          var groupedArgs = [arg]
           let defaultValue = arg.help.defaultValue.map { "(default: \($0))" } ?? ""
-          synopsis = "\(arg.synopsisForHelp ?? "")/\(nextArg.synopsisForHelp ?? "")"
-          description = [arg.help.help?.abstract ?? nextArg.help.help?.abstract, defaultValue]
+          while i < args.count - 1 && args[i + 1].help.keys == arg.help.keys {
+            groupedArgs.append(args[i + 1])
+            i += 1
+          }
+
+          var synopsisString = ""
+          for arg in groupedArgs {
+            if !synopsisString.isEmpty { synopsisString.append("/") }
+            synopsisString.append("\(arg.synopsisForHelp ?? "")")
+          }
+          synopsis = synopsisString
+
+          var descriptionString: String?
+          for arg in groupedArgs {
+            if let desc = arg.help.help?.abstract {
+              descriptionString = desc
+              break
+            }
+          }
+          description = [descriptionString, defaultValue]
             .compactMap { $0 }
             .joined(separator: " ")
-          i += 1
-          
         } else {
           let defaultValue = arg.help.defaultValue.flatMap { $0.isEmpty ? nil : "(default: \($0))" } ?? ""
           synopsis = arg.synopsisForHelp ?? ""


Test patch:
diff --git a/Tests/UnitTests/HelpGenerationTests.swift b/Tests/UnitTests/HelpGenerationTests.swift
index c63a7c5a8..90397c378 100644
--- a/Tests/UnitTests/HelpGenerationTests.swift
+++ b/Tests/UnitTests/HelpGenerationTests.swift
@@ -124,4 +124,48 @@ extension HelpGenerationTests {
 
             """)
   }
+
+  enum OutputBehaviour: String, CaseIterable { case stats, count, list }
+  struct E: ParsableCommand {
+    @Flag(name: .shortAndLong, help: "Change the program output")
+    var behaviour: OutputBehaviour
+  }
+  struct F: ParsableCommand {
+    @Flag(name: .short, default: .list, help: "Change the program output")
+    var behaviour: OutputBehaviour
+  }
+  struct G: ParsableCommand {
+    @Flag(inversion: .prefixedNo, help: "Whether to flag")
+    var flag: Bool
+  }
+
+  func testHelpWithMutuallyExclusiveFlags() {
+    AssertHelp(for: E.self, equals: """
+               USAGE: e --stats --count --list
+
+               OPTIONS:
+                 -s, --stats/-c, --count/-l, --list
+                                         Change the program output
+                 -h, --help              Show help information.
+
+               """)
+
+    AssertHelp(for: F.self, equals: """
+               USAGE: f [-s] [-c] [-l]
+
+               OPTIONS:
+                 -s/-c/-l                Change the program output
+                 -h, --help              Show help information.
+
+               """)
+
+    AssertHelp(for: G.self, equals: """
+               USAGE: g [--flag] [--no-flag]
+
+               OPTIONS:
+                 --flag/--no-flag        Whether to flag (default: false)
+                 -h, --help              Show help information.
+
+               """)
+  }
 }