diff --git a/Documentation/02 Arguments, Options, and Flags.md b/Documentation/02 Arguments, Options, and Flags.md index c3da670d7..9ceec3bfa 100644 --- a/Documentation/02 Arguments, Options, and Flags.md +++ b/Documentation/02 Arguments, Options, and Flags.md @@ -70,6 +70,31 @@ 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. + +``` +struct Lucky: ParsableCommand { + @Argument(default: [7, 14, 21]) + var numbers: [Int] + + mutating func run() throws { + print(""" + Your lucky numbers are: + \(numbers.map(String.init).joined(separator: " ")) + """) + } +} +``` + +``` +% lucky +Your lucky numbers are: +7 14 21 +% lucky 1 2 3 +Your lucky numbers are: +1 2 3 +``` + ## Customizing option and flag names By default, options and flags derive the name that you use on the command line from the name of the property, such as `--count` and `--index`. Camel-case names are converted to lowercase with hyphen-separated words, like `--strip-whitespace`. diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index 8eb3ad908..96e38b0b9 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -204,13 +204,13 @@ extension Argument { /// Creates a property that reads an array from zero or more arguments. /// - /// The property has an empty array as its default value. - /// /// - Parameters: + /// - initial: A default value to use for this property. /// - parsingStrategy: The behavior to use when parsing multiple values /// from the command-line arguments. /// - help: Information about how to use this argument. public init( + default initial: Value = [], parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil ) @@ -218,14 +218,15 @@ extension Argument { { self.init(_parsedValue: .init { key in let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - let arg = ArgumentDefinition( + var arg = ArgumentDefinition( kind: .positional, help: help, parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in - values.set([], forKey: key, inputOrigin: origin) + values.set(initial, forKey: key, inputOrigin: origin) }) + arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil return ArgumentSet(alternatives: [arg]) }) } @@ -233,15 +234,15 @@ extension Argument { /// Creates a property that reads an array from zero or more arguments, /// parsing each element with the given closure. /// - /// The property has an empty array as its default value. - /// /// - Parameters: + /// - initial: A default value to use for this property. /// - parsingStrategy: The behavior to use when parsing multiple values /// from the command-line arguments. /// - help: Information about how to use this argument. /// - transform: A closure that converts a string into this property's /// element type or throws an error. public init( + default initial: Value = [], parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining, help: ArgumentHelp? = nil, transform: @escaping (String) throws -> Element @@ -250,7 +251,7 @@ extension Argument { { self.init(_parsedValue: .init { key in let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - let arg = ArgumentDefinition( + var arg = ArgumentDefinition( kind: .positional, help: help, parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput, @@ -266,8 +267,9 @@ extension Argument { } }), initial: { origin, values in - values.set([], forKey: key, inputOrigin: origin) + values.set(initial, forKey: key, inputOrigin: origin) }) + arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil return ArgumentSet(alternatives: [arg]) }) } diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 343f90f91..03e3e98df 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -309,24 +309,25 @@ extension Option { /// Creates an array property that reads its values from zero or more /// labeled options. /// - /// This property defaults to an empty array. - /// /// - Parameters: /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. /// - parsingStrategy: The behavior to use when parsing multiple values /// from the command-line arguments. /// - help: Information about how to use this option. public init( name: NameSpecification = .long, + default initial: Array = [], parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil ) where Element: ExpressibleByArgument, Value == Array { self.init(_parsedValue: .init { key in let kind = ArgumentDefinition.Kind.name(key: key, specification: name) let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in - values.set([], forKey: key, inputOrigin: origin) + 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) }) + arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil return ArgumentSet(alternatives: [arg]) }) } @@ -334,10 +335,13 @@ extension Option { /// Creates an array property that reads its values from zero or more /// labeled options, parsing with the given closure. /// - /// This property defaults to an empty array. + /// This property defaults to an empty array if the `initial` parameter + /// is not specified. /// /// - Parameters: /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. If `initial` is + /// `nil`, this option defaults to an empty array. /// - parsingStrategy: The behavior to use when parsing multiple values /// from the command-line arguments. /// - help: Information about how to use this option. @@ -345,6 +349,7 @@ extension Option { /// element type or throws an error. public init( name: NameSpecification = .long, + default initial: Array = [], parsing parsingStrategy: ArrayParsingStrategy = .singleValue, help: ArgumentHelp? = nil, transform: @escaping (String) throws -> Element @@ -352,7 +357,7 @@ extension Option { self.init(_parsedValue: .init { key in let kind = ArgumentDefinition.Kind.name(key: key, specification: name) let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) - let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({ + var arg = ArgumentDefinition.init(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({ (origin, name, valueString, parsedValues) in do { let transformedElement = try transform(valueString) @@ -363,8 +368,9 @@ extension Option { throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) } }), initial: { origin, values in - values.set([], forKey: key, inputOrigin: origin) + values.set(initial, forKey: key, inputOrigin: origin) }) + arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil return ArgumentSet(alternatives: [arg]) }) } diff --git a/Sources/ArgumentParser/Parsing/ParsedValues.swift b/Sources/ArgumentParser/Parsing/ParsedValues.swift index e5c0b6f03..839f15cbc 100644 --- a/Sources/ArgumentParser/Parsing/ParsedValues.swift +++ b/Sources/ArgumentParser/Parsing/ParsedValues.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -struct InputKey: RawRepresentable, Equatable { +struct InputKey: RawRepresentable, Hashable { var rawValue: String init(rawValue: String) { @@ -32,6 +32,7 @@ struct ParsedValues { var value: Any /// Where in the input that this came from. var inputOrigin: InputOrigin + fileprivate var shouldClearArrayIfParsed = true } /// These are the parsed key-value pairs. @@ -73,4 +74,18 @@ extension ParsedValues { e.inputOrigin.formUnion(inputOrigin) set(e) } + + mutating func update(forKey key: InputKey, inputOrigin: InputOrigin, initial: [A], closure: (inout [A]) -> Void) { + var e = element(forKey: key) ?? Element(key: key, value: initial, inputOrigin: InputOrigin()) + var v = (e.value as? [A] ) ?? initial + // The first time a value is parsed from command line, empty array of any default values. + if e.shouldClearArrayIfParsed { + v.removeAll() + e.shouldClearArrayIfParsed = false + } + closure(&v) + e.value = v + e.inputOrigin.formUnion(inputOrigin) + set(e) + } } diff --git a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift index 1abe202a8..fb71c41b0 100644 --- a/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift @@ -373,3 +373,72 @@ extension DefaultsEndToEndTests { } } +fileprivate struct Quux: ParsableArguments { + @Option(default: ["A", "B"], parsing: .upToNextOption) + var letters: [String] + + @Argument(default: [1, 2]) + var numbers: [Int] +} + +extension DefaultsEndToEndTests { + func testParsing_ArrayDefaults() throws { + AssertParse(Quux.self, []) { qux in + XCTAssertEqual(qux.letters, ["A", "B"]) + XCTAssertEqual(qux.numbers, [1, 2]) + } + AssertParse(Quux.self, ["--letters", "C", "D"]) { qux in + XCTAssertEqual(qux.letters, ["C", "D"]) + XCTAssertEqual(qux.numbers, [1, 2]) + } + AssertParse(Quux.self, ["3", "4"]) { qux in + XCTAssertEqual(qux.letters, ["A", "B"]) + XCTAssertEqual(qux.numbers, [3, 4]) + } + AssertParse(Quux.self, ["3", "4", "--letters", "C", "D"]) { qux in + XCTAssertEqual(qux.letters, ["C", "D"]) + XCTAssertEqual(qux.numbers, [3, 4]) + } + } +} + +fileprivate struct Main: ParsableCommand { + static var configuration = CommandConfiguration( + subcommands: [Sub.self], + defaultSubcommand: Sub.self + ) + + struct Options: ParsableArguments { + @Option(default: ["A", "B"], parsing: .upToNextOption) + var letters: [String] + } + + struct Sub: ParsableCommand { + @Argument(default: [1, 2]) + var numbers: [Int] + + @OptionGroup() + var options: Main.Options + } +} + +extension DefaultsEndToEndTests { + func testParsing_ArrayDefaults_Subcommands() { + AssertParseCommand(Main.self, Main.Sub.self, []) { sub in + XCTAssertEqual(sub.options.letters, ["A", "B"]) + XCTAssertEqual(sub.numbers, [1, 2]) + } + AssertParseCommand(Main.self, Main.Sub.self, ["--letters", "C", "D"]) { sub in + XCTAssertEqual(sub.options.letters, ["C", "D"]) + XCTAssertEqual(sub.numbers, [1, 2]) + } + AssertParseCommand(Main.self, Main.Sub.self, ["3", "4"]) { sub in + XCTAssertEqual(sub.options.letters, ["A", "B"]) + XCTAssertEqual(sub.numbers, [3, 4]) + } + AssertParseCommand(Main.self, Main.Sub.self, ["3", "4", "--letters", "C", "D"]) { sub in + XCTAssertEqual(sub.options.letters, ["C", "D"]) + XCTAssertEqual(sub.numbers, [3, 4]) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift index 056d940ee..fcc63b71e 100644 --- a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift @@ -44,14 +44,22 @@ fileprivate struct AlmostAllArguments: ParsableArguments { @Argument(help: "", transform: { _ in 0 }) var d4: Int? @Argument(default: 0, transform: { _ in 0 }) var d5: Int? - @Argument(parsing: .remaining, help: "") var e: [Int] - @Argument() var e0: [Int] - @Argument(help: "") var e1: [Int] - @Argument(parsing: .remaining) var e2: [Int] - @Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e3: [Int] - @Argument(transform: { _ in 0 }) var e4: [Int] - @Argument(help: "", transform: { _ in 0 }) var e5: [Int] - @Argument(parsing: .remaining, transform: { _ in 0 }) var e6: [Int] + @Argument(default: [1, 2], parsing: .remaining, help: "") var e: [Int] + @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() 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(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(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] } fileprivate struct AllOptions: ParsableArguments { @@ -115,21 +123,35 @@ fileprivate struct AllOptions: ParsableArguments { @Option(parsing: .next, transform: { _ in 0 }) var d12: Int? @Option(help: "", transform: { _ in 0 }) var d13: Int? - @Option(name: .long, parsing: .singleValue, help: "") var e: [Int] - @Option(parsing: .singleValue, help: "") var e1: [Int] - @Option(name: .long, help: "") var e2: [Int] - @Option(name: .long, parsing: .singleValue) var e3: [Int] - @Option(name: .long) var e4: [Int] - @Option(parsing: .singleValue) var e5: [Int] - @Option(help: "") var e6: [Int] - - @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int] - @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int] - @Option(name: .long, help: "", transform: { _ in 0 }) var f2: [Int] - @Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f3: [Int] - @Option(name: .long, transform: { _ in 0 }) var f4: [Int] - @Option(parsing: .singleValue, transform: { _ in 0 }) var f5: [Int] - @Option(help: "", transform: { _ in 0 }) var f6: [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 e2: [Int] + @Option(name: .long, default: [1, 2], help: "") var e3: [Int] + @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(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 e10: [Int] + @Option(default: [1, 2]) var e11: [Int] + @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 f2: [Int] + @Option(name: .long, default: [1, 2], help: "", transform: { _ in 0 }) var f3: [Int] + @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(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 f10: [Int] + @Option(default: [1, 2], transform: { _ in 0 }) var f11: [Int] + @Option(parsing: .singleValue, transform: { _ in 0 }) var f12: [Int] + @Option(help: "", transform: { _ in 0 }) var f13: [Int] } struct AllFlags: ParsableArguments { diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index 48539f452..dcd4a64c1 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -145,6 +145,9 @@ extension HelpGenerationTests { @Option(default: 20, help: "Your age.") var age: Int + @Option(default: [7, 14], parsing: .upToNextOption, help: ArgumentHelp("Your lucky numbers.", valueName: "numbers")) + var lucky: [Int] + @Option(default: false, help: "Whether logging is enabled.") var logging: Bool @@ -160,7 +163,7 @@ extension HelpGenerationTests { func testHelpWithDefaultValues() { AssertHelp(for: D.self, equals: """ - USAGE: d [] [--name ] [--middle-name ] [--age ] [--logging ] [--optional] [--required] [--degree ] [--directory ] + USAGE: d [] [--name ] [--middle-name ] [--age ] [--lucky ...] [--logging ] [--optional] [--required] [--degree ] [--directory ] ARGUMENTS: Your occupation. (default: --) @@ -170,6 +173,7 @@ extension HelpGenerationTests { --middle-name Your middle name. (default: Winston) --age Your age. (default: 20) + --lucky Your lucky numbers. (default: [7, 14]) --logging Whether logging is enabled. (default: false) --optional/--required Vegan diet. (default: optional) --degree Your degree. (default: bachelor)