Repository: apple/swift-argument-parser
Pull request #53 (2020-03-03 10:22:10)

[Github] Adding templates for ISSUE & PRs and environment script
Partial fix of #46 + some extra goodies. This PR is a version of https://github.com/apple/swift-crypto/pull/21 updated for this repo, copy pasting my description from that PR in here (we developers are a lazy bunch right?)

Upgrading ISSUE_TEMPLATE from single markdown to 4 option selection: Bug, Regression, Question, Feature Request

### Motivation:

Having this template where when user presses `NEW ISSUE` button she has to think about what she's doing might lead to better issues submitted. 

### Modifications:

1. Adding a `.github` directory containing four different issues types.
2. Added new script `./Scripts/environment.sh` which puts to together a summary of users environment and copies to pasteboard (asking first). The scripts output looks like this:

```bash
Swift version: Apple Swift version 5.2 (swiftlang-1103.0.22 clang-1103.0.22)
Target: x86_64-apple-darwin19.3.0
Unix version: Darwin Sajjode.local 19.3.0 Darwin Kernel Version 19.3.0: Thu Jan  9 20:58:23 PST 2020
 root:xnu-6153.81.5~1/RELEASE_X86_64 x86_64

macOS version: 10.15.3
Xcode-select path: '/Applications/Xcode.app/Contents/Developer
Xcode: Xcode 11.4 (Build version 11N111s)

```


### Result:

#### New-Issue alternatives
![new_issue_options](https://user-images.githubusercontent.com/864410/73887452-dfb36d00-486b-11ea-8975-363bfdf85c77.png)

#### Bug report
With having pasted output generated from `./Scripts/environment.sh`
![bug_report](https://user-images.githubusercontent.com/864410/73887500-f8bc1e00-486b-11ea-8fa0-fe45593a9e4d.png)

#### Feature request

![feature_request](https://user-images.githubusercontent.com/864410/73887602-2903bc80-486c-11ea-9e33-5b241b458d08.png)



Patch:
diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
new file mode 100755
index 0000000..9d02100
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md
@@ -0,0 +1,33 @@
+---
+name: 🐛 Bug Report
+about: Something isn't working as expected
+---
+
+<!--
+    Thanks for contributing to the Swift Argument Parser!
+
+    Before you submit your issue, please replace each paragraph
+    below with the relevant details for your bug, and complete
+    the steps in the checklist by placing an 'x' in each box:
+    
+    - [x] I've completed this task
+    - [ ] This task isn't completed
+-->
+
+Replace this paragraph with a short description of the incorrect incorrect behavior. If this is a regression, please note the last version that the behavior was correct in addition to your current version.
+
+**ArgumentParser version:** `0.1.0` or the `master` branch, for example.
+**Swift version:** Paste the output of `swift --version` here.
+
+### Checklist
+- [ ] If possible, I've reproduced the issue using the `master` branch of this package
+- [ ] I've searched for [existing GitHub issues](https://github.com/apple/swift-argument-parser/issues)
+
+### Steps to Reproduce
+Replace this paragraph with an explanation of how to reproduce the incorrect behavior. This could include a code listing for a reduced version of your command, or a link to the code that is exhibiting the issue.
+
+### Expected behavior
+Describe what you expect to happen.
+
+### Actual behavior
+Describe or copy/paste the behavior you observe.
diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
new file mode 100755
index 0000000..dcd28fa
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
@@ -0,0 +1,13 @@
+---
+name: 💡 Feature Request
+about: A suggestion for a new feature
+---
+
+<!--
+    Thanks for contributing to the Swift Argument Parser!
+
+    Before you submit your issue, please replace the paragraph
+    below with information about your proposed feature.
+-->
+
+Replace this paragraph with a description of your proposed feature. Code samples that show what's missing, or what new capabilities will be possible, are very helpful! Provide links to existing issues or external references/discussions, if appropriate.
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..963525d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+  - name: ❓ Discussion Forum
+    url: https://forums.swift.org/c/related-projects/argumentparser/60
+    about: Questions about using Swift Argument Parser? Ask here!
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..fac09ad
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,21 @@
+<!--
+    Thanks for contributing to the Swift Argument Parser!
+
+    If this pull request adds new API, please add '?template=new.md'
+    to the URL to switch to the appropriate template.
+
+    Before you submit your request, please replace the paragraph
+    below with the relevant details, and complete the steps in the
+    checklist by placing an 'x' in each box:
+    
+    - [x] I've completed this task
+    - [ ] This task isn't completed
+-->
+
+Replace this paragraph with a description of your changes and rationale. Provide links to an existing issue or external references/discussions, if appropriate.
+
+### Checklist
+- [ ] I've added at least one test that validates that my change is working, if appropriate
+- [ ] I've followed the code style of the rest of the project
+- [ ] I've read the [Contribution Guidelines](CONTRIBUTING.md)
+- [ ] I've updated the documentation if necessary
diff --git a/.github/PULL_REQUEST_TEMPLATE/NEW.md b/.github/PULL_REQUEST_TEMPLATE/NEW.md
new file mode 100644
index 0000000..dfbd632
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/NEW.md
@@ -0,0 +1,36 @@
+<!--
+    Thanks for contributing to the Swift Argument Parser!
+
+    Before you submit your request, please replace each paragraph
+    below with the relevant details, and complete the steps in the
+    checklist by placing an 'x' in each box:
+    
+    - [x] I've completed this task
+    - [ ] This task isn't completed
+-->
+
+### Description
+Replace this paragraph with a description of your changes and rationale. Provide links to an existing issue or external references/discussions, if appropriate.
+
+### Detailed Design
+Include any additional information about the design here. At minimum, show any new API:
+
+```swift
+/// The new feature implemented by this pull request.
+struct NewFeature {}
+```
+
+### Documentation Plan
+How has the new feature been documented? Have the relevant portions of the guide been updated in addition to symbol-level documentation?
+
+### Test Plan
+How is the new feature tested?
+
+### Source Impact
+What is the impact of this change on existing users? Does it deprecate or remove any existing API?
+
+### Checklist
+- [ ] I've added at least one test that validates that my change is working, if appropriate
+- [ ] I've followed the code style of the rest of the project
+- [ ] I've read the [Contribution Guidelines](CONTRIBUTING.md)
+- [ ] I've updated the documentation if necessary
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/Scripts/environment.sh b/Scripts/environment.sh
new file mode 100755
index 0000000..ed244e5
--- /dev/null
+++ b/Scripts/environment.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the Swift Argument Parser open source project
+##
+## Copyright (c) 2020 Apple Inc. and the Swift project authors
+## Licensed under Apache License v2.0 with Runtime Library Exception
+##
+## See https://swift.org/LICENSE.txt for license information
+##
+##===----------------------------------------------------------------------===##
+
+read -p "This will replace your current pasteboard. Continue? [y/n]" -n 1 -r
+echo    # (optional) move to a new line
+if [[ $REPLY =~ ^[Yy]$ ]]
+then
+	swiftversion=$(swift --version)
+	unix_version_name=$(uname -a | tr ";" '\n')
+	i="${i}Swift version: ${swiftversion}\n"
+	i="${i}Unix version: ${unix_version_name}\n"
+
+	# Check if OS is macOS, if so retrieve which version and Xcode version.
+	if [[ "$OSTYPE" == "darwin"* ]]; then
+		macos=$(defaults read loginwindow SystemVersionStampAsString | cat -)
+		xcodebuild_version=$(/usr/bin/xcodebuild -version | grep Xcode)
+		xcodebuild_build=$(/usr/bin/xcodebuild -version | grep Build)
+		xcodeselectpath=$(xcode-select -p | cat -)
+	
+		i="${i}\nmacOS version: ${macos}\n"
+		i="${i}Xcode-select path: '${xcodeselectpath}\n"
+		i="${i}Xcode: ${xcodebuild_version} (${xcodebuild_build})"
+	fi
+
+	echo -e "${i}" | pbcopy
+	echo "Your pasteboard now contains debug info, paste it on Github"
+fi
\ No newline at end of file
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\'")
+  }
+}