Skip to content

Commit f07d2fb

Browse files
authored
Enable default for option and argument arrays (#186)
* Enable default for option and argument arrays * Moved array reset to ParsedValues
1 parent 618f282 commit f07d2fb

File tree

7 files changed

+183
-40
lines changed

7 files changed

+183
-40
lines changed

Documentation/02 Arguments, Options, and Flags.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,31 @@ Usage: example --user-name <user-name> <value>
7070
See 'example --help' for more information.
7171
```
7272

73+
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.
74+
75+
```
76+
struct Lucky: ParsableCommand {
77+
@Argument(default: [7, 14, 21])
78+
var numbers: [Int]
79+
80+
mutating func run() throws {
81+
print("""
82+
Your lucky numbers are:
83+
\(numbers.map(String.init).joined(separator: " "))
84+
""")
85+
}
86+
}
87+
```
88+
89+
```
90+
% lucky
91+
Your lucky numbers are:
92+
7 14 21
93+
% lucky 1 2 3
94+
Your lucky numbers are:
95+
1 2 3
96+
```
97+
7398
## Customizing option and flag names
7499

75100
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`.

Sources/ArgumentParser/Parsable Properties/Argument.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,44 +204,45 @@ extension Argument {
204204

205205
/// Creates a property that reads an array from zero or more arguments.
206206
///
207-
/// The property has an empty array as its default value.
208-
///
209207
/// - Parameters:
208+
/// - initial: A default value to use for this property.
210209
/// - parsingStrategy: The behavior to use when parsing multiple values
211210
/// from the command-line arguments.
212211
/// - help: Information about how to use this argument.
213212
public init<Element>(
213+
default initial: Value = [],
214214
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
215215
help: ArgumentHelp? = nil
216216
)
217217
where Element: ExpressibleByArgument, Value == Array<Element>
218218
{
219219
self.init(_parsedValue: .init { key in
220220
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
221-
let arg = ArgumentDefinition(
221+
var arg = ArgumentDefinition(
222222
kind: .positional,
223223
help: help,
224224
parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput,
225225
update: .appendToArray(forType: Element.self, key: key),
226226
initial: { origin, values in
227-
values.set([], forKey: key, inputOrigin: origin)
227+
values.set(initial, forKey: key, inputOrigin: origin)
228228
})
229+
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
229230
return ArgumentSet(alternatives: [arg])
230231
})
231232
}
232233

233234
/// Creates a property that reads an array from zero or more arguments,
234235
/// parsing each element with the given closure.
235236
///
236-
/// The property has an empty array as its default value.
237-
///
238237
/// - Parameters:
238+
/// - initial: A default value to use for this property.
239239
/// - parsingStrategy: The behavior to use when parsing multiple values
240240
/// from the command-line arguments.
241241
/// - help: Information about how to use this argument.
242242
/// - transform: A closure that converts a string into this property's
243243
/// element type or throws an error.
244244
public init<Element>(
245+
default initial: Value = [],
245246
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
246247
help: ArgumentHelp? = nil,
247248
transform: @escaping (String) throws -> Element
@@ -250,7 +251,7 @@ extension Argument {
250251
{
251252
self.init(_parsedValue: .init { key in
252253
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
253-
let arg = ArgumentDefinition(
254+
var arg = ArgumentDefinition(
254255
kind: .positional,
255256
help: help,
256257
parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput,
@@ -266,8 +267,9 @@ extension Argument {
266267
}
267268
}),
268269
initial: { origin, values in
269-
values.set([], forKey: key, inputOrigin: origin)
270+
values.set(initial, forKey: key, inputOrigin: origin)
270271
})
272+
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
271273
return ArgumentSet(alternatives: [arg])
272274
})
273275
}

Sources/ArgumentParser/Parsable Properties/Option.swift

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -309,50 +309,55 @@ extension Option {
309309
/// Creates an array property that reads its values from zero or more
310310
/// labeled options.
311311
///
312-
/// This property defaults to an empty array.
313-
///
314312
/// - Parameters:
315313
/// - name: A specification for what names are allowed for this flag.
314+
/// - initial: A default value to use for this property.
316315
/// - parsingStrategy: The behavior to use when parsing multiple values
317316
/// from the command-line arguments.
318317
/// - help: Information about how to use this option.
319318
public init<Element>(
320319
name: NameSpecification = .long,
320+
default initial: Array<Element> = [],
321321
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
322322
help: ArgumentHelp? = nil
323323
) where Element: ExpressibleByArgument, Value == Array<Element> {
324324
self.init(_parsedValue: .init { key in
325325
let kind = ArgumentDefinition.Kind.name(key: key, specification: name)
326326
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
327-
let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in
328-
values.set([], forKey: key, inputOrigin: origin)
327+
var arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .appendToArray(forType: Element.self, key: key), initial: { origin, values in
328+
values.set(initial, forKey: key, inputOrigin: origin)
329329
})
330+
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
330331
return ArgumentSet(alternatives: [arg])
331332
})
332333
}
333334

334335
/// Creates an array property that reads its values from zero or more
335336
/// labeled options, parsing with the given closure.
336337
///
337-
/// This property defaults to an empty array.
338+
/// This property defaults to an empty array if the `initial` parameter
339+
/// is not specified.
338340
///
339341
/// - Parameters:
340342
/// - name: A specification for what names are allowed for this flag.
343+
/// - initial: A default value to use for this property. If `initial` is
344+
/// `nil`, this option defaults to an empty array.
341345
/// - parsingStrategy: The behavior to use when parsing multiple values
342346
/// from the command-line arguments.
343347
/// - help: Information about how to use this option.
344348
/// - transform: A closure that converts a string into this property's
345349
/// element type or throws an error.
346350
public init<Element>(
347351
name: NameSpecification = .long,
352+
default initial: Array<Element> = [],
348353
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
349354
help: ArgumentHelp? = nil,
350355
transform: @escaping (String) throws -> Element
351356
) where Value == Array<Element> {
352357
self.init(_parsedValue: .init { key in
353358
let kind = ArgumentDefinition.Kind.name(key: key, specification: name)
354359
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
355-
let arg = ArgumentDefinition(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({
360+
var arg = ArgumentDefinition.init(kind: kind, help: help, parsingStrategy: ArgumentDefinition.ParsingStrategy(parsingStrategy), update: .unary({
356361
(origin, name, valueString, parsedValues) in
357362
do {
358363
let transformedElement = try transform(valueString)
@@ -363,8 +368,9 @@ extension Option {
363368
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
364369
}
365370
}), initial: { origin, values in
366-
values.set([], forKey: key, inputOrigin: origin)
371+
values.set(initial, forKey: key, inputOrigin: origin)
367372
})
373+
arg.help.defaultValue = !initial.isEmpty ? "\(initial)" : nil
368374
return ArgumentSet(alternatives: [arg])
369375
})
370376
}

Sources/ArgumentParser/Parsing/ParsedValues.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12-
struct InputKey: RawRepresentable, Equatable {
12+
struct InputKey: RawRepresentable, Hashable {
1313
var rawValue: String
1414

1515
init(rawValue: String) {
@@ -32,6 +32,7 @@ struct ParsedValues {
3232
var value: Any
3333
/// Where in the input that this came from.
3434
var inputOrigin: InputOrigin
35+
fileprivate var shouldClearArrayIfParsed = true
3536
}
3637

3738
/// These are the parsed key-value pairs.
@@ -73,4 +74,18 @@ extension ParsedValues {
7374
e.inputOrigin.formUnion(inputOrigin)
7475
set(e)
7576
}
77+
78+
mutating func update<A>(forKey key: InputKey, inputOrigin: InputOrigin, initial: [A], closure: (inout [A]) -> Void) {
79+
var e = element(forKey: key) ?? Element(key: key, value: initial, inputOrigin: InputOrigin())
80+
var v = (e.value as? [A] ) ?? initial
81+
// The first time a value is parsed from command line, empty array of any default values.
82+
if e.shouldClearArrayIfParsed {
83+
v.removeAll()
84+
e.shouldClearArrayIfParsed = false
85+
}
86+
closure(&v)
87+
e.value = v
88+
e.inputOrigin.formUnion(inputOrigin)
89+
set(e)
90+
}
7691
}

Tests/ArgumentParserEndToEndTests/DefaultsEndToEndTests.swift

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,72 @@ extension DefaultsEndToEndTests {
373373
}
374374
}
375375

376+
fileprivate struct Quux: ParsableArguments {
377+
@Option(default: ["A", "B"], parsing: .upToNextOption)
378+
var letters: [String]
379+
380+
@Argument(default: [1, 2])
381+
var numbers: [Int]
382+
}
383+
384+
extension DefaultsEndToEndTests {
385+
func testParsing_ArrayDefaults() throws {
386+
AssertParse(Quux.self, []) { qux in
387+
XCTAssertEqual(qux.letters, ["A", "B"])
388+
XCTAssertEqual(qux.numbers, [1, 2])
389+
}
390+
AssertParse(Quux.self, ["--letters", "C", "D"]) { qux in
391+
XCTAssertEqual(qux.letters, ["C", "D"])
392+
XCTAssertEqual(qux.numbers, [1, 2])
393+
}
394+
AssertParse(Quux.self, ["3", "4"]) { qux in
395+
XCTAssertEqual(qux.letters, ["A", "B"])
396+
XCTAssertEqual(qux.numbers, [3, 4])
397+
}
398+
AssertParse(Quux.self, ["3", "4", "--letters", "C", "D"]) { qux in
399+
XCTAssertEqual(qux.letters, ["C", "D"])
400+
XCTAssertEqual(qux.numbers, [3, 4])
401+
}
402+
}
403+
}
404+
405+
fileprivate struct Main: ParsableCommand {
406+
static var configuration = CommandConfiguration(
407+
subcommands: [Sub.self],
408+
defaultSubcommand: Sub.self
409+
)
410+
411+
struct Options: ParsableArguments {
412+
@Option(default: ["A", "B"], parsing: .upToNextOption)
413+
var letters: [String]
414+
}
415+
416+
struct Sub: ParsableCommand {
417+
@Argument(default: [1, 2])
418+
var numbers: [Int]
419+
420+
@OptionGroup()
421+
var options: Main.Options
422+
}
423+
}
424+
425+
extension DefaultsEndToEndTests {
426+
func testParsing_ArrayDefaults_Subcommands() {
427+
AssertParseCommand(Main.self, Main.Sub.self, []) { sub in
428+
XCTAssertEqual(sub.options.letters, ["A", "B"])
429+
XCTAssertEqual(sub.numbers, [1, 2])
430+
}
431+
AssertParseCommand(Main.self, Main.Sub.self, ["--letters", "C", "D"]) { sub in
432+
XCTAssertEqual(sub.options.letters, ["C", "D"])
433+
XCTAssertEqual(sub.numbers, [1, 2])
434+
}
435+
AssertParseCommand(Main.self, Main.Sub.self, ["3", "4"]) { sub in
436+
XCTAssertEqual(sub.options.letters, ["A", "B"])
437+
XCTAssertEqual(sub.numbers, [3, 4])
438+
}
439+
AssertParseCommand(Main.self, Main.Sub.self, ["3", "4", "--letters", "C", "D"]) { sub in
440+
XCTAssertEqual(sub.options.letters, ["C", "D"])
441+
XCTAssertEqual(sub.numbers, [3, 4])
442+
}
443+
}
444+
}

Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,22 @@ fileprivate struct AlmostAllArguments: ParsableArguments {
4444
@Argument(help: "", transform: { _ in 0 }) var d4: Int?
4545
@Argument(default: 0, transform: { _ in 0 }) var d5: Int?
4646

47-
@Argument(parsing: .remaining, help: "") var e: [Int]
48-
@Argument() var e0: [Int]
49-
@Argument(help: "") var e1: [Int]
50-
@Argument(parsing: .remaining) var e2: [Int]
51-
@Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e3: [Int]
52-
@Argument(transform: { _ in 0 }) var e4: [Int]
53-
@Argument(help: "", transform: { _ in 0 }) var e5: [Int]
54-
@Argument(parsing: .remaining, transform: { _ in 0 }) var e6: [Int]
47+
@Argument(default: [1, 2], parsing: .remaining, help: "") var e: [Int]
48+
@Argument(parsing: .remaining, help: "") var e1: [Int]
49+
@Argument(default: [1, 2], parsing: .remaining) var e2: [Int]
50+
@Argument(default: [1, 2], help: "") var e3: [Int]
51+
@Argument() var e4: [Int]
52+
@Argument(help: "") var e5: [Int]
53+
@Argument(parsing: .remaining) var e6: [Int]
54+
@Argument(default: [1, 2]) var e7: [Int]
55+
@Argument(default: [1, 2], parsing: .remaining, help: "", transform: { _ in 0 }) var e8: [Int]
56+
@Argument(parsing: .remaining, help: "", transform: { _ in 0 }) var e9: [Int]
57+
@Argument(default: [1, 2], parsing: .remaining, transform: { _ in 0 }) var e10: [Int]
58+
@Argument(default: [1, 2], help: "", transform: { _ in 0 }) var e11: [Int]
59+
@Argument(transform: { _ in 0 }) var e12: [Int]
60+
@Argument(help: "", transform: { _ in 0 }) var e13: [Int]
61+
@Argument(parsing: .remaining, transform: { _ in 0 }) var e14: [Int]
62+
@Argument(default: [1, 2], transform: { _ in 0 }) var e15: [Int]
5563
}
5664

5765
fileprivate struct AllOptions: ParsableArguments {
@@ -115,21 +123,35 @@ fileprivate struct AllOptions: ParsableArguments {
115123
@Option(parsing: .next, transform: { _ in 0 }) var d12: Int?
116124
@Option(help: "", transform: { _ in 0 }) var d13: Int?
117125

118-
@Option(name: .long, parsing: .singleValue, help: "") var e: [Int]
119-
@Option(parsing: .singleValue, help: "") var e1: [Int]
120-
@Option(name: .long, help: "") var e2: [Int]
121-
@Option(name: .long, parsing: .singleValue) var e3: [Int]
122-
@Option(name: .long) var e4: [Int]
123-
@Option(parsing: .singleValue) var e5: [Int]
124-
@Option(help: "") var e6: [Int]
125-
126-
@Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int]
127-
@Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int]
128-
@Option(name: .long, help: "", transform: { _ in 0 }) var f2: [Int]
129-
@Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f3: [Int]
130-
@Option(name: .long, transform: { _ in 0 }) var f4: [Int]
131-
@Option(parsing: .singleValue, transform: { _ in 0 }) var f5: [Int]
132-
@Option(help: "", transform: { _ in 0 }) var f6: [Int]
126+
@Option(name: .long, default: [1, 2], parsing: .singleValue, help: "") var e: [Int]
127+
@Option(default: [1, 2], parsing: .singleValue, help: "") var e1: [Int]
128+
@Option(name: .long, parsing: .singleValue, help: "") var e2: [Int]
129+
@Option(name: .long, default: [1, 2], help: "") var e3: [Int]
130+
@Option(parsing: .singleValue, help: "") var e4: [Int]
131+
@Option(default: [1, 2], help: "") var e5: [Int]
132+
@Option(default: [1, 2], parsing: .singleValue) var e6: [Int]
133+
@Option(name: .long, help: "") var e7: [Int]
134+
@Option(name: .long, parsing: .singleValue) var e8: [Int]
135+
@Option(name: .long, default: [1, 2]) var e9: [Int]
136+
@Option(name: .long) var e10: [Int]
137+
@Option(default: [1, 2]) var e11: [Int]
138+
@Option(parsing: .singleValue) var e12: [Int]
139+
@Option(help: "") var e13: [Int]
140+
141+
@Option(name: .long, default: [1, 2], parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int]
142+
@Option(default: [1, 2], parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int]
143+
@Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f2: [Int]
144+
@Option(name: .long, default: [1, 2], help: "", transform: { _ in 0 }) var f3: [Int]
145+
@Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f4: [Int]
146+
@Option(default: [1, 2], help: "", transform: { _ in 0 }) var f5: [Int]
147+
@Option(default: [1, 2], parsing: .singleValue, transform: { _ in 0 }) var f6: [Int]
148+
@Option(name: .long, help: "", transform: { _ in 0 }) var f7: [Int]
149+
@Option(name: .long, parsing: .singleValue, transform: { _ in 0 }) var f8: [Int]
150+
@Option(name: .long, default: [1, 2], transform: { _ in 0 }) var f9: [Int]
151+
@Option(name: .long, transform: { _ in 0 }) var f10: [Int]
152+
@Option(default: [1, 2], transform: { _ in 0 }) var f11: [Int]
153+
@Option(parsing: .singleValue, transform: { _ in 0 }) var f12: [Int]
154+
@Option(help: "", transform: { _ in 0 }) var f13: [Int]
133155
}
134156

135157
struct AllFlags: ParsableArguments {

Tests/ArgumentParserUnitTests/HelpGenerationTests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ extension HelpGenerationTests {
145145
@Option(default: 20, help: "Your age.")
146146
var age: Int
147147

148+
@Option(default: [7, 14], parsing: .upToNextOption, help: ArgumentHelp("Your lucky numbers.", valueName: "numbers"))
149+
var lucky: [Int]
150+
148151
@Option(default: false, help: "Whether logging is enabled.")
149152
var logging: Bool
150153

@@ -160,7 +163,7 @@ extension HelpGenerationTests {
160163

161164
func testHelpWithDefaultValues() {
162165
AssertHelp(for: D.self, equals: """
163-
USAGE: d [<occupation>] [--name <name>] [--middle-name <middle-name>] [--age <age>] [--logging <logging>] [--optional] [--required] [--degree <degree>] [--directory <directory>]
166+
USAGE: d [<occupation>] [--name <name>] [--middle-name <middle-name>] [--age <age>] [--lucky <numbers> ...] [--logging <logging>] [--optional] [--required] [--degree <degree>] [--directory <directory>]
164167
165168
ARGUMENTS:
166169
<occupation> Your occupation. (default: --)
@@ -170,6 +173,7 @@ extension HelpGenerationTests {
170173
--middle-name <middle-name>
171174
Your middle name. (default: Winston)
172175
--age <age> Your age. (default: 20)
176+
--lucky <numbers> Your lucky numbers. (default: [7, 14])
173177
--logging <logging> Whether logging is enabled. (default: false)
174178
--optional/--required Vegan diet. (default: optional)
175179
--degree <degree> Your degree. (default: bachelor)

0 commit comments

Comments
 (0)