Skip to content

Allow required array arguments, options, and flags #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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