Skip to content

Commit

Permalink
Allow required array arguments, options, and flags (apple#196)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
MPLew-is authored Aug 14, 2020
1 parent 2104b1a commit 8dfa177
Show file tree
Hide file tree
Showing 5 changed files with 476 additions and 87 deletions.
40 changes: 40 additions & 0 deletions Documentation/02 Arguments, Options, and Flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,46 @@ Verbosity level: 1
Verbosity level: 4
```


## Specifying default values

You can specify default values for almost all supported argument, option, and flag types using normal property initialization syntax:

```swift
enum CustomFlag: String, EnumerableFlag {
case foo, bar, baz
}

struct Example: ParsableCommand {
@Flag
var booleanFlag = false

@Flag
var arrayFlag: [CustomFlag] = [.foo, .baz]

@Option
var singleOption = 0

@Option
var arrayOption = ["bar", "qux"]

@Argument
var singleArgument = "quux"

@Argument
var arrayArgument = ["quux", "quuz"]
}
```

This includes all of the variants of the argument types above (including `@Option(transform: ...)`, etc.), with a few notable exceptions:
- `Optional`-typed values (which default to `nil` and for which a default would not make sense, as the value could never be `nil`)
- `Int` flags (which are used for counting the number of times a flag is specified and therefore default to `0`)

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.

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.


## Specifying a parsing strategy

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.
Expand Down
161 changes: 129 additions & 32 deletions Sources/ArgumentParser/Parsable Properties/Argument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -368,57 +368,120 @@ extension Argument {
)
}

/// Creates a property that reads an array from zero or more arguments.

/// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic.
///
/// - 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<Element>(
wrappedValue: Value,
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
private init<Element>(
initial: Value?,
parsingStrategy: ArgumentArrayParsingStrategy,
help: ArgumentHelp?,
completion: CompletionKind?
)
where Element: ExpressibleByArgument, Value == Array<Element>
{
self.init(_parsedValue: .init { key in
// Assign the initial-value setter and help text for default value based on if an initial value was provided.
let setInitialValue: ArgumentDefinition.Initial
let helpDefaultValue: String?
if let initial = initial {
setInitialValue = { origin, values in
values.set(initial, forKey: key, inputOrigin: origin)
}
helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil
} else {
setInitialValue = { _, _ in }
helpDefaultValue = nil
}

let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
var arg = ArgumentDefinition(
kind: .positional,
help: help,
completion: completion ?? Element.defaultCompletionKind,
parsingStrategy: parsingStrategy == .remaining ? .nextAsValue : .allRemainingInput,
update: .appendToArray(forType: Element.self, key: key),
initial: { origin, values in
values.set(wrappedValue, forKey: key, inputOrigin: origin)
})
arg.help.defaultValue = !wrappedValue.isEmpty ? wrappedValue.defaultValueDescription : nil
initial: setInitialValue)
arg.help.defaultValue = helpDefaultValue
return ArgumentSet(alternatives: [arg])
})
}

/// Creates a property that reads an array from zero or more arguments,
/// parsing each element with the given closure.

/// Creates a property that reads an array from zero or more arguments.
///
/// - 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<Element>(
wrappedValue: Value,
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
completion: CompletionKind? = nil
)
where Element: ExpressibleByArgument, Value == Array<Element>
{
self.init(
initial: wrappedValue,
parsingStrategy: parsingStrategy,
help: help,
completion: completion
)
}

/// Creates a property with no default value that reads an array from zero or more arguments.
///
/// This method is called to initialize an array `Argument` with no default value such as:
/// ```swift
/// @Argument()
/// var foo: [String]
/// ```
///
/// - Parameters:
/// - parsingStrategy: The behavior to use when parsing multiple values from the command-line arguments.
/// - help: Information about how to use this argument.
public init<Element>(
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
)
where Element: ExpressibleByArgument, Value == Array<Element>
{
self.init(
initial: nil,
parsingStrategy: parsingStrategy,
help: help,
completion: completion
)
}

/// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic.
///
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
private init<Element>(
initial: Value?,
parsingStrategy: ArgumentArrayParsingStrategy,
help: ArgumentHelp?,
completion: CompletionKind?,
transform: @escaping (String) throws -> Element
)
where Value == Array<Element>
{
self.init(_parsedValue: .init { key in
// Assign the initial-value setter and help text for default value based on if an initial value was provided.
let setInitialValue: ArgumentDefinition.Initial
let helpDefaultValue: String?
if let initial = initial {
setInitialValue = { origin, values in
values.set(initial, forKey: key, inputOrigin: origin)
}
helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil
} else {
setInitialValue = { _, _ in }
helpDefaultValue = nil
}

let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
var arg = ArgumentDefinition(
kind: .positional,
Expand All @@ -436,32 +499,66 @@ extension Argument {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
}),
initial: { origin, values in
values.set(wrappedValue, forKey: key, inputOrigin: origin)
})
arg.help.defaultValue = !wrappedValue.isEmpty ? "\(wrappedValue)" : nil
initial: setInitialValue)
arg.help.defaultValue = helpDefaultValue
return ArgumentSet(alternatives: [arg])
})
}

@available(*, deprecated, message: "Provide an empty array literal as a default value.")

/// Creates a property that reads an array from zero or more arguments,
/// parsing each element with the given closure.
///
/// - 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<Element>(
wrappedValue: Value,
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @escaping (String) throws -> Element
)
where Element: ExpressibleByArgument, Value == Array<Element>
where Value == Array<Element>
{
self.init(wrappedValue: [], parsing: parsingStrategy, help: help)
self.init(
initial: wrappedValue,
parsingStrategy: parsingStrategy,
help: help,
completion: completion,
transform: transform
)
}

@available(*, deprecated, message: "Provide an empty array literal as a default value.")
/// Creates a property with no default value that reads an array from zero or more arguments, parsing each element with the given closure.
///
/// This method is called to initialize an array `Argument` with no default value such as:
/// ```swift
/// @Argument(tranform: baz)
/// var foo: [String]
/// ```
///
/// - Parameters:
/// - 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<Element>(
parsing parsingStrategy: ArgumentArrayParsingStrategy = .remaining,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @escaping (String) throws -> Element
)
where Value == Array<Element>
{
self.init(wrappedValue: [], parsing: parsingStrategy, help: help, transform: transform)
self.init(
initial: nil,
parsingStrategy: parsingStrategy,
help: help,
completion: completion,
transform: transform
)
}
}
55 changes: 40 additions & 15 deletions Sources/ArgumentParser/Parsable Properties/Flag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -530,18 +530,12 @@ extension Flag {
: ArgumentSet(additive: args)
})
}

/// Creates an array property that gets its values from the presence of
/// zero or more flags, where the allowed flags are defined by an
/// `EnumerableFlag` type.
///
/// This property has an empty array as its default value.

/// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - help: Information about how to use this flag.
public init<Element>(
wrappedValue: [Element],
/// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication.
private init<Element>(
initial: [Element]?,
help: ArgumentHelp? = nil
) where Value == Array<Element>, Element: EnumerableFlag {
self.init(_parsedValue: .init { key in
Expand All @@ -553,7 +547,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: wrappedValue, update: .nullary({ (origin, name, values) in
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in
values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
$0.append(value)
})
Expand All @@ -562,12 +556,43 @@ extension Flag {
return ArgumentSet(additive: args)
})
}

@available(*, deprecated, message: "Provide an empty array literal as a default value.")

/// Creates an array property that gets its values from the presence of
/// zero or more flags, where the allowed flags are defined by an
/// `EnumerableFlag` type.
///
/// This property has an empty array as its default value.
///
/// - Parameters:
/// - name: A specification for what names are allowed for this flag.
/// - help: Information about how to use this flag.
public init<Element>(
wrappedValue: [Element],
help: ArgumentHelp? = nil
) where Value == Array<Element>, Element: EnumerableFlag {
self.init(
initial: wrappedValue,
help: help
)
}

/// 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.
///
/// This method is called to initialize an array `Flag` with no default value such as:
/// ```swift
/// @Flag
/// var foo: [CustomFlagType]
/// ```
///
/// - Parameters:
/// - help: Information about how to use this flag.
public init<Element>(
help: ArgumentHelp? = nil
) where Value == Array<Element>, Element: EnumerableFlag {
self.init(wrappedValue: [], help: help)
self.init(
initial: nil,
help: help
)
}
}

Expand Down
Loading

0 comments on commit 8dfa177

Please sign in to comment.