Repository: apple/swift-argument-parser
Pull request #480 (2022-08-30 17:03:25)

Allow default `nil` values for optional properties
This adds underscored initializers that let library users add `= nil` to declarations of optional `@Option` and `@Argument` properties. Previously, default values have been available for properties of non-optional types only.

These new initializers use `_OptionalNilComparisonType` as the wrapped value parameter, so only a `nil` literal is acceptable in the default value position. This avoids the problem of declaring an optional property with a non-`nil` default, which ends up negating the purpose of an optional.

### 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


natecook1000: @swift-ci Please test

Patch:
diff --git a/Examples/count-lines/CountLines.swift b/Examples/count-lines/CountLines.swift
index 9bda535..898f3fb 100644
--- a/Examples/count-lines/CountLines.swift
+++ b/Examples/count-lines/CountLines.swift
@@ -18,10 +18,10 @@ struct CountLines: AsyncParsableCommand {
     @Argument(
         help: "A file to count lines in. If omitted, counts the lines of stdin.",
         completion: .file(), transform: URL.init(fileURLWithPath:))
-    var inputFile: URL?
+    var inputFile: URL? = nil
     
     @Option(help: "Only count lines with this prefix.")
-    var prefix: String?
+    var prefix: String? = nil
     
     @Flag(help: "Include extra information in the output.")
     var verbose = false
diff --git a/Examples/repeat/Repeat.swift b/Examples/repeat/Repeat.swift
index bb3768f..06ea90c 100644
--- a/Examples/repeat/Repeat.swift
+++ b/Examples/repeat/Repeat.swift
@@ -14,7 +14,7 @@ import ArgumentParser
 @main
 struct Repeat: ParsableCommand {
     @Option(help: "The number of times to repeat 'phrase'.")
-    var count: Int?
+    var count: Int? = nil
 
     @Flag(help: "Include a counter with each repetition.")
     var includeCounter = false
diff --git a/Examples/roll/main.swift b/Examples/roll/main.swift
index 3ae5865..a12063b 100644
--- a/Examples/roll/main.swift
+++ b/Examples/roll/main.swift
@@ -22,7 +22,7 @@ struct RollOptions: ParsableArguments {
     var sides = 6
 
     @Option(help: "A seed to use for repeatable random generation.")
-    var seed: Int?
+    var seed: Int? = nil
 
     @Flag(name: .shortAndLong, help: "Show all roll results.")
     var verbose = false
diff --git a/README.md b/README.md
index 8a21540..46bc618 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ struct Repeat: ParsableCommand {
     var includeCounter = false
 
     @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
-    var count: Int?
+    var count: Int? = nil
 
     @Argument(help: "The phrase to repeat.")
     var phrase: String
diff --git a/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md b/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md
index 80fde02..a0d940b 100644
--- a/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md
+++ b/Sources/ArgumentParser/Documentation.docc/ArgumentParser.md
@@ -21,7 +21,7 @@ struct Repeat: ParsableCommand {
     var phrase: String
 
     @Option(help: "The number of times to repeat 'phrase'.")
-    var count: Int?
+    var count: Int? = nil
 
     mutating func run() throws {
         let repeatCount = count ?? 2
diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md
index c1549d2..fb847d8 100644
--- a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md
+++ b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md
@@ -22,7 +22,7 @@ struct Repeat: ParsableCommand {
     var phrase: String
 
     @Option(help: "How many times to repeat.")
-    var count: Int?
+    var count: Int? = nil
 
     mutating func run() throws {
         for _ in 0..<(count ?? 2) {
diff --git a/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md b/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md
index d720bc8..ca26c87 100644
--- a/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md
+++ b/Sources/ArgumentParser/Documentation.docc/Extensions/ParsableCommand.md
@@ -9,7 +9,7 @@ struct Repeat: ParsableCommand {
     var phrase: String
 
     @Option(help: "The number of times to repeat 'phrase'.")
-    var count: Int?
+    var count: Int? = nil
 
     mutating func run() throws {
         let repeatCount = count ?? 2
diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift
index 54e0e09..3daf38b 100644
--- a/Sources/ArgumentParser/Parsable Properties/Argument.swift
+++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift
@@ -234,6 +234,19 @@ extension Argument {
     })
   }
   
+  /// This initializer allows a user to provide a `nil` default value for an
+  /// optional `@Argument`-marked property without allowing a non-`nil` default
+  /// value.
+  public init<T: ExpressibleByArgument>(
+    wrappedValue _value: _OptionalNilComparisonType,
+    help: ArgumentHelp? = nil,
+    completion: CompletionKind? = nil
+  ) where Value == T? {
+    self.init(
+      help: help,
+      completion: completion)
+  }
+  
   /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic.
   ///
   /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift
index d4d1b58..8b0dc70 100644
--- a/Sources/ArgumentParser/Parsable Properties/Flag.swift
+++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift
@@ -219,6 +219,24 @@ extension Flag where Value == Optional<Bool> {
         help: help)
     })
   }
+  
+  /// This initializer allows a user to provide a `nil` default value for
+  /// `@Flag`-marked `Optional<Bool>` property without allowing a non-`nil`
+  /// default value.
+  public init(
+    wrappedValue _value: _OptionalNilComparisonType,
+    name: NameSpecification = .long,
+    inversion: FlagInversion,
+    exclusivity: FlagExclusivity = .chooseLast,
+    help: ArgumentHelp? = nil
+  ) {
+    self.init(
+      name: name,
+      inversion: inversion,
+      exclusivity: exclusivity,
+      help: help)
+  }
+
 }
 
 extension Flag where Value == Bool {
diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift
index 7e23e4b..05f464d 100644
--- a/Sources/ArgumentParser/Parsable Properties/Option.swift
+++ b/Sources/ArgumentParser/Parsable Properties/Option.swift
@@ -25,7 +25,7 @@
 ///     @main
 ///     struct Greet: ParsableCommand {
 ///         @Option var greeting = "Hello"
-///         @Option var age: Int?
+///         @Option var age: Int? = nil
 ///         @Option var name: String
 ///
 ///         mutating func run() {
@@ -361,6 +361,24 @@ extension Option {
     })
   }
 
+  /// This initializer allows a user to provide a `nil` default value for an
+  /// optional `@Option`-marked property without allowing a non-`nil` default
+  /// value.
+  public init<T: ExpressibleByArgument>(
+    wrappedValue _value: _OptionalNilComparisonType,
+    name: NameSpecification = .long,
+    parsing parsingStrategy: SingleValueParsingStrategy = .next,
+    help: ArgumentHelp? = nil,
+    completion: CompletionKind? = nil
+  ) where Value == T? {
+    self.init(
+      name: name,
+      parsing: parsingStrategy,
+      help: help,
+      completion: completion
+    )
+  }
+
   /// Creates a property with an optional default value, intended to be called by other constructors to centralize logic.
   ///
   /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.


Test patch:
diff --git a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift
index 2ff0983..dd4403d 100644
--- a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift
+++ b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift
@@ -96,6 +96,8 @@ fileprivate struct Foo: ParsableArguments {
   var sandbox: Bool = true
   @Flag(inversion: .prefixedEnableDisable)
   var requiredElement: Bool
+  @Flag(inversion: .prefixedEnableDisable)
+  var optional: Bool? = nil
 }
 
 extension FlagsEndToEndTests {
@@ -104,22 +106,25 @@ extension FlagsEndToEndTests {
       XCTAssertEqual(options.index, false)
       XCTAssertEqual(options.sandbox, true)
       XCTAssertEqual(options.requiredElement, true)
+      XCTAssertNil(options.optional)
     }
   }
 
   func testParsingEnableDisable_disableAll() throws {
-    AssertParse(Foo.self, ["--disable-index", "--disable-sandbox", "--disable-required-element"]) { options in
+    AssertParse(Foo.self, ["--disable-index", "--disable-sandbox", "--disable-required-element", "--disable-optional"]) { options in
       XCTAssertEqual(options.index, false)
       XCTAssertEqual(options.sandbox, false)
       XCTAssertEqual(options.requiredElement, false)
+      XCTAssertEqual(options.optional, false)
     }
   }
 
   func testParsingEnableDisable_enableAll() throws {
-    AssertParse(Foo.self, ["--enable-index", "--enable-sandbox", "--enable-required-element"]) { options in
+    AssertParse(Foo.self, ["--enable-index", "--enable-sandbox", "--enable-required-element", "--enable-optional"]) { options in
       XCTAssertEqual(options.index, true)
       XCTAssertEqual(options.sandbox, true)
       XCTAssertEqual(options.requiredElement, true)
+      XCTAssertEqual(options.optional, true)
     }
   }
 
diff --git a/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift
index 9148b3c..cbb3b83 100644
--- a/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift
+++ b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift
@@ -58,10 +58,10 @@ fileprivate struct Bar: ParsableArguments {
     case B
     case C
   }
-  @Option() var name: String?
-  @Option() var format: Format?
+  @Option() var name: String? = nil
+  @Option() var format: Format? = nil
   @Option() var foo: String
-  @Argument() var bar: String?
+  @Argument() var bar: String? = nil
 }
 
 extension OptionalEndToEndTests {