From 634863a9daed1457925979ccf669f863cff4a0f8 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 22 Jun 2020 23:17:29 -0500 Subject: [PATCH 1/7] Use type inference for flags / options --- Examples/math/main.swift | 8 ++++---- Examples/repeat/main.swift | 2 +- Examples/roll/main.swift | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Examples/math/main.swift b/Examples/math/main.swift index dd8632607..8ae592427 100644 --- a/Examples/math/main.swift +++ b/Examples/math/main.swift @@ -35,7 +35,7 @@ struct Math: ParsableCommand { struct Options: ParsableArguments { @Flag(name: [.customLong("hex-output"), .customShort("x")], help: "Use hexadecimal notation for the result.") - var hexadecimalOutput: Bool = false + var hexadecimalOutput = false @Argument( help: "A group of integers to operate on.") @@ -193,11 +193,11 @@ extension Math.Statistics { // These args and the validation method are for testing exit codes: @Flag(help: .hidden) - var testSuccessExitCode: Bool = false + var testSuccessExitCode = false @Flag(help: .hidden) - var testFailureExitCode: Bool = false + var testFailureExitCode = false @Flag(help: .hidden) - var testValidationExitCode: Bool = false + var testValidationExitCode = false @Option(help: .hidden) var testCustomExitCode: Int32? diff --git a/Examples/repeat/main.swift b/Examples/repeat/main.swift index a73ea4192..bc5e8b1cc 100644 --- a/Examples/repeat/main.swift +++ b/Examples/repeat/main.swift @@ -16,7 +16,7 @@ struct Repeat: ParsableCommand { var count: Int? @Flag(help: "Include a counter with each repetition.") - var includeCounter: Bool = false + var includeCounter = false @Argument(help: "The phrase to repeat.") var phrase: String diff --git a/Examples/roll/main.swift b/Examples/roll/main.swift index 45ca9e1d3..3ae5865bb 100644 --- a/Examples/roll/main.swift +++ b/Examples/roll/main.swift @@ -13,19 +13,19 @@ import ArgumentParser struct RollOptions: ParsableArguments { @Option(help: ArgumentHelp("Rolls the dice times.", valueName: "n")) - var times: Int = 1 + var times = 1 @Option(help: ArgumentHelp( "Rolls an -sided dice.", discussion: "Use this option to override the default value of a six-sided die.", valueName: "m")) - var sides: Int = 6 + var sides = 6 @Option(help: "A seed to use for repeatable random generation.") var seed: Int? @Flag(name: .shortAndLong, help: "Show all roll results.") - var verbose: Bool = false + var verbose = false } // If you prefer writing in a "script" style, you can call `parseOrExit()` to From c71153a6c1cb626c01e3d7a4d3c20848be945516 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 23 Jun 2020 08:18:07 -0500 Subject: [PATCH 2/7] Use default value syntax for arg/option arrays --- Examples/math/main.swift | 8 ++-- .../Parsable Properties/Argument.swift | 33 +++++++++++--- .../Parsable Properties/Option.swift | 35 +++++++++++---- .../ArgumentParser/Usage/HelpCommand.swift | 2 +- .../CustomParsingEndToEndTests.swift | 4 +- .../DefaultsEndToEndTests.swift | 16 +++---- .../PositionalEndToEndTests.swift | 8 ++-- .../RepeatingEndToEndTests.swift | 20 ++++----- .../SourceCompatEndToEndTests.swift | 44 +++++++++---------- .../SubcommandEndToEndTests.swift | 2 +- .../TransformEndToEndTests.swift | 4 +- .../ValidationEndToEndTests.swift | 2 +- .../PackageManager/Options.swift | 8 ++-- .../HelpGenerationTests.swift | 6 +-- .../ParsableArgumentsValidationTests.swift | 10 ++--- .../UsageGenerationTests.swift | 4 +- 16 files changed, 123 insertions(+), 83 deletions(-) diff --git a/Examples/math/main.swift b/Examples/math/main.swift index 8ae592427..73f8b400b 100644 --- a/Examples/math/main.swift +++ b/Examples/math/main.swift @@ -39,7 +39,7 @@ struct Options: ParsableArguments { @Argument( help: "A group of integers to operate on.") - var values: [Int] + var values: [Int] = [] } extension Math { @@ -103,7 +103,7 @@ extension Math.Statistics { var kind: Kind = .mean @Argument(help: "A group of floating-point values to operate on.") - var values: [Double] + var values: [Double] = [] func validate() throws { if (kind == .median || kind == .mode) && values.isEmpty { @@ -166,7 +166,7 @@ extension Math.Statistics { abstract: "Print the standard deviation of the values.") @Argument(help: "A group of floating-point values to operate on.") - var values: [Double] + var values: [Double] = [] mutating func run() { if values.isEmpty { @@ -189,7 +189,7 @@ extension Math.Statistics { abstract: "Print the quantiles of the values (TBD).") @Argument(help: "A group of floating-point values to operate on.") - var values: [Double] + var values: [Double] = [] // These args and the validation method are for testing exit codes: @Flag(help: .hidden) diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index 49710165b..5b127a935 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -350,7 +350,7 @@ extension Argument { /// from the command-line arguments. /// - help: Information about how to use this argument. public init( - default initial: Value = [], + wrappedValue: Value, parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil ) @@ -364,9 +364,9 @@ extension Argument { parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) + values.set(wrappedValue, forKey: key, inputOrigin: origin) }) - arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil + arg.help.defaultValue = !wrappedValue.isEmpty ? "\(wrappedValue)" : nil return ArgumentSet(alternatives: [arg]) }) } @@ -382,7 +382,7 @@ extension Argument { /// - transform: A closure that converts a string into this property's /// element type or throws an error. public init( - default initial: Value = [], + wrappedValue: Value, parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil, transform: @escaping (String) throws -> Element @@ -407,10 +407,31 @@ extension Argument { } }), initial: { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) + values.set(wrappedValue, forKey: key, inputOrigin: origin) }) - arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil + arg.help.defaultValue = !wrappedValue.isEmpty ? "\(wrappedValue)" : nil return ArgumentSet(alternatives: [arg]) }) } + + @available(*, deprecated, message: "Provide an empty array literal as a default value.") + public init( + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil + ) + where Element: ExpressibleByArgument, Value == Array + { + self.init(wrappedValue: [], parsing: parsingStrategy, help: help) + } + + @available(*, deprecated, message: "Provide an empty array literal as a default value.") + public init( + parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, + help: ArgumentHelp? = nil, + transform: @escaping (String) throws -> Element + ) + where Value == Array + { + self.init(wrappedValue: [], parsing: parsingStrategy, help: help, transform: transform) + } } diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 2ddf19a6d..30a497eac 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -487,8 +487,8 @@ extension Option { /// from the command-line arguments. /// - help: Information about how to use this option. public init( + wrappedValue: [Element], name: NameSpecification = .long, - default initial: Array = [], parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil ) where Element: ExpressibleByArgument, Value == Array { @@ -496,9 +496,9 @@ extension Option { let kind = ArgumentDefinition.Kind.name(key: key, specification: name) let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) var arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) + values.set(wrappedValue, forKey: key, inputOrigin: origin) }) - arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil + arg.help.defaultValue = !wrappedValue.isEmpty ? "\(wrappedValue)" : nil return ArgumentSet(alternatives: [arg]) }) } @@ -519,8 +519,8 @@ extension Option { /// - transform: A closure that converts a string into this property's /// element type or throws an error. public init( + wrappedValue: [Element], name: NameSpecification = .long, - default initial: Array = [], parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil, transform: @escaping (String) throws -> Element @@ -536,13 +536,32 @@ extension Option { $0.append(transformedElement) }) } catch { - throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) + throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) } }), initial: { origin, values in - values.set(initial, forKey: key, inputOrigin: origin) + values.set(wrappedValue, forKey: key, inputOrigin: origin) }) - arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil + arg.help.defaultValue = !wrappedValue.isEmpty ? "\(wrappedValue)" : nil return ArgumentSet(alternatives: [arg]) - }) + }) + } + + @available(*, deprecated, message: "Provide an empty array literal as a default value.") + public init( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil + ) where Element: ExpressibleByArgument, Value == Array { + self.init(wrappedValue: [], name: name, parsing: parsingStrategy, help: help) + } + + @available(*, deprecated, message: "Provide an empty array literal as a default value.") + public init( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + transform: @escaping (String) throws -> Element + ) where Value == Array { + self.init(wrappedValue: [], name: name, parsing: parsingStrategy, help: help, transform: transform) } } diff --git a/Sources/ArgumentParser/Usage/HelpCommand.swift b/Sources/ArgumentParser/Usage/HelpCommand.swift index 647d242e7..31e269b9e 100644 --- a/Sources/ArgumentParser/Usage/HelpCommand.swift +++ b/Sources/ArgumentParser/Usage/HelpCommand.swift @@ -12,7 +12,7 @@ struct HelpCommand: ParsableCommand { static var configuration = CommandConfiguration(commandName: "help") - @Argument() var subcommands: [String] + @Argument() var subcommands: [String] = [] private(set) var commandStack: [ParsableCommand.Type] = [] diff --git a/Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift index 3068e566a..57d95f46f 100644 --- a/Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/CustomParsingEndToEndTests.swift @@ -126,10 +126,10 @@ extension ParsingEndToEndTests { fileprivate struct Qux: ParsableCommand { @Option(transform: { try Name(rawValue: $0) }) - var firstName: [Name] + var firstName: [Name] = [] @Argument(transform: { try Name(rawValue: $0) }) - var lastName: [Name] + var lastName: [Name] = [] } extension ParsingEndToEndTests { diff --git a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift index 07efc3ed5..b5cc5a09c 100644 --- a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift @@ -509,11 +509,11 @@ extension DefaultsEndToEndTests { } fileprivate struct Quux: ParsableArguments { - @Option(default: ["A", "B"], parsing: .upToNextOption) - var letters: [String] + @Option(parsing: .upToNextOption) + var letters: [String] = ["A", "B"] - @Argument(default: [1, 2]) - var numbers: [Int] + @Argument() + var numbers: [Int] = [1, 2] } extension DefaultsEndToEndTests { @@ -619,13 +619,13 @@ fileprivate struct Main: ParsableCommand { ) struct Options: ParsableArguments { - @Option(default: ["A", "B"], parsing: .upToNextOption) - var letters: [String] + @Option(parsing: .upToNextOption) + var letters: [String] = ["A", "B"] } struct Sub: ParsableCommand { - @Argument(default: [1, 2]) - var numbers: [Int] + @Argument() + var numbers: [Int] = [1, 2] @OptionGroup() var options: Main.Options diff --git a/Tests/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift index a3eb8723c..c1e3f1b5d 100644 --- a/Tests/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/PositionalEndToEndTests.swift @@ -95,7 +95,7 @@ extension PositionalEndToEndTests { // MARK: Multiple values fileprivate struct Qux: ParsableArguments { - @Argument() var names: [String] + @Argument() var names: [String] = [] } extension PositionalEndToEndTests { @@ -133,7 +133,7 @@ extension PositionalEndToEndTests { fileprivate struct Wobble: ParsableArguments { @Argument() var count: Int - @Argument() var names: [String] + @Argument() var names: [String] = [] } extension PositionalEndToEndTests { @@ -180,7 +180,7 @@ extension PositionalEndToEndTests { // MARK: Multiple parsed values fileprivate struct Flob: ParsableArguments { - @Argument() var counts: [Int] + @Argument() var counts: [Int] = [] } extension PositionalEndToEndTests { @@ -215,7 +215,7 @@ extension PositionalEndToEndTests { // MARK: Multiple parsed values fileprivate struct BadlyFormed: ParsableArguments { - @Argument() var numbers: [Int] + @Argument() var numbers: [Int] = [] @Argument() var name: String } diff --git a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift index 37f3f3823..38d85de01 100644 --- a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift @@ -19,7 +19,7 @@ final class RepeatingEndToEndTests: XCTestCase { // MARK: - fileprivate struct Bar: ParsableArguments { - @Option() var name: [String] + @Option() var name: [String] = [] } extension RepeatingEndToEndTests { @@ -66,7 +66,7 @@ extension RepeatingEndToEndTests { fileprivate struct Baz: ParsableArguments { @Flag var verbose: Bool = false - @Option(parsing: .remaining) var names: [String] + @Option(parsing: .remaining) var names: [String] = [] } extension RepeatingEndToEndTests { @@ -138,7 +138,7 @@ fileprivate struct Inner: ParsableCommand { var verbose: Bool = false @Argument(parsing: .unconditionalRemaining) - var files: [String] + var files: [String] = [] } extension RepeatingEndToEndTests { @@ -156,7 +156,7 @@ extension RepeatingEndToEndTests { // MARK: - fileprivate struct Qux: ParsableArguments { - @Option(parsing: .upToNextOption) var names: [String] + @Option(parsing: .upToNextOption) var names: [String] = [] @Flag var verbose: Bool = false @Argument() var extra: String? } @@ -234,9 +234,9 @@ fileprivate struct Wobble: ParsableArguments { self.value = value } } - @Option(transform: Name.init) var names: [Name] - @Option(parsing: .upToNextOption, transform: Name.init) var moreNames: [Name] - @Option(parsing: .remaining, transform: Name.init) var evenMoreNames: [Name] + @Option(transform: Name.init) var names: [Name] = [] + @Option(parsing: .upToNextOption, transform: Name.init) var moreNames: [Name] = [] + @Option(parsing: .remaining, transform: Name.init) var evenMoreNames: [Name] = [] } extension RepeatingEndToEndTests { @@ -302,7 +302,7 @@ extension RepeatingEndToEndTests { fileprivate struct Weazle: ParsableArguments { @Flag var verbose: Bool = false - @Argument() var names: [String] + @Argument() var names: [String] = [] } extension RepeatingEndToEndTests { @@ -330,7 +330,7 @@ fileprivate struct Foozle: ParsableArguments { @Flag var verbose: Bool = false @Flag(name: .customShort("f")) var useFiles: Bool = false @Flag(name: .customShort("i")) var useStandardInput: Bool = false - @Argument(parsing: .unconditionalRemaining) var names: [String] + @Argument(parsing: .unconditionalRemaining) var names: [String] = [] } extension RepeatingEndToEndTests { @@ -399,7 +399,7 @@ extension RepeatingEndToEndTests { // MARK: - struct PerformanceTest: ParsableCommand { - @Option(name: .short) var bundleIdentifiers: [String] + @Option(name: .short) var bundleIdentifiers: [String] = [] mutating func run() throws { print(bundleIdentifiers) } } diff --git a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift index 03f502d04..1c772d6be 100644 --- a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift @@ -50,22 +50,22 @@ fileprivate struct AlmostAllArguments: ParsableArguments { @Argument(help: "", transform: { _ in 0 }) var d4: Int? @Argument(default: 0, transform: { _ in 0 }) var d5: Int? - @Argument(default: [1, 2], parsing: .remaining, help: "") var e: [Int] + @Argument(parsing: .remaining, help: "") var e: [Int] = [1, 2] @Argument(parsing: .remaining, help: "") var e1: [Int] - @Argument(default: [1, 2], parsing: .remaining) var e2: [Int] - @Argument(default: [1, 2], help: "") var e3: [Int] + @Argument(parsing: .remaining) var e2: [Int] = [1, 2] + @Argument(help: "") var e3: [Int] = [1, 2] @Argument() var e4: [Int] @Argument(help: "") var e5: [Int] @Argument(parsing: .remaining) var e6: [Int] - @Argument(default: [1, 2]) var e7: [Int] - @Argument(default: [1, 2], parsing: .remaining, help: "", transform: { _ in 0 }) var e8: [Int] + @Argument() var e7: [Int] = [1, 2] + @Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e8: [Int] = [1, 2] @Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e9: [Int] - @Argument(default: [1, 2], parsing: .remaining, transform: { _ in 0 }) var e10: [Int] - @Argument(default: [1, 2], help: "", transform: { _ in 0 }) var e11: [Int] + @Argument(parsing: .remaining, transform: { _ in 0 }) var e10: [Int] = [1, 2] + @Argument(help: "", transform: { _ in 0 }) var e11: [Int] = [1, 2] @Argument(transform: { _ in 0 }) var e12: [Int] @Argument(help: "", transform: { _ in 0 }) var e13: [Int] @Argument(parsing: .remaining, transform: { _ in 0 }) var e14: [Int] - @Argument(default: [1, 2], transform: { _ in 0 }) var e15: [Int] + @Argument(transform: { _ in 0 }) var e15: [Int] = [1, 2] } fileprivate struct AllOptions: ParsableArguments { @@ -143,33 +143,33 @@ fileprivate struct AllOptions: ParsableArguments { @Option(parsing: .next, transform: { _ in 0 }) var d12: Int? @Option(help: "", transform: { _ in 0 }) var d13: Int? - @Option(name: .long, default: [1, 2], parsing: .singleValue, help: "") var e: [Int] - @Option(default: [1, 2], parsing: .singleValue, help: "") var e1: [Int] + @Option(name: .long, parsing: .singleValue, help: "") var e: [Int] = [1, 2] + @Option(parsing: .singleValue, help: "") var e1: [Int] = [1, 2] @Option(name: .long, parsing: .singleValue, help: "") var e2: [Int] - @Option(name: .long, default: [1, 2], help: "") var e3: [Int] + @Option(name: .long, help: "") var e3: [Int] = [1, 2] @Option(parsing: .singleValue, help: "") var e4: [Int] - @Option(default: [1, 2], help: "") var e5: [Int] - @Option(default: [1, 2], parsing: .singleValue) var e6: [Int] + @Option(help: "") var e5: [Int] = [1, 2] + @Option(parsing: .singleValue) var e6: [Int] = [1, 2] @Option(name: .long, help: "") var e7: [Int] @Option(name: .long, parsing: .singleValue) var e8: [Int] - @Option(name: .long, default: [1, 2]) var e9: [Int] + @Option(name: .long) var e9: [Int] = [1, 2] @Option(name: .long) var e10: [Int] - @Option(default: [1, 2]) var e11: [Int] + @Option() var e11: [Int] = [1, 2] @Option(parsing: .singleValue) var e12: [Int] @Option(help: "") var e13: [Int] - @Option(name: .long, default: [1, 2], parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int] - @Option(default: [1, 2], parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int] + @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int] = [1, 2] + @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int] = [1, 2] @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f2: [Int] - @Option(name: .long, default: [1, 2], help: "", transform: { _ in 0 }) var f3: [Int] + @Option(name: .long, help: "", transform: { _ in 0 }) var f3: [Int] = [1, 2] @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f4: [Int] - @Option(default: [1, 2], help: "", transform: { _ in 0 }) var f5: [Int] - @Option(default: [1, 2], parsing: .singleValue, transform: { _ in 0 }) var f6: [Int] + @Option(help: "", transform: { _ in 0 }) var f5: [Int] = [1, 2] + @Option(parsing: .singleValue, transform: { _ in 0 }) var f6: [Int] = [1, 2] @Option(name: .long, help: "", transform: { _ in 0 }) var f7: [Int] @Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f8: [Int] - @Option(name: .long, default: [1, 2], transform: { _ in 0 }) var f9: [Int] + @Option(name: .long, transform: { _ in 0 }) var f9: [Int] = [1, 2] @Option(name: .long, transform: { _ in 0 }) var f10: [Int] - @Option(default: [1, 2], transform: { _ in 0 }) var f11: [Int] + @Option(transform: { _ in 0 }) var f11: [Int] = [1, 2] @Option(parsing: .singleValue, transform: { _ in 0 }) var f12: [Int] @Option(help: "", transform: { _ in 0 }) var f13: [Int] } diff --git a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift index 47316ee98..ed89a44d3 100644 --- a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift @@ -125,7 +125,7 @@ fileprivate struct Math: ParsableCommand { var verbose: Bool = false @Argument(help: "The first operand") - var operands: [Int] + var operands: [Int] = [] mutating func run() { XCTAssertEqual(operation, .multiply) diff --git a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift index 82c47fbfa..f6edbd677 100644 --- a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift @@ -55,7 +55,7 @@ fileprivate struct BarOption: Convert, ParsableCommand { @Option(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"), transform: { try convert($0) }) - var strings: [Int] + var strings: [Int] = [] } extension TransformEndToEndTests { @@ -120,7 +120,7 @@ fileprivate struct BarArgument: Convert, ParsableCommand { @Argument(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"), transform: { try convert($0) }) - var strings: [Int] + var strings: [Int] = [] } extension TransformEndToEndTests { diff --git a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift index 0aae878fc..8228c490d 100644 --- a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift @@ -50,7 +50,7 @@ fileprivate struct Foo: ParsableArguments { var count: Int? @Argument() - var names: [String] + var names: [String] = [] @Flag var version: Bool = false diff --git a/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift index f7d458aa1..ccdbc5a9b 100644 --- a/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift +++ b/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift @@ -66,25 +66,25 @@ struct Options: ParsableArguments { parsing: .unconditionalSingleValue, help: ArgumentHelp("Pass flag through to all C compiler invocations", valueName: "c-compiler-flag")) - var cCompilerFlags: [String] + var cCompilerFlags: [String] = [] @Option(name: .customLong("Xcxx", withSingleDash: true), parsing: .unconditionalSingleValue, help: ArgumentHelp("Pass flag through to all C++ compiler invocations", valueName: "cxx-compiler-flag")) - var cxxCompilerFlags: [String] + var cxxCompilerFlags: [String] = [] @Option(name: .customLong("Xlinker", withSingleDash: true), parsing: .unconditionalSingleValue, help: ArgumentHelp("Pass flag through to all linker invocations", valueName: "linker-flag")) - var linkerFlags: [String] + var linkerFlags: [String] = [] @Option(name: .customLong("Xswiftc", withSingleDash: true), parsing: .unconditionalSingleValue, help: ArgumentHelp("Pass flag through to all Swift compiler invocations", valueName: "swift-compiler-flag")) - var swiftCompilerFlags: [String] + var swiftCompilerFlags: [String] = [] } struct Package: ParsableCommand { diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index 499620ae6..49585dab0 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -148,8 +148,8 @@ extension HelpGenerationTests { @Option(help: "Whether logging is enabled.") var logging: Bool = false - @Option(default: [7, 14], parsing: .upToNextOption, help: ArgumentHelp("Your lucky numbers.", valueName: "numbers")) - var lucky: [Int] + @Option(parsing: .upToNextOption, help: ArgumentHelp("Your lucky numbers.", valueName: "numbers")) + var lucky: [Int] = [7, 14] @Flag(help: "Vegan diet.") var nda: OptionFlags = .optional @@ -341,7 +341,7 @@ extension HelpGenerationTests { struct K: ParsableCommand { @Argument(help: "A list of paths.") - var paths: [String] + var paths: [String] = [] func validate() throws { if paths.isEmpty { diff --git a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift index 4e6bcbaeb..1dc8fe5a1 100644 --- a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -115,12 +115,12 @@ final class ParsableArgumentsValidationTests: XCTestCase { var phrase: String @Argument() - var items: [Int] + var items: [Int] = [] } private struct G: ParsableArguments { @Argument() - var items: [Int] + var items: [Int] = [] @Argument() var phrase: String @@ -128,7 +128,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { private struct H: ParsableArguments { @Argument() - var items: [Int] + var items: [Int] = [] @Option() var option: Bool @@ -145,7 +145,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { private struct J: ParsableArguments { struct Options: ParsableArguments { @Argument() - var numberOfItems: [Int] + var numberOfItems: [Int] = [] } @OptionGroup() @@ -158,7 +158,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { private struct K: ParsableArguments { struct Options: ParsableArguments { @Argument() - var items: [Int] + var items: [Int] = [] } @Argument() diff --git a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift index 0586137b9..c52f2fda2 100644 --- a/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/UsageGenerationTests.swift @@ -83,8 +83,8 @@ extension UsageGenerationTests { } struct F: ParsableArguments { - @Option() var name: [String] - @Argument() var nameCounts: [Int] + @Option() var name: [String] = [] + @Argument() var nameCounts: [Int] = [] } func testSynopsisWithRepeats() { From fe41d8f1f812654d4f330f32bcc84dc3b1944613 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 23 Jun 2020 08:22:56 -0500 Subject: [PATCH 3/7] Allow a default for flag arrays --- .../ArgumentParser/Parsable Properties/Flag.swift | 12 ++++++++++-- .../FlagsEndToEndTests.swift | 10 +++++----- .../SourceCompatEndToEndTests.swift | 6 ++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index 6de8225c2..95f06b1fa 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -361,7 +361,7 @@ extension Flag where Value == Int { ) { self.init(_parsedValue: .init { key in .counter(key: key, name: name, help: help) - }) + }) } } @@ -536,6 +536,7 @@ extension Flag { /// - name: A specification for what names are allowed for this flag. /// - help: Information about how to use this flag. public init( + wrappedValue: [Element], help: ArgumentHelp? = nil ) where Value == Array, Element: EnumerableFlag { self.init(_parsedValue: .init { key in @@ -547,7 +548,7 @@ extension Flag { let name = Element.name(for: value) let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp) - return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: [Element](), update: .nullary({ (origin, name, values) in + return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: wrappedValue, update: .nullary({ (origin, name, values) in values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { $0.append(value) }) @@ -556,6 +557,13 @@ extension Flag { return ArgumentSet(additive: args) }) } + + @available(*, deprecated, message: "Provide an empty array literal as a default value.") + public init( + help: ArgumentHelp? = nil + ) where Value == Array, Element: EnumerableFlag { + self.init(wrappedValue: [], help: help) + } } // - MARK: Unavailable CaseIterable/RawValue == String diff --git a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift index b3d9d9130..2ff0983c6 100644 --- a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift @@ -267,21 +267,21 @@ extension FlagsEndToEndTests { fileprivate struct Qux: ParsableArguments { @Flag() - var color: [Color] + var color: [Color] = [] @Flag() - var size: [Size] + var size: [Size] = [.small, .medium] } extension FlagsEndToEndTests { func testParsingCaseIterableArray_Values() throws { AssertParse(Qux.self, []) { options in XCTAssertEqual(options.color, []) - XCTAssertEqual(options.size, []) + XCTAssertEqual(options.size, [.small, .medium]) } AssertParse(Qux.self, ["--pink"]) { options in XCTAssertEqual(options.color, [.pink]) - XCTAssertEqual(options.size, []) + XCTAssertEqual(options.size, [.small, .medium]) } AssertParse(Qux.self, ["--pink", "--purple", "--small"]) { options in XCTAssertEqual(options.color, [.pink, .purple]) @@ -293,7 +293,7 @@ extension FlagsEndToEndTests { } AssertParse(Qux.self, ["--pink", "--pink", "--purple", "--pink"]) { options in XCTAssertEqual(options.color, [.pink, .pink, .purple, .pink]) - XCTAssertEqual(options.size, []) + XCTAssertEqual(options.size, [.small, .medium]) } } diff --git a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift index 1c772d6be..c315e3a03 100644 --- a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift @@ -254,8 +254,10 @@ struct AllFlags: ParsableArguments { @Flag(help: "") var g2: E? @Flag(exclusivity: .chooseLast) var g3: E? - @Flag(help: "") var h: [E] - @Flag() var h1: [E] + @Flag(help: "") var h: [E] = [] + @Flag() var h1: [E] = [] + @Flag(help: "") var h2: [E] + @Flag() var h3: [E] } extension SourceCompatEndToEndTests { From cb4ef088747cdc2b02296a635692e04b1631b589 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 23 Jun 2020 08:31:07 -0500 Subject: [PATCH 4/7] Fix some whitespace --- Sources/ArgumentParser/Parsable Properties/Flag.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index 95f06b1fa..f0ad4faf0 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -465,7 +465,7 @@ extension Flag where Value: EnumerableFlag { ) } -/// Creates a property with no default value that gets its value from the presence of a flag. + /// Creates a property with no default value that gets its value from the presence of a flag. /// /// Use this initializer to customize the name and number of states further than using a `Bool`. /// To use, define an `EnumerableFlag` enumeration with a case for each state, and use that as the type for your flag. @@ -483,7 +483,7 @@ extension Flag where Value: EnumerableFlag { /// - Parameters: /// - exclusivity: The behavior to use when multiple flags are specified. /// - help: Information about how to use this flag. - public init( + public init( exclusivity: FlagExclusivity = .exclusive, help: ArgumentHelp? = nil ) { From d5a59270b6067fdb627896cb51d5a609279e7f0f Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 23 Jun 2020 09:48:25 -0500 Subject: [PATCH 5/7] Allow arguments validations to warn instead of fail --- .../ParsableArgumentsValidation.swift | 71 +++++--- .../ParsableArgumentsValidationTests.swift | 167 +++++++++--------- 2 files changed, 124 insertions(+), 114 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift index dfade04f3..a8ebde081 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift @@ -10,7 +10,16 @@ //===----------------------------------------------------------------------===// fileprivate protocol ParsableArgumentsValidator { - static func validate(_ type: ParsableArguments.Type) throws + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? +} + +enum ValidatorErrorKind { + case warning + case failure +} + +protocol ParsableArgumentsValidatorError: Error { + var kind: ValidatorErrorKind { get } } struct ParsableArgumentsValidationError: Error, CustomStringConvertible { @@ -32,13 +41,8 @@ extension ParsableArguments { ParsableArgumentsCodingKeyValidator.self, ParsableArgumentsUniqueNamesValidator.self, ] - let errors: [Error] = validators.compactMap { validator in - do { - try validator.validate(self) - return nil - } catch { - return error - } + let errors = validators.compactMap { validator in + validator.validate(self) } if errors.count > 0 { throw ParsableArgumentsValidationError(parsableArgumentsType: self, underlayingErrors: errors) @@ -72,15 +76,19 @@ fileprivate extension ArgumentSet { /// parsing the arguments. struct PositionalArgumentsValidator: ParsableArgumentsValidator { - struct Error: Swift.Error, CustomStringConvertible { + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { let repeatedPositionalArgument: String + let positionalArgumentFollowingRepeated: String + var description: String { "Can't have a positional argument `\(positionalArgumentFollowingRepeated)` following an array of positional arguments `\(repeatedPositionalArgument)`." } + + var kind: ValidatorErrorKind { .failure } } - static func validate(_ type: ParsableArguments.Type) throws { + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { let sets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in @@ -97,17 +105,17 @@ struct PositionalArgumentsValidator: ParsableArgumentsValidator { } guard let repeatedPositional = sets.firstIndex(where: { $0.firstRepeatedPositionalArgument != nil }) - else { return } - let positionalFollowingRepeated = sets[repeatedPositional...] + else { return nil } + guard let positionalFollowingRepeated = sets[repeatedPositional...] .dropFirst() .first(where: { $0.firstPositionalArgument != nil }) + else { return nil } - if let positionalFollowingRepeated = positionalFollowingRepeated { - let firstRepeatedPositionalArgument: ArgumentDefinition = sets[repeatedPositional].firstRepeatedPositionalArgument! - let positionalFollowingRepeatedArgument: ArgumentDefinition = positionalFollowingRepeated.firstPositionalArgument! - throw Error(repeatedPositionalArgument: firstRepeatedPositionalArgument.help.keys.first!.rawValue, - positionalArgumentFollowingRepeated: positionalFollowingRepeatedArgument.help.keys.first!.rawValue) - } + let firstRepeatedPositionalArgument: ArgumentDefinition = sets[repeatedPositional].firstRepeatedPositionalArgument! + let positionalFollowingRepeatedArgument: ArgumentDefinition = positionalFollowingRepeated.firstPositionalArgument! + return Error( + repeatedPositionalArgument: firstRepeatedPositionalArgument.help.keys.first!.rawValue, + positionalArgumentFollowingRepeated: positionalFollowingRepeatedArgument.help.keys.first!.rawValue) } } @@ -145,8 +153,9 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { /// This error indicates that an option, a flag, or an argument of /// a `ParsableArguments` is defined without a corresponding `CodingKey`. - struct Error: Swift.Error, CustomStringConvertible { + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { let missingCodingKeys: [String] + var description: String { if missingCodingKeys.count > 1 { return "Arguments \(missingCodingKeys.map({ "`\($0)`" }).joined(separator: ",")) are defined without corresponding `CodingKey`s." @@ -154,9 +163,13 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { return "Argument `\(missingCodingKeys[0])` is defined without a corresponding `CodingKey`." } } + + var kind: ValidatorErrorKind { + .failure + } } - static func validate(_ type: ParsableArguments.Type) throws { + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { let argumentKeys: [String] = Mirror(reflecting: type.init()) .children .compactMap { child in @@ -169,7 +182,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { return String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) } guard argumentKeys.count > 0 else { - return + return nil } do { let _ = try type.init(from: Validator(argumentKeys: argumentKeys)) @@ -177,9 +190,9 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { } catch let result as Validator.ValidationResult { switch result { case .missingCodingKeys(let keys): - throw Error(missingCodingKeys: keys) + return Error(missingCodingKeys: keys) case .success: - break + return nil } } catch { fatalError("Unexpected validation error: \(error)") @@ -189,7 +202,7 @@ struct ParsableArgumentsCodingKeyValidator: ParsableArgumentsValidator { /// Ensure argument names are unique within a `ParsableArguments` or `ParsableCommand`. struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { - struct Error: Swift.Error, CustomStringConvertible { + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { var duplicateNames: [String: Int] = [:] var description: String { @@ -197,9 +210,11 @@ struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { "Multiple (\(entry.value)) `Option` or `Flag` arguments are named \"\(entry.key)\"." }.joined(separator: "\n") } + + var kind: ValidatorErrorKind { .failure } } - static func validate(_ type: ParsableArguments.Type) throws { + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { let argSets: [ArgumentSet] = Mirror(reflecting: type.init()) .children .compactMap { child in @@ -227,8 +242,8 @@ struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { } let duplicateNames = countedNames.filter { $0.value > 1 } - if !duplicateNames.isEmpty { - throw Error(duplicateNames: duplicateNames) - } + return duplicateNames.isEmpty + ? nil + : Error(duplicateNames: duplicateNames) } } diff --git a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift index 1dc8fe5a1..6b5a1c8b8 100644 --- a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -81,32 +81,31 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testCodingKeyValidation() throws { - try ParsableArgumentsCodingKeyValidator.validate(A.self) - - try ParsableArgumentsCodingKeyValidator.validate(B.self) - - XCTAssertThrowsError(try ParsableArgumentsCodingKeyValidator.validate(C.self)) { (error) in - if let error = error as? ParsableArgumentsCodingKeyValidator.Error { - XCTAssert(error.missingCodingKeys == ["count"]) - } else { - XCTFail() - } + XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(A.self)) + XCTAssertNil(ParsableArgumentsCodingKeyValidator.validate(B.self)) + + if let error = ParsableArgumentsCodingKeyValidator.validate(C.self) + as? ParsableArgumentsCodingKeyValidator.Error + { + XCTAssert(error.missingCodingKeys == ["count"]) + } else { + XCTFail() } - - XCTAssertThrowsError(try ParsableArgumentsCodingKeyValidator.validate(D.self)) { (error) in - if let error = error as? ParsableArgumentsCodingKeyValidator.Error { - XCTAssert(error.missingCodingKeys == ["phrase"]) - } else { - XCTFail() - } + + if let error = ParsableArgumentsCodingKeyValidator.validate(D.self) + as? ParsableArgumentsCodingKeyValidator.Error + { + XCTAssert(error.missingCodingKeys == ["phrase"]) + } else { + XCTFail() } - XCTAssertThrowsError(try ParsableArgumentsCodingKeyValidator.validate(E.self)) { (error) in - if let error = error as? ParsableArgumentsCodingKeyValidator.Error { - XCTAssert(error.missingCodingKeys == ["phrase", "includeCounter"]) - } else { - XCTFail() - } + if let error = ParsableArgumentsCodingKeyValidator.validate(E.self) + as? ParsableArgumentsCodingKeyValidator.Error + { + XCTAssert(error.missingCodingKeys == ["phrase", "includeCounter"]) + } else { + XCTFail() } } @@ -169,29 +168,25 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testPositionalArgumentsValidation() throws { - try PositionalArgumentsValidator.validate(A.self) - try PositionalArgumentsValidator.validate(F.self) - XCTAssertThrowsError(try PositionalArgumentsValidator.validate(G.self)) { error in - if let error = error as? PositionalArgumentsValidator.Error { - XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") - XCTAssert(error.repeatedPositionalArgument == "items") - } else { - XCTFail() - } - XCTAssert(error is PositionalArgumentsValidator.Error) + XCTAssertNil(PositionalArgumentsValidator.validate(A.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(F.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(H.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(I.self)) + XCTAssertNil(PositionalArgumentsValidator.validate(K.self)) + + if let error = PositionalArgumentsValidator.validate(G.self) as? PositionalArgumentsValidator.Error { + XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") + XCTAssert(error.repeatedPositionalArgument == "items") + } else { + XCTFail() } - try PositionalArgumentsValidator.validate(H.self) - try PositionalArgumentsValidator.validate(I.self) - XCTAssertThrowsError(try PositionalArgumentsValidator.validate(J.self)) { error in - if let error = error as? PositionalArgumentsValidator.Error { - XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") - XCTAssert(error.repeatedPositionalArgument == "numberOfItems") - } else { - XCTFail() - } - XCTAssert(error is PositionalArgumentsValidator.Error) + + if let error = PositionalArgumentsValidator.validate(J.self) as? PositionalArgumentsValidator.Error { + XCTAssert(error.positionalArgumentFollowingRepeated == "phrase") + XCTAssert(error.repeatedPositionalArgument == "numberOfItems") + } else { + XCTFail() } - try PositionalArgumentsValidator.validate(K.self) } // MARK: ParsableArgumentsUniqueNamesValidator tests @@ -207,7 +202,7 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_NoViolation() throws { - XCTAssertNoThrow(try ParsableArgumentsUniqueNamesValidator.validate(DifferentNames.self)) + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DifferentNames.self)) } // MARK: One name is duplicated @@ -220,12 +215,12 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_TwoOfSameName() throws { - XCTAssertThrowsError(try ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self)) { error in - if let error = error as? ParsableArgumentsUniqueNamesValidator.Error { - XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"foo\".") - } else { - XCTFail(unexpectedErrorMessage) - } + if let error = ParsableArgumentsUniqueNamesValidator.validate(TwoOfTheSameName.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"foo\".") + } else { + XCTFail(unexpectedErrorMessage) } } @@ -248,22 +243,22 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_TwoDuplications() throws { - XCTAssertThrowsError(try ParsableArgumentsUniqueNamesValidator.validate(MultipleUniquenessViolations.self)) { error in - if let error = error as? ParsableArgumentsUniqueNamesValidator.Error { - XCTAssert( - /// The `Mirror` reflects the properties `foo` and `bar` in a random order each time it's built. - error.description == """ - Multiple (2) `Option` or `Flag` arguments are named \"bar\". - Multiple (2) `Option` or `Flag` arguments are named \"foo\". - """ - || error.description == """ - Multiple (2) `Option` or `Flag` arguments are named \"foo\". - Multiple (2) `Option` or `Flag` arguments are named \"bar\". - """ - ) - } else { - XCTFail(unexpectedErrorMessage) - } + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleUniquenessViolations.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssert( + /// The `Mirror` reflects the properties `foo` and `bar` in a random order each time it's built. + error.description == """ + Multiple (2) `Option` or `Flag` arguments are named \"bar\". + Multiple (2) `Option` or `Flag` arguments are named \"foo\". + """ + || error.description == """ + Multiple (2) `Option` or `Flag` arguments are named \"foo\". + Multiple (2) `Option` or `Flag` arguments are named \"bar\". + """ + ) + } else { + XCTFail(unexpectedErrorMessage) } } @@ -283,12 +278,12 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_ArgumentHasMultipleNames() throws { - XCTAssertThrowsError(try ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self)) { error in - if let error = error as? ParsableArgumentsUniqueNamesValidator.Error { - XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"v\".") - } else { - XCTFail(unexpectedErrorMessage) - } + if let error = ParsableArgumentsUniqueNamesValidator.validate(MultipleNamesPerArgument.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (2) `Option` or `Flag` arguments are named \"v\".") + } else { + XCTFail(unexpectedErrorMessage) } } @@ -314,12 +309,12 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_MoreThanTwoDuplications() throws { - XCTAssertThrowsError(try ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self)) { error in - if let error = error as? ParsableArgumentsUniqueNamesValidator.Error { - XCTAssertEqual(error.description, "Multiple (4) `Option` or `Flag` arguments are named \"foo\".") - } else { - XCTFail(unexpectedErrorMessage) - } + if let error = ParsableArgumentsUniqueNamesValidator.validate(FourDuplicateNames.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (4) `Option` or `Flag` arguments are named \"foo\".") + } else { + XCTFail(unexpectedErrorMessage) } } @@ -356,16 +351,16 @@ final class ParsableArgumentsValidationTests: XCTestCase { } func testUniqueNamesValidation_DuplicatedFlagFirstLetters_ShortNames() throws { - XCTAssertThrowsError(try ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self)) { error in - if let error = error as? ParsableArgumentsUniqueNamesValidator.Error { - XCTAssertEqual(error.description, "Multiple (3) `Option` or `Flag` arguments are named \"f\".") - } else { - XCTFail(unexpectedErrorMessage) - } + if let error = ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersShortNames.self) + as? ParsableArgumentsUniqueNamesValidator.Error + { + XCTAssertEqual(error.description, "Multiple (3) `Option` or `Flag` arguments are named \"f\".") + } else { + XCTFail(unexpectedErrorMessage) } } func testUniqueNamesValidation_DuplicatedFlagFirstLetters_LongNames() throws { - XCTAssertNoThrow(try ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self)) + XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self)) } } From 55d80e0016d20d1164a638f72120b3917f060cf6 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 23 Jun 2020 10:28:39 -0500 Subject: [PATCH 6/7] Move nonsense flag warning to argument validation --- .../Parsable Properties/Flag.swift | 10 +-- .../ParsableArgumentsValidation.swift | 59 +++++++++++++ .../ArgumentParser/Parsing/ArgumentSet.swift | 5 +- .../ParsableArgumentsValidationTests.swift | 86 +++++++++++++++++++ 4 files changed, 149 insertions(+), 11 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Flag.swift b/Sources/ArgumentParser/Parsable Properties/Flag.swift index f0ad4faf0..4ee71bb15 100644 --- a/Sources/ArgumentParser/Parsable Properties/Flag.swift +++ b/Sources/ArgumentParser/Parsable Properties/Flag.swift @@ -155,7 +155,7 @@ extension Flag where Value == Bool { ) { self.init(_parsedValue: .init { key in .flag(key: key, name: name, default: initial, help: help) - }) + }) } /// Creates a Boolean property that reads its value from the presence of a @@ -189,14 +189,6 @@ extension Flag where Value == Bool { name: NameSpecification = .long, help: ArgumentHelp? = nil ) { - // Print a warning if the default value is set to `true`, as it cannot be overridden from the command line - if wrappedValue { - // Print a warning with color in the style of a deprecation warning - let magenta = "\u{001B}[0;35m" - let reset = "\u{001B}[0;0m" - print("\(magenta)warning:\(reset) setting the default value to `true` for a Flag without an inversion will always result in that flag being `true`, regardless of what is provided from the command line. Consider enabling an inversion (`@Flag(inversion: .prefixedNo)`) or removing the `@Flag` property wrapper altogether.") - } - self.init( name: name, initial: wrappedValue, diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift index a8ebde081..c37d57fb0 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArgumentsValidation.swift @@ -28,8 +28,10 @@ struct ParsableArgumentsValidationError: Error, CustomStringConvertible { var description: String { """ Validation failed for `\(parsableArgumentsType)`: + \(underlayingErrors.map({"- \($0)"}).joined(separator: "\n")) + """ } } @@ -40,6 +42,7 @@ extension ParsableArguments { PositionalArgumentsValidator.self, ParsableArgumentsCodingKeyValidator.self, ParsableArgumentsUniqueNamesValidator.self, + NonsenseFlagsValidator.self, ] let errors = validators.compactMap { validator in validator.validate(self) @@ -247,3 +250,59 @@ struct ParsableArgumentsUniqueNamesValidator: ParsableArgumentsValidator { : Error(duplicateNames: duplicateNames) } } + +struct NonsenseFlagsValidator: ParsableArgumentsValidator { + struct Error: ParsableArgumentsValidatorError, CustomStringConvertible { + var names: [String] + + var description: String { + """ + One or more Boolean flags is declared with an initial value of `true`. + This results in the flag always being `true`, no matter whether the user + specifies the flag or not. To resolve this error, change the default to + `false`, provide a value for the `inversion:` parameter, or remove the + `@Flag` property wrapper altogether. + + Affected flag(s): + \(names.joined(separator: "\n")) + """ + } + + var kind: ValidatorErrorKind { .warning } + } + + static func validate(_ type: ParsableArguments.Type) -> ParsableArgumentsValidatorError? { + let argSets: [ArgumentSet] = Mirror(reflecting: type.init()) + .children + .compactMap { child in + guard + var codingKey = child.label, + let parsed = child.value as? ArgumentSetProvider + else { return nil } + + // Property wrappers have underscore-prefixed names + codingKey = String(codingKey.first == "_" ? codingKey.dropFirst(1) : codingKey.dropFirst(0)) + + let key = InputKey(rawValue: codingKey) + return parsed.argumentSet(for: key) + } + + let nonsenseFlags: [String] = argSets.flatMap { args -> [String] in + args.compactMap { def in + if case .nullary = def.update, + !def.help.isComposite, + def.help.options.contains(.isOptional), + def.help.defaultValue == "true" + { + return def.unadornedSynopsis + } else { + return nil + } + } + } + + return nonsenseFlags.isEmpty + ? nil + : Error(names: nonsenseFlags) + } +} diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 30334fcdb..c02f67513 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -97,8 +97,9 @@ extension ArgumentSet { static func flag(key: InputKey, name: NameSpecification, default initialValue: Bool?, help: ArgumentHelp?) -> ArgumentSet { // The flag is required if initialValue is `nil`, otherwise it's optional let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : [] - - let help = ArgumentDefinition.Help(options: helpOptions, help: help, key: key) + let defaultValueString = initialValue == true ? "true" : nil + + let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: defaultValueString, key: key) let arg = ArgumentDefinition(kind: .name(key: key, specification: name), help: help, update: .nullary({ (origin, name, values) in values.set(true, forKey: key, inputOrigin: origin) }), initial: { origin, values in diff --git a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift index 6b5a1c8b8..fb1fe9633 100644 --- a/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift +++ b/Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift @@ -363,4 +363,90 @@ final class ParsableArgumentsValidationTests: XCTestCase { func testUniqueNamesValidation_DuplicatedFlagFirstLetters_LongNames() throws { XCTAssertNil(ParsableArgumentsUniqueNamesValidator.validate(DuplicatedFirstLettersLongNames.self)) } + + fileprivate struct HasOneNonsenseFlag: ParsableCommand { + enum ExampleEnum: String, EnumerableFlag { + case first + case second + case other + case forth + case fith + } + + @Flag + var enumFlag: ExampleEnum = .first + + @Flag + var fine: Bool = false + + @Flag(inversion: .prefixedNo) + var alsoFine: Bool = false + + @Flag(inversion: .prefixedNo) + var stillFine: Bool = true + + @Flag(inversion: .prefixedNo) + var yetStillFine: Bool + + @Flag + var nonsense: Bool = true + } + + func testNonsenseFlagsValidation_OneFlag() throws { + if let error = NonsenseFlagsValidator.validate(HasOneNonsenseFlag.self) + as? NonsenseFlagsValidator.Error + { + XCTAssertEqual( + error.description, + """ + One or more Boolean flags is declared with an initial value of `true`. + This results in the flag always being `true`, no matter whether the user + specifies the flag or not. To resolve this error, change the default to + `false`, provide a value for the `inversion:` parameter, or remove the + `@Flag` property wrapper altogether. + + Affected flag(s): + --nonsense + """) + } else { + XCTFail(unexpectedErrorMessage) + } + } + + fileprivate struct MultipleNonsenseFlags: ParsableCommand { + @Flag + var stuff = true + + @Flag + var nonsense = true + + @Flag + var okay = false + + @Flag + var moreNonsense = true + } + + func testNonsenseFlagsValidation_MultipleFlags() throws { + if let error = NonsenseFlagsValidator.validate(MultipleNonsenseFlags.self) + as? NonsenseFlagsValidator.Error + { + XCTAssertEqual( + error.description, + """ + One or more Boolean flags is declared with an initial value of `true`. + This results in the flag always being `true`, no matter whether the user + specifies the flag or not. To resolve this error, change the default to + `false`, provide a value for the `inversion:` parameter, or remove the + `@Flag` property wrapper altogether. + + Affected flag(s): + --stuff + --nonsense + --more-nonsense + """) + } else { + XCTFail(unexpectedErrorMessage) + } + } } From 9d623a2a22b5df03cef9ef4aff5fc681b366b167 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 23 Jun 2020 10:31:54 -0500 Subject: [PATCH 7/7] Update guides/readme with default literal syntax --- Documentation/01 Getting Started.md | 8 ++-- .../02 Arguments, Options, and Flags.md | 44 +++++++++---------- Documentation/03 Commands and Subcommands.md | 6 +-- Documentation/04 Customizing Help.md | 8 ++-- Documentation/05 Validation and Errors.md | 5 +-- .../06 Manual Parsing and Testing.md | 4 +- README.md | 2 +- 7 files changed, 38 insertions(+), 39 deletions(-) diff --git a/Documentation/01 Getting Started.md b/Documentation/01 Getting Started.md index a79d8d0e7..dd4bc494b 100644 --- a/Documentation/01 Getting Started.md +++ b/Documentation/01 Getting Started.md @@ -133,7 +133,7 @@ struct Count: ParsableCommand { var outputFile: String @Flag() - var verbose: Bool + var verbose = false mutating func run() throws { if verbose { @@ -175,7 +175,7 @@ struct Count: ParsableCommand { var outputFile: String @Flag(name: .shortAndLong) - var verbose: Bool + var verbose = false mutating func run() throws { ... } } @@ -209,7 +209,7 @@ struct Count: ParsableCommand { var outputFile: String @Flag(name: .shortAndLong, help: "Print status updates while counting.") - var verbose: Bool + var verbose = false mutating func run() throws { ... } } @@ -244,7 +244,7 @@ struct Count: ParsableCommand { var outputFile: String @Flag(name: .shortAndLong, help: "Print status updates while counting.") - var verbose: Bool + var verbose = false mutating func run() throws { if verbose { diff --git a/Documentation/02 Arguments, Options, and Flags.md b/Documentation/02 Arguments, Options, and Flags.md index 4150f34d2..1ca97fb98 100644 --- a/Documentation/02 Arguments, Options, and Flags.md +++ b/Documentation/02 Arguments, Options, and Flags.md @@ -26,24 +26,24 @@ The three preceding examples could be calls of this `Example` command: ```swift struct Example: ParsableCommand { - @Argument() var files: [String] + @Argument() var files: [String] = [] @Option() var count: Int? - @Option var index: Int = 0 + @Option() var index = 0 - @Flag() var verbose: Bool + @Flag() var verbose = false - @Flag() var stripWhitespace: Bool + @Flag() var stripWhitespace = false } ``` This example shows how `ArgumentParser` provides defaults that speed up your initial development process: - Option and flag names are derived from the names of your command's properties. -- Whether arguments are required and what kinds of inputs are valid is based on your properties' types. +- What kinds of inputs are valid, and whether arguments are required, is based on your properties' types and default values. -In this example, all of the properties have default values — Boolean flags always default to `false`, optional properties default to `nil`, and arrays default to an empty array. An option, flag, or argument with a `default` parameter can also be omitted by the user. +In this example, all of the properties have default values (optional properties default to `nil`). Users must provide values for all properties with no implicit or specified default. For example, this command would require one integer argument and a string with the key `--user-name`. @@ -70,12 +70,12 @@ Usage: example --user-name See 'example --help' for more information. ``` -When using the `default` parameter for an array property, the default values will not be included if additional values are passed on the command line. +When providing a default value for an array property, any user-supplied values replace the entire default. -``` +```swift struct Lucky: ParsableCommand { - @Argument(default: [7, 14, 21]) - var numbers: [Int] + @Argument() + var numbers = [7, 14, 21] mutating func run() throws { print(""" @@ -104,10 +104,10 @@ You can override this default by specifying one or more name specifications in t ```swift struct Example: ParsableCommand { @Flag(name: .long) // Same as the default - var stripWhitespace: Bool + var stripWhitespace = false @Flag(name: .short) - var verbose: Bool + var verbose = false @Option(name: .customLong("count")) var iterationCount: Int @@ -214,7 +214,7 @@ Flags are most frequently used for `Bool` properties. You can generate a `true`/ ```swift struct Example: ParsableCommand { @Flag(inversion: .prefixedNo) - var index: Bool = true + var index = true @Flag(inversion: .prefixedEnableDisable) var requiredElement: Bool @@ -225,7 +225,7 @@ struct Example: ParsableCommand { } ``` -When providing a flag inversion, you can pass your own default with normal property initialization syntax (`@Flag var foo: Bool = true`). If you want to require that the user specify one of the two inversions, use a non-Optional type and do not pass a default value. +When declaring a flag with an inversion, set the default by specifying `true` or `false` as the property's initial value. If you want to require that the user specify one of the two inversions, leave off the default value. In the `Example` command defined above, a flag is required for the `requiredElement` property. The specified prefixes are prepended to the long names for the flags: @@ -241,19 +241,19 @@ Error: Missing one of: '--enable-required-element', '--disable-required-element' To create a flag with custom names for a Boolean value, to provide an exclusive choice between more than two names, or for collecting multiple values from a set of defined choices, define an enumeration that conforms to the `EnumerableFlag` protocol. ```swift -enum CacheMethod: EnumerableFlag { +enum CacheMethod: String, EnumerableFlag { case inMemoryCache case persistentCache } -enum Color: EnumerableFlag { +enum Color: String, EnumerableFlag { case pink, purple, silver } struct Example: ParsableCommand { @Flag() var cacheMethod: CacheMethod - @Flag() var colors: [Color] + @Flag() var colors: [Color] = [] mutating func run() throws { print(cacheMethod) @@ -302,7 +302,7 @@ For example, this command defines a `--verbose` flag, a `--name` option, and an ```swift struct Example: ParsableCommand { - @Flag() var verbose: Bool + @Flag() var verbose = false @Option() var name: String @Argument() var file: String? @@ -349,8 +349,8 @@ The default strategy for parsing options as arrays is to read each value from a ```swift struct Example: ParsableCommand { - @Option() var file: [String] - @Flag() var verbose: Bool + @Option() var file: [String] = [] + @Flag() var verbose = false mutating func run() throws { print("Verbose: \(verbose), files: \(file)") @@ -400,8 +400,8 @@ The default strategy for parsing arrays of positional arguments is to ignore al ```swift struct Example: ParsableCommand { - @Flag() var verbose: Bool - @Argument() var files: [String] + @Flag() var verbose = false + @Argument() var files: [String] = [] mutating func run() throws { print("Verbose: \(verbose), files: \(files)") diff --git a/Documentation/03 Commands and Subcommands.md b/Documentation/03 Commands and Subcommands.md index eaa139962..6a7f24114 100644 --- a/Documentation/03 Commands and Subcommands.md +++ b/Documentation/03 Commands and Subcommands.md @@ -56,7 +56,7 @@ In this case, the `Options` type accepts a `--hexadecimal-output` flag and expec ```swift struct Options: ParsableArguments { @Flag(name: [.long, .customShort("x")], help: "Use hexadecimal notation for the result.") - var hexadecimalOutput: Bool + var hexadecimalOutput = false @Argument(help: "A group of integers to operate on.") var values: [Int] @@ -124,7 +124,7 @@ extension Math.Statistics { var kind: Kind = .mean @Argument(help: "A group of floating-point values to operate on.") - var values: [Double] + var values: [Double] = [] func calculateMean() -> Double { ... } func calculateMedian() -> Double { ... } @@ -151,7 +151,7 @@ extension Math.Statistics { abstract: "Print the standard deviation of the values.") @Argument(help: "A group of floating-point values to operate on.") - var values: [Double] + var values: [Double] = [] mutating func run() { if values.isEmpty { diff --git a/Documentation/04 Customizing Help.md b/Documentation/04 Customizing Help.md index 5bf025170..a3e11c58a 100644 --- a/Documentation/04 Customizing Help.md +++ b/Documentation/04 Customizing Help.md @@ -7,10 +7,10 @@ You can provide help text when declaring any `@Argument`, `@Option`, or `@Flag` ```swift struct Example: ParsableCommand { @Flag(help: "Display extra information while processing.") - var verbose: Bool + var verbose = false @Option(help: "The number of extra lines to show.") - var extraLines: Int = 0 + var extraLines = 0 @Argument(help: "The input file.") var inputFile: String? @@ -42,12 +42,12 @@ Here's the same command with some extra customization: ```swift struct Example: ParsableCommand { @Flag(help: "Display extra information while processing.") - var verbose: Bool + var verbose = false @Option(help: ArgumentHelp( "The number of extra lines to show.", valueName: "n")) - var extraLines: Int = 0 + var extraLines = 0 @Argument(help: ArgumentHelp( "The input file.", diff --git a/Documentation/05 Validation and Errors.md b/Documentation/05 Validation and Errors.md index 947f0ded8..3b358a178 100644 --- a/Documentation/05 Validation and Errors.md +++ b/Documentation/05 Validation and Errors.md @@ -16,7 +16,7 @@ struct Select: ParsableCommand { var count: Int = 1 @Argument() - var elements: [String] + var elements: [String] = [] mutating func validate() throws { guard count >= 1 else { @@ -127,12 +127,11 @@ struct ExampleDataModel: Codable { } struct Example: ParsableCommand { - // Reads in the argument string and attempts to transform it to // an `ExampleDataModel` object using the `JSONDecoder`. If the // string is not valid JSON, `decode` will throw an error and // parsing will halt. - @Argument(transform: ExampleDataModel.dataModel ) + @Argument(transform: ExampleDataModel.dataModel) var inputJSON: ExampleDataModel // Specifiying this option will always cause the parser to exit diff --git a/Documentation/06 Manual Parsing and Testing.md b/Documentation/06 Manual Parsing and Testing.md index cf2ba7a98..4b7d03e0b 100644 --- a/Documentation/06 Manual Parsing and Testing.md +++ b/Documentation/06 Manual Parsing and Testing.md @@ -12,11 +12,11 @@ Let's implement the `Select` command discussed in [Validation and Errors](05%20V ```swift struct SelectOptions: ParsableArguments { - @Option + @Option() var count: Int = 1 @Argument() - var elements: [String] + var elements: [String] = [] } ``` diff --git a/README.md b/README.md index a58ec12ad..8dcb55dbc 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ import ArgumentParser struct Repeat: ParsableCommand { @Flag(help: "Include a counter with each repetition.") - var includeCounter: Bool + var includeCounter = false @Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.") var count: Int?