Repository: apple/swift-argument-parser
Pull request #51 (2020-03-02 23:33:11)

Add support for exiting without printing an error
### Description

This adds an `ExitCode` error type that stores an exit code, and updates the exit machinery (1) to use `ExitCode` as the source for exit codes, and (2) to silently exit when the provided error is an `ExitCode` instance.

New API:

```swift
/// An error type that only includes an exit code.
///
/// If you're printing custom errors messages yourself, you can throw this error
/// to specify the exit code without adding any additional output to standard
/// out or standard error.
public struct ExitCode: Error {
   /// Creates a new `ExitCode` with the given code.
   public init(_ code: Int32)

   /// An exit code that indicates successful completion of a command.
   public static let success

   /// An exit code that indicates that the command failed.
   public static let failure

   /// An exit code that indicates that the user provided invalid input.
   public static let validationFailure
}
```

### Documentation Plan
Included API documentation and expanded a section in the [Validation & Errors guide](https://github.com/apple/swift-argument-parser/pull/51/files#diff-d57fe4925b10ddced8a151a63627b3de).

### Test Plan
Added tests for failing silently and for exiting with each preset error code and a custom error code.

### Source Impact
The new type is an additive change. There's an additional behavior change that I'd call a bug fix, where errors that convert to an empty string now don't print the `"Error:"` prefix by itself.



Patch:
diff --git a/Documentation/05 Validation and Errors.md b/Documentation/05 Validation and Errors.md
index ffe8453..b9bf00e 100644
--- a/Documentation/05 Validation and Errors.md
+++ b/Documentation/05 Validation and Errors.md
@@ -57,7 +57,7 @@ hey
 
 ## Handling Post-Validation Errors
 
-The `ValidationError` type is a special `ArgumentParser` error — a validation error's message is always accompanied by an appropriate usage string. You can throw other errors, from either the `validate()` or `run()` method to indicate that something has gone wrong that isn't validation-specific.
+The `ValidationError` type is a special `ArgumentParser` error — a validation error's message is always accompanied by an appropriate usage string. You can throw other errors, from either the `validate()` or `run()` method to indicate that something has gone wrong that isn't validation-specific. Errors that conform to `CustomStringConvertible` or `LocalizedError` provide the best experience for users.
 
 ```swift
 struct LineCount: ParsableCommand {
@@ -80,3 +80,23 @@ The throwing `String(contentsOfFile:encoding:)` initializer fails when the user
 Error: The file “non-existing-file.swift” couldn’t be opened because
 there is no such file.
 ```
+
+If you print your error output yourself, you still need to throw an error from `validate()` or `run()`, so that your command exits with the appropriate exit code. To avoid printing an extra error message, use the `ExitCode` error, which has static properties for success, failure, and validation errors, or lets you specify a specific exit code.
+
+```swift
+struct RuntimeError: Error, CustomStringConvertible {
+    var description: String
+}
+
+struct Example: ParsableCommand {
+    @Argument() var inputFile: String
+    
+    func run() throws {
+        if !ExampleCore.processFile(inputFile) {
+            // ExampleCore.processFile(_:) prints its own errors
+            // and returns `false` on failure.
+            throw ExitCode.failure
+        }
+    }
+}
+```
diff --git a/Examples/math/main.swift b/Examples/math/main.swift
index 4d3dcfd..379a665 100644
--- a/Examples/math/main.swift
+++ b/Examples/math/main.swift
@@ -186,6 +186,34 @@ extension Math.Statistics {
 
         @Argument(help: "A group of floating-point values to operate on.")
         var values: [Double]
+
+        // These args and the validation method are for testing exit codes:
+        @Flag(help: .hidden)
+        var testSuccessExitCode: Bool
+        @Flag(help: .hidden)
+        var testFailureExitCode: Bool
+        @Flag(help: .hidden)
+        var testValidationExitCode: Bool
+        @Option(help: .hidden)
+        var testCustomExitCode: Int32?
+      
+        func validate() throws {
+            if testSuccessExitCode {
+                throw ExitCode.success
+            }
+            
+            if testFailureExitCode {
+                throw ExitCode.failure
+            }
+            
+            if testValidationExitCode {
+                throw ExitCode.validationFailure
+            }
+            
+            if let exitCode = testCustomExitCode {
+                throw ExitCode(exitCode)
+            }
+        }
     }
 }
 
diff --git a/Sources/ArgumentParser/CMakeLists.txt b/Sources/ArgumentParser/CMakeLists.txt
index bf6715e..11e3c49 100644
--- a/Sources/ArgumentParser/CMakeLists.txt
+++ b/Sources/ArgumentParser/CMakeLists.txt
@@ -1,11 +1,11 @@
 add_library(ArgumentParser
   "Parsable Properties/Argument.swift"
   "Parsable Properties/ArgumentHelp.swift"
+  "Parsable Properties/Errors.swift"
   "Parsable Properties/Flag.swift"
   "Parsable Properties/NameSpecification.swift"
   "Parsable Properties/Option.swift"
   "Parsable Properties/OptionGroup.swift"
-  "Parsable Properties/ValidationError.swift"
 
   "Parsable Types/CommandConfiguration.swift"
   "Parsable Types/ExpressibleByArgument.swift"
diff --git a/Sources/ArgumentParser/Parsable Properties/ValidationError.swift b/Sources/ArgumentParser/Parsable Properties/Errors.swift
similarity index 70%
rename from Sources/ArgumentParser/Parsable Properties/ValidationError.swift
rename to Sources/ArgumentParser/Parsable Properties/Errors.swift
index ac6ef6a..47c9bc5 100644
--- a/Sources/ArgumentParser/Parsable Properties/ValidationError.swift
+++ b/Sources/ArgumentParser/Parsable Properties/Errors.swift
@@ -9,6 +9,14 @@
 //
 //===----------------------------------------------------------------------===//
 
+#if canImport(Glibc)
+import Glibc
+#elseif canImport(Darwin)
+import Darwin
+#elseif canImport(MSVCRT)
+import MSVCRT
+#endif
+
 /// An error type that is presented to the user as an error with parsing their
 /// command-line input.
 public struct ValidationError: Error, CustomStringConvertible {
@@ -24,6 +32,29 @@ public struct ValidationError: Error, CustomStringConvertible {
   }
 }
 
+/// An error type that only includes an exit code.
+///
+/// If you're printing custom errors messages yourself, you can throw this error
+/// to specify the exit code without adding any additional output to standard
+/// out or standard error.
+public struct ExitCode: Error {
+  var code: Int32
+
+  /// Creates a new `ExitCode` with the given code.
+  public init(_ code: Int32) {
+    self.code = code
+  }
+  
+  /// An exit code that indicates successful completion of a command.
+  public static let success = ExitCode(EXIT_SUCCESS)
+  
+  /// An exit code that indicates that the command failed.
+  public static let failure = ExitCode(EXIT_FAILURE)
+  
+  /// An exit code that indicates that the user provided invalid input.
+  public static let validationFailure = ExitCode(EX_USAGE)
+}
+
 /// An error type that represents a clean (i.e. non-error state) exit of the
 /// utility.
 ///
diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift
index 2f52ee0..5dec6a0 100644
--- a/Sources/ArgumentParser/Parsable Properties/Flag.swift
+++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift
@@ -222,7 +222,7 @@ extension Flag where Value: CaseIterable, Value: RawRepresentable, Value.RawValu
           switch (hasUpdated, previous, exclusivity) {
           case (true, let p?, .exclusive):
             // This value has already been set.
-            throw ParserError.duplicateExclusiveValues(previous: p.inputOrigin, duplicate: origin)
+            throw ParserError.duplicateExclusiveValues(previous: p.inputOrigin, duplicate: origin, originalInput: values.originalInput)
           case (false, _, _), (_, _, .chooseLast):
             values.set(value, forKey: key, inputOrigin: origin)
           default:
diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift
index 6091038..bf88162 100644
--- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift
+++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift
@@ -139,14 +139,16 @@ extension ParsableArguments {
     withError error: Error? = nil
   ) -> Never {
     guard let error = error else {
-      _exit(EXIT_SUCCESS)
+      _exit(ExitCode.success.code)
     }
     
     let messageInfo = MessageInfo(error: error, type: self)
-    if messageInfo.shouldExitCleanly {
-      print(messageInfo.fullText)
-    } else {
-      print(messageInfo.fullText, to: &standardError)
+    if !messageInfo.fullText.isEmpty {
+      if messageInfo.shouldExitCleanly {
+        print(messageInfo.fullText)
+      } else {
+        print(messageInfo.fullText, to: &standardError)
+      }
     }
     _exit(messageInfo.exitCode)
   }
diff --git a/Sources/ArgumentParser/Parsing/InputOrigin.swift b/Sources/ArgumentParser/Parsing/InputOrigin.swift
index fee4eb1..34d6966 100644
--- a/Sources/ArgumentParser/Parsing/InputOrigin.swift
+++ b/Sources/ArgumentParser/Parsing/InputOrigin.swift
@@ -22,12 +22,7 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral {
   
   private var _elements: Set<Element> = []
   var elements: [Element] {
-    get {
-      Array(_elements).sorted()
-    }
-    set {
-      _elements = Set(newValue)
-    }
+    Array(_elements).sorted()
   }
   
   init() {
@@ -41,20 +36,20 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral {
     _elements = Set([element])
   }
   
-  init(arrayLiteral elements: InputOrigin.Element...) {
+  init(arrayLiteral elements: Element...) {
     self.init(elements: elements)
   }
-  
-  static func argumentIndex(_ index: SplitArguments.Index) -> InputOrigin {
-    return InputOrigin(elements: [.argumentIndex(index)])
+
+  init(argumentIndex: SplitArguments.Index) {
+    self.init(element: .argumentIndex(argumentIndex))
   }
   
-  mutating func insert(_ other: InputOrigin.Element) {
+  mutating func insert(_ other: Element) {
     guard !_elements.contains(other) else { return }
     _elements.insert(other)
   }
   
-  func inserting(_ other: InputOrigin.Element) -> InputOrigin {
+  func inserting(_ other: Element) -> Self {
     guard !_elements.contains(other) else { return self }
     var result = self
     result.insert(other)
@@ -64,17 +59,7 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral {
   mutating func formUnion(_ other: InputOrigin) {
     _elements.formUnion(other._elements)
   }
-  
-  func union(_ other: InputOrigin) -> InputOrigin {
-    var result = self
-    result._elements.formUnion(other._elements)
-    return result
-  }
-  
-  func isSubset(of other: Self) -> Bool {
-    return _elements.isSubset(of: other._elements)
-  }
-  
+
   func forEach(_ closure: (Element) -> Void) {
     _elements.forEach(closure)
   }
diff --git a/Sources/ArgumentParser/Parsing/ParserError.swift b/Sources/ArgumentParser/Parsing/ParserError.swift
index c9ffc6d..8cfece0 100644
--- a/Sources/ArgumentParser/Parsing/ParserError.swift
+++ b/Sources/ArgumentParser/Parsing/ParserError.swift
@@ -21,7 +21,7 @@ enum ParserError: Error {
   case missingValueForOption(InputOrigin, Name)
   case unexpectedValueForOption(InputOrigin.Element, Name, String)
   case unexpectedExtraValues([(InputOrigin, String)])
-  case duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin)
+  case duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin, originalInput: [String])
   /// We need a value for the given key, but it’s not there. Some non-optional option or argument is missing.
   case noValue(forKey: InputKey)
   case unableToParseValue(InputOrigin, Name?, String, forKey: InputKey)
diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift
index 7932986..639d9df 100644
--- a/Sources/ArgumentParser/Parsing/SplitArguments.swift
+++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift
@@ -393,7 +393,7 @@ extension SplitArguments {
           input = originalInput[index.inputIndex.rawValue]
         }
       }
-      return (.argumentIndex(index), input)
+      return (.init(argumentIndex: index), input)
     }
   }
 }
diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift
index bbee2a2..1bf1b54 100644
--- a/Sources/ArgumentParser/Usage/MessageInfo.swift
+++ b/Sources/ArgumentParser/Usage/MessageInfo.swift
@@ -14,7 +14,7 @@
 enum MessageInfo {
   case help(text: String)
   case validation(message: String, usage: String)
-  case other(message: String)
+  case other(message: String, exitCode: Int32)
   
   init(error: Error, type: ParsableArguments.Type) {
     var commandStack: [ParsableCommand.Type]
@@ -61,16 +61,18 @@ enum MessageInfo {
         case .message(let message):
           self = .help(text: message)
         }
+      case let error as ExitCode:
+        self = .other(message: "", exitCode: error.code)
       case let error as LocalizedError where error.errorDescription != nil:
-        self = .other(message: error.errorDescription!)
+        self = .other(message: error.errorDescription!, exitCode: EXIT_FAILURE)
       default:
-        self = .other(message: String(describing: error))
+        self = .other(message: String(describing: error), exitCode: EXIT_FAILURE)
       }
     } else if let parserError = parserError {
       let message = ArgumentSet(commandStack.last!).helpMessage(for: parserError)
       self = .validation(message: message, usage: usage)
     } else {
-      self = .other(message: String(describing: error))
+      self = .other(message: String(describing: error), exitCode: EXIT_FAILURE)
     }
   }
   
@@ -80,7 +82,7 @@ enum MessageInfo {
       return text
     case .validation(message: let message, usage: _):
       return message
-    case .other(message: let message):
+    case .other(let message, _):
       return message
     }
   }
@@ -90,9 +92,10 @@ enum MessageInfo {
     case .help(text: let text):
       return text
     case .validation(message: let message, usage: let usage):
-      return "Error: \(message)\n\(usage)"
-    case .other(message: let message):
-      return "Error: \(message)"
+      let errorMessage = message.isEmpty ? "" : "Error: \(message)\n"
+      return errorMessage + usage
+    case .other(let message, _):
+      return message.isEmpty ? "" : "Error: \(message)"
     }
   }
   
@@ -105,9 +108,9 @@ enum MessageInfo {
 
   var exitCode: Int32 {
     switch self {
-    case .help: return EXIT_SUCCESS
-    case .validation: return EX_USAGE
-    case .other: return EXIT_FAILURE
+    case .help: return ExitCode.success.code
+    case .validation: return ExitCode.validationFailure.code
+    case .other(_, let exitCode): return exitCode
     }
   }
 }
diff --git a/Sources/ArgumentParser/Usage/UsageGenerator.swift b/Sources/ArgumentParser/Usage/UsageGenerator.swift
index 4971c48..7d03e52 100644
--- a/Sources/ArgumentParser/Usage/UsageGenerator.swift
+++ b/Sources/ArgumentParser/Usage/UsageGenerator.swift
@@ -188,8 +188,8 @@ extension ErrorMessageGenerator {
       return unexpectedValueForOptionMessage(origin: o, name: n, value: v)
     case .unexpectedExtraValues(let v):
       return unexpectedExtraValuesMessage(values: v)
-    case .duplicateExclusiveValues(previous: let previous, duplicate: let duplicate):
-      return duplicateExclusiveValues(previous: previous, duplicate: duplicate)
+    case .duplicateExclusiveValues(previous: let previous, duplicate: let duplicate, originalInput: let arguments):
+      return duplicateExclusiveValues(previous: previous, duplicate: duplicate, arguments: arguments)
     case .noValue(forKey: let k):
       return noValueMessage(key: k)
     case .unableToParseValue(let o, let n, let v, forKey: let k):
@@ -301,8 +301,23 @@ extension ErrorMessageGenerator {
     }
   }
   
-  func duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin) -> String? {
-    return "Value at position \(duplicate) has already been set from value at position \(previous)."
+  func duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin, arguments: [String]) -> String? {
+    func elementString(_ origin: InputOrigin, _ arguments: [String]) -> String? {
+      guard case .argumentIndex(let split) = origin.elements.first else { return nil }
+      var argument = "\'\(arguments[split.inputIndex.rawValue])\'"
+      if case let .sub(offsetIndex) = split.subIndex {
+        let stringIndex = argument.index(argument.startIndex, offsetBy: offsetIndex+2)
+        argument = "\'\(argument[stringIndex])\' in \(argument)"
+      }
+      return "flag \(argument)"
+    }
+
+    // Note that the RHS of these coalescing operators cannot be reached at this time.
+    let dupeString = elementString(duplicate, arguments) ?? "position \(duplicate)"
+    let origString = elementString(previous, arguments) ?? "position \(previous)"
+
+    //TODO: review this message once environment values are supported.
+    return "Value to be set with \(dupeString) had already been set with \(origString)"
   }
   
   func noValueMessage(key: InputKey) -> String? {


Test patch:
diff --git a/Sources/TestHelpers/TestHelpers.swift b/Sources/TestHelpers/TestHelpers.swift
index e5ce6ff..6caa691 100644
--- a/Sources/TestHelpers/TestHelpers.swift
+++ b/Sources/TestHelpers/TestHelpers.swift
@@ -136,7 +136,7 @@ extension XCTest {
   public func AssertExecuteCommand(
     command: String,
     expected: String? = nil,
-    shouldError: Bool = false,
+    exitCode: Int32 = 0,
     file: StaticString = #file, line: UInt = #line)
   {
     let splitCommand = command.split(separator: " ")
@@ -171,10 +171,7 @@ extension XCTest {
     if let expected = expected {
       AssertEqualStringsIgnoringTrailingWhitespace(expected, errorActual + outputActual, file: file, line: line)
     }
-    if shouldError {
-      XCTAssertNotEqual(process.terminationStatus, 0, file: file, line: line)
-    } else {
-      XCTAssertEqual(process.terminationStatus, 0, file: file, line: line)
-    }
+
+    XCTAssertEqual(process.terminationStatus, exitCode, file: file, line: line)
   }
 }
diff --git a/Tests/EndToEndTests/ValidationEndToEndTests.swift b/Tests/EndToEndTests/ValidationEndToEndTests.swift
index 473fb9a..8854c73 100644
--- a/Tests/EndToEndTests/ValidationEndToEndTests.swift
+++ b/Tests/EndToEndTests/ValidationEndToEndTests.swift
@@ -28,6 +28,10 @@ fileprivate enum UserValidationError: LocalizedError {
 }
 
 fileprivate struct Foo: ParsableArguments {
+  static var usageString: String = """
+    Usage: foo [--count <count>] [<names> ...] [--version] [--throw]
+    """
+  
   @Option()
   var count: Int?
   
@@ -40,6 +44,15 @@ fileprivate struct Foo: ParsableArguments {
   @Flag(name: [.customLong("throw")])
   var throwCustomError: Bool
   
+  @Flag(help: .hidden)
+  var showUsageOnly: Bool
+  
+  @Flag(help: .hidden)
+  var failValidationSilently: Bool
+
+  @Flag(help: .hidden)
+  var failSilently: Bool
+
   mutating func validate() throws {
     if version {
       throw CleanExit.message("0.0.1")
@@ -56,6 +69,18 @@ fileprivate struct Foo: ParsableArguments {
     if throwCustomError {
       throw UserValidationError.userValidationError
     }
+    
+    if showUsageOnly {
+      throw ValidationError("")
+    }
+    
+    if failValidationSilently {
+      throw ExitCode.validationFailure
+    }
+    
+    if failSilently {
+      throw ExitCode.failure
+    }
   }
 }
 
@@ -81,7 +106,7 @@ extension ValidationEndToEndTests {
     AssertErrorMessage(Foo.self, [], "Must specify at least one name.")
     AssertFullErrorMessage(Foo.self, [], """
             Error: Must specify at least one name.
-            Usage: foo [--count <count>] [<names> ...] [--version] [--throw]
+            \(Foo.usageString)
             """)
     
     AssertErrorMessage(Foo.self, ["--count", "3", "Joe"], """
@@ -89,7 +114,7 @@ extension ValidationEndToEndTests {
             """)
     AssertFullErrorMessage(Foo.self, ["--count", "3", "Joe"], """
             Error: Number of names (1) doesn't match count (3).
-            Usage: foo [--count <count>] [<names> ...] [--version] [--throw]
+            \(Foo.usageString)
             """)
   }
   
@@ -97,4 +122,11 @@ extension ValidationEndToEndTests {
     // verify that error description is printed if avaiable via LocalizedError
     AssertErrorMessage(Foo.self, ["--throw", "Joe"], UserValidationError.userValidationError.errorDescription!)
   }
+  
+  func testEmptyErrorValidation() {
+    AssertErrorMessage(Foo.self, ["--show-usage-only", "Joe"], "")
+    AssertFullErrorMessage(Foo.self, ["--show-usage-only", "Joe"], Foo.usageString)
+    AssertFullErrorMessage(Foo.self, ["--fail-validation-silently", "Joe"], "")
+    AssertFullErrorMessage(Foo.self, ["--fail-silently", "Joe"], "")
+  }
 }
diff --git a/Tests/ExampleTests/MathExampleTests.swift b/Tests/ExampleTests/MathExampleTests.swift
index b090c27..8758de6 100644
--- a/Tests/ExampleTests/MathExampleTests.swift
+++ b/Tests/ExampleTests/MathExampleTests.swift
@@ -105,9 +105,28 @@ final class MathExampleTests: XCTestCase {
             Error: Please provide at least one value to calculate the mode.
             Usage: math stats average [--kind <kind>] [<values> ...]
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
   }
 
+  func testMath_ExitCodes() throws {
+    AssertExecuteCommand(
+      command: "math stats quantiles --test-success-exit-code",
+      expected: "",
+      exitCode: EXIT_SUCCESS)
+    AssertExecuteCommand(
+      command: "math stats quantiles --test-failure-exit-code",
+      expected: "",
+      exitCode: EXIT_FAILURE)
+    AssertExecuteCommand(
+      command: "math stats quantiles --test-validation-exit-code",
+      expected: "",
+      exitCode: EX_USAGE)
+    AssertExecuteCommand(
+      command: "math stats quantiles --test-custom-exit-code 42",
+      expected: "",
+      exitCode: 42)
+  }
+  
   func testMath_Fail() throws {
     AssertExecuteCommand(
       command: "math --foo",
@@ -115,7 +134,7 @@ final class MathExampleTests: XCTestCase {
             Error: Unknown option '--foo'
             Usage: math add [--hex-output] [<values> ...]
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
     
     AssertExecuteCommand(
       command: "math ZZZ",
@@ -123,6 +142,6 @@ final class MathExampleTests: XCTestCase {
             Error: The value 'ZZZ' is invalid for '<values>'
             Usage: math add [--hex-output] [<values> ...]
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
   }
 }
diff --git a/Tests/ExampleTests/RepeatExampleTests.swift b/Tests/ExampleTests/RepeatExampleTests.swift
index 6da5b2e..4bbf4eb 100644
--- a/Tests/ExampleTests/RepeatExampleTests.swift
+++ b/Tests/ExampleTests/RepeatExampleTests.swift
@@ -48,7 +48,7 @@ final class RepeatExampleTests: XCTestCase {
             Error: Missing expected argument '<phrase>'
             Usage: repeat [--count <count>] [--include-counter] <phrase>
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
     
     AssertExecuteCommand(
       command: "repeat hello --count",
@@ -56,7 +56,7 @@ final class RepeatExampleTests: XCTestCase {
             Error: Missing value for '--count <count>'
             Usage: repeat [--count <count>] [--include-counter] <phrase>
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
     
     AssertExecuteCommand(
       command: "repeat hello --count ZZZ",
@@ -64,6 +64,6 @@ final class RepeatExampleTests: XCTestCase {
             Error: The value 'ZZZ' is invalid for '--count <count>'
             Usage: repeat [--count <count>] [--include-counter] <phrase>
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
   }
 }
diff --git a/Tests/ExampleTests/RollDiceExampleTests.swift b/Tests/ExampleTests/RollDiceExampleTests.swift
index aa80d31..d9e3ce4 100644
--- a/Tests/ExampleTests/RollDiceExampleTests.swift
+++ b/Tests/ExampleTests/RollDiceExampleTests.swift
@@ -41,7 +41,7 @@ final class RollDiceExampleTests: XCTestCase {
             Error: Missing value for '--times <n>'
             Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
     
     AssertExecuteCommand(
       command: "roll --times ZZZ",
@@ -49,6 +49,6 @@ final class RollDiceExampleTests: XCTestCase {
             Error: The value 'ZZZ' is invalid for '--times <n>'
             Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
             """,
-      shouldError: true)
+      exitCode: EX_USAGE)
   }
 }
diff --git a/Tests/UnitTests/ErrorMessageTests.swift b/Tests/UnitTests/ErrorMessageTests.swift
index 371717b..4713abf 100644
--- a/Tests/UnitTests/ErrorMessageTests.swift
+++ b/Tests/UnitTests/ErrorMessageTests.swift
@@ -134,3 +134,19 @@ extension ErrorMessageTests {
     AssertErrorMessage(Qwz.self, ["-x"], "Unknown option '-x'")
   }
 }
+
+private enum OutputBehaviour: String, CaseIterable { case stats, count, list }
+private struct Options: ParsableArguments {
+  @Flag(name: .shortAndLong, help: "Program output")
+  var behaviour: OutputBehaviour
+
+  @Flag() var bool: Bool
+}
+
+extension ErrorMessageTests {
+  func testDuplicateFlags() {
+    AssertErrorMessage(Options.self, ["--list", "--bool", "-s"], "Value to be set with flag \'-s\' had already been set with flag \'--list\'")
+    AssertErrorMessage(Options.self, ["-cbl"], "Value to be set with flag \'l\' in \'-cbl\' had already been set with flag \'c\' in \'-cbl\'")
+    AssertErrorMessage(Options.self, ["-bc", "--stats", "-l"], "Value to be set with flag \'--stats\' had already been set with flag \'c\' in \'-bc\'")
+  }
+}