Skip to content

Commit 8dfa177

Browse files
authored
Allow required array arguments, options, and flags (#196)
* Allow required arrays in `Option`s Un-deprecates (but changes the semantics of) an initializer for an array value type without a default, forcing the user to specify at least one value from the command line. * Allow required arrays in `Argument`s Extends the parent commit to arguments, still un-deprecating and changing the semantics of the previous initializer to force users to provide a value on the command line. * Allow required arrays in `Flag`s Extends the previous commits to flags, still un-deprecating and changing the semantics of the previous initializer to force users to provide a value on the command line. * Add default-value section to documentation
1 parent 2104b1a commit 8dfa177

File tree

5 files changed

+476
-87
lines changed

5 files changed

+476
-87
lines changed

Documentation/02 Arguments, Options, and Flags.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,46 @@ Verbosity level: 1
285285
Verbosity level: 4
286286
```
287287

288+
289+
## Specifying default values
290+
291+
You can specify default values for almost all supported argument, option, and flag types using normal property initialization syntax:
292+
293+
```swift
294+
enum CustomFlag: String, EnumerableFlag {
295+
case foo, bar, baz
296+
}
297+
298+
struct Example: ParsableCommand {
299+
@Flag
300+
var booleanFlag = false
301+
302+
@Flag
303+
var arrayFlag: [CustomFlag] = [.foo, .baz]
304+
305+
@Option
306+
var singleOption = 0
307+
308+
@Option
309+
var arrayOption = ["bar", "qux"]
310+
311+
@Argument
312+
var singleArgument = "quux"
313+
314+
@Argument
315+
var arrayArgument = ["quux", "quuz"]
316+
}
317+
```
318+
319+
This includes all of the variants of the argument types above (including `@Option(transform: ...)`, etc.), with a few notable exceptions:
320+
- `Optional`-typed values (which default to `nil` and for which a default would not make sense, as the value could never be `nil`)
321+
- `Int` flags (which are used for counting the number of times a flag is specified and therefore default to `0`)
322+
323+
If a default is not specified, the user must provide a value for that argument/option/flag or will receive an error that the value is missing.
324+
325+
You must also always specify a default of `false` for a non-optional `Bool` flag, as in the example above. This makes the behavior consistent with both normal Swift properties (which either must be explicitly initialized or optional to initialize a `struct`/`class` containing them) and the other property types.
326+
327+
288328
## Specifying a parsing strategy
289329

290330
When parsing a list of command-line inputs, `ArgumentParser` distinguishes between dash-prefixed keys and un-prefixed values. When looking for the value for a key, only an un-prefixed value will be selected by default.

Sources/ArgumentParser/Parsable Properties/Argument.swift

Lines changed: 129 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -368,57 +368,120 @@ extension Argument {
368368
)
369369
}
370370

371-
/// Creates a property that reads an array from zero or more arguments.
371+
372+
/// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic.
372373
///
373-
/// - Parameters:
374-
/// - initial: A default value to use for this property.
375-
/// - parsingStrategy: The behavior to use when parsing multiple values
376-
/// from the command-line arguments.
377-
/// - help: Information about how to use this argument.
378-
public init<Element>(
379-
wrappedValue: Value,
380-
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
381-
help: ArgumentHelp? = nil,
382-
completion: CompletionKind? = nil
374+
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
375+
private init<Element>(
376+
initial: Value?,
377+
parsingStrategy: ArgumentArrayParsingStrategy,
378+
help: ArgumentHelp?,
379+
completion: CompletionKind?
383380
)
384381
where Element: ExpressibleByArgument, Value == Array<Element>
385382
{
386383
self.init(_parsedValue: .init { key in
384+
// Assign the initial-value setter and help text for default value based on if an initial value was provided.
385+
let setInitialValue: ArgumentDefinition.Initial
386+
let helpDefaultValue: String?
387+
if let initial = initial {
388+
setInitialValue = { origin, values in
389+
values.set(initial, forKey: key, inputOrigin: origin)
390+
}
391+
helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil
392+
} else {
393+
setInitialValue = { _, _ in }
394+
helpDefaultValue = nil
395+
}
396+
387397
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
388398
var arg = ArgumentDefinition(
389399
kind: .positional,
390400
help: help,
391401
completion: completion ?? Element.defaultCompletionKind,
392402
parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput,
393403
update: .appendToArray(forType: Element.self, key: key),
394-
initial: { origin, values in
395-
values.set(wrappedValue, forKey: key, inputOrigin: origin)
396-
})
397-
arg.help.defaultValue = !wrappedValue.isEmpty ? wrappedValue.defaultValueDescription : nil
404+
initial: setInitialValue)
405+
arg.help.defaultValue = helpDefaultValue
398406
return ArgumentSet(alternatives: [arg])
399407
})
400408
}
401-
402-
/// Creates a property that reads an array from zero or more arguments,
403-
/// parsing each element with the given closure.
409+
410+
/// Creates a property that reads an array from zero or more arguments.
404411
///
405412
/// - Parameters:
406413
/// - initial: A default value to use for this property.
407414
/// - parsingStrategy: The behavior to use when parsing multiple values
408415
/// from the command-line arguments.
409416
/// - help: Information about how to use this argument.
410-
/// - transform: A closure that converts a string into this property's
411-
/// element type or throws an error.
412417
public init<Element>(
413418
wrappedValue: Value,
414419
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
415420
help: ArgumentHelp? = nil,
416-
completion: CompletionKind? = nil,
421+
completion: CompletionKind? = nil
422+
)
423+
where Element: ExpressibleByArgument, Value == Array<Element>
424+
{
425+
self.init(
426+
initial: wrappedValue,
427+
parsingStrategy: parsingStrategy,
428+
help: help,
429+
completion: completion
430+
)
431+
}
432+
433+
/// Creates a property with no default value that reads an array from zero or more arguments.
434+
///
435+
/// This method is called to initialize an array `Argument` with no default value such as:
436+
/// ```swift
437+
/// @Argument()
438+
/// var foo: [String]
439+
/// ```
440+
///
441+
/// - Parameters:
442+
/// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments.
443+
/// - help: Information about how to use this argument.
444+
public init<Element>(
445+
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
446+
help: ArgumentHelp? = nil,
447+
completion: CompletionKind? = nil
448+
)
449+
where Element: ExpressibleByArgument, Value == Array<Element>
450+
{
451+
self.init(
452+
initial: nil,
453+
parsingStrategy: parsingStrategy,
454+
help: help,
455+
completion: completion
456+
)
457+
}
458+
459+
/// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic.
460+
///
461+
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
462+
private init<Element>(
463+
initial: Value?,
464+
parsingStrategy: ArgumentArrayParsingStrategy,
465+
help: ArgumentHelp?,
466+
completion: CompletionKind?,
417467
transform: @escaping (String) throws -> Element
418468
)
419469
where Value == Array<Element>
420470
{
421471
self.init(_parsedValue: .init { key in
472+
// Assign the initial-value setter and help text for default value based on if an initial value was provided.
473+
let setInitialValue: ArgumentDefinition.Initial
474+
let helpDefaultValue: String?
475+
if let initial = initial {
476+
setInitialValue = { origin, values in
477+
values.set(initial, forKey: key, inputOrigin: origin)
478+
}
479+
helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil
480+
} else {
481+
setInitialValue = { _, _ in }
482+
helpDefaultValue = nil
483+
}
484+
422485
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
423486
var arg = ArgumentDefinition(
424487
kind: .positional,
@@ -436,32 +499,66 @@ extension Argument {
436499
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
437500
}
438501
}),
439-
initial: { origin, values in
440-
values.set(wrappedValue, forKey: key, inputOrigin: origin)
441-
})
442-
arg.help.defaultValue = !wrappedValue.isEmpty ? "\(wrappedValue)" : nil
502+
initial: setInitialValue)
503+
arg.help.defaultValue = helpDefaultValue
443504
return ArgumentSet(alternatives: [arg])
444505
})
445506
}
446-
447-
@available(*, deprecated, message: "Provide an empty array literal as a default value.")
507+
508+
/// Creates a property that reads an array from zero or more arguments,
509+
/// parsing each element with the given closure.
510+
///
511+
/// - Parameters:
512+
/// - initial: A default value to use for this property.
513+
/// - parsingStrategy: The behavior to use when parsing multiple values
514+
/// from the command-line arguments.
515+
/// - help: Information about how to use this argument.
516+
/// - transform: A closure that converts a string into this property's
517+
/// element type or throws an error.
448518
public init<Element>(
519+
wrappedValue: Value,
449520
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
450-
help: ArgumentHelp? = nil
521+
help: ArgumentHelp? = nil,
522+
completion: CompletionKind? = nil,
523+
transform: @escaping (String) throws -> Element
451524
)
452-
where Element: ExpressibleByArgument, Value == Array<Element>
525+
where Value == Array<Element>
453526
{
454-
self.init(wrappedValue: [], parsing: parsingStrategy, help: help)
527+
self.init(
528+
initial: wrappedValue,
529+
parsingStrategy: parsingStrategy,
530+
help: help,
531+
completion: completion,
532+
transform: transform
533+
)
455534
}
456535

457-
@available(*, deprecated, message: "Provide an empty array literal as a default value.")
536+
/// Creates a property with no default value that reads an array from zero or more arguments, parsing each element with the given closure.
537+
///
538+
/// This method is called to initialize an array `Argument` with no default value such as:
539+
/// ```swift
540+
/// @Argument(tranform: baz)
541+
/// var foo: [String]
542+
/// ```
543+
///
544+
/// - Parameters:
545+
/// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments.
546+
/// - help: Information about how to use this argument.
547+
/// - transform: A closure that converts a string into this property's element type or throws an error.
458548
public init<Element>(
459549
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
460550
help: ArgumentHelp? = nil,
551+
completion: CompletionKind? = nil,
461552
transform: @escaping (String) throws -> Element
462553
)
463554
where Value == Array<Element>
464555
{
465-
self.init(wrappedValue: [], parsing: parsingStrategy, help: help, transform: transform)
556+
self.init(
557+
initial: nil,
558+
parsingStrategy: parsingStrategy,
559+
help: help,
560+
completion: completion,
561+
transform: transform
562+
)
466563
}
467564
}

Sources/ArgumentParser/Parsable Properties/Flag.swift

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -530,18 +530,12 @@ extension Flag {
530530
: ArgumentSet(additive: args)
531531
})
532532
}
533-
534-
/// Creates an array property that gets its values from the presence of
535-
/// zero or more flags, where the allowed flags are defined by an
536-
/// `EnumerableFlag` type.
537-
///
538-
/// This property has an empty array as its default value.
533+
534+
/// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic.
539535
///
540-
/// - Parameters:
541-
/// - name: A specification for what names are allowed for this flag.
542-
/// - help: Information about how to use this flag.
543-
public init<Element>(
544-
wrappedValue: [Element],
536+
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
537+
private init<Element>(
538+
initial: [Element]?,
545539
help: ArgumentHelp? = nil
546540
) where Value == Array<Element>, Element: EnumerableFlag {
547541
self.init(_parsedValue: .init { key in
@@ -553,7 +547,7 @@ extension Flag {
553547
let name = Element.name(for: value)
554548
let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help
555549
let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp)
556-
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: wrappedValue, update: .nullary({ (origin, name, values) in
550+
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in
557551
values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
558552
$0.append(value)
559553
})
@@ -562,12 +556,43 @@ extension Flag {
562556
return ArgumentSet(additive: args)
563557
})
564558
}
565-
566-
@available(*, deprecated, message: "Provide an empty array literal as a default value.")
559+
560+
/// Creates an array property that gets its values from the presence of
561+
/// zero or more flags, where the allowed flags are defined by an
562+
/// `EnumerableFlag` type.
563+
///
564+
/// This property has an empty array as its default value.
565+
///
566+
/// - Parameters:
567+
/// - name: A specification for what names are allowed for this flag.
568+
/// - help: Information about how to use this flag.
569+
public init<Element>(
570+
wrappedValue: [Element],
571+
help: ArgumentHelp? = nil
572+
) where Value == Array<Element>, Element: EnumerableFlag {
573+
self.init(
574+
initial: wrappedValue,
575+
help: help
576+
)
577+
}
578+
579+
/// Creates an array property with no default value that gets its values from the presence of zero or more flags, where the allowed flags are defined by an `EnumerableFlag` type.
580+
///
581+
/// This method is called to initialize an array `Flag` with no default value such as:
582+
/// ```swift
583+
/// @Flag
584+
/// var foo: [CustomFlagType]
585+
/// ```
586+
///
587+
/// - Parameters:
588+
/// - help: Information about how to use this flag.
567589
public init<Element>(
568590
help: ArgumentHelp? = nil
569591
) where Value == Array<Element>, Element: EnumerableFlag {
570-
self.init(wrappedValue: [], help: help)
592+
self.init(
593+
initial: nil,
594+
help: help
595+
)
571596
}
572597
}
573598

0 commit comments

Comments
 (0)