Skip to content

Commit

Permalink
Allow default nil values for optional properties (#480)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
natecook1000 committed Aug 31, 2022
1 parent 9f39744 commit 0bac2cc
Show file tree
Hide file tree
Showing 12 changed files with 68 additions and 14 deletions.
4 changes: 2 additions & 2 deletions Examples/count-lines/CountLines.swift
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Examples/repeat/Repeat.swift
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Examples/roll/main.swift
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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) {
Expand Down
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/Argument.swift
Expand Up @@ -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.
Expand Down
18 changes: 18 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/Flag.swift
Expand Up @@ -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 {
Expand Down
20 changes: 19 additions & 1 deletion Sources/ArgumentParser/Parsable Properties/Option.swift
Expand Up @@ -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() {
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift
Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift
Expand Up @@ -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 {
Expand Down

0 comments on commit 0bac2cc

Please sign in to comment.