Skip to content
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

Optional arrays #317

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/Argument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,14 @@ extension Argument {
let help = ArgumentDefinition.Help(options: [], help: help, key: key)
let arg = ArgumentDefinition(kind: .positional, help: help, completion: completion ?? .default, update: .unary({
(origin, name, valueString, parsedValues) in
guard let valueString = valueString else { return false }
do {
let transformedValue = try transform(valueString)
parsedValues.set(transformedValue, forKey: key, inputOrigin: origin)
} catch {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
return true
}), initial: { origin, values in
if let v = initial {
values.set(v, forKey: key, inputOrigin: origin)
Expand Down Expand Up @@ -412,6 +414,7 @@ extension Argument {
parsingStrategy: parsingStrategy.base,
update: .unary({
(origin, name, valueString, parsedValues) in
guard let valueString = valueString else { return false }
do {
let transformedElement = try transform(valueString)
parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
Expand All @@ -420,6 +423,7 @@ extension Argument {
} catch {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
return true
}),
initial: setInitialValue)
arg.help.defaultValue = helpDefaultValue
Expand Down
155 changes: 155 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,14 @@ extension Option {
let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, key: key)
var arg = ArgumentDefinition(kind: kind, help: help, completion: completion ?? .default, parsingStrategy: parsingStrategy.base, update: .unary({
(origin, name, valueString, parsedValues) in
guard let valueString = valueString else { return false }
do {
let transformedValue = try transform(valueString)
parsedValues.set(transformedValue, forKey: key, inputOrigin: origin)
} catch {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
return true
}), initial: { origin, values in
if let v = initial {
values.set(v, forKey: key, inputOrigin: origin)
Expand Down Expand Up @@ -456,6 +458,45 @@ extension Option {
})
}

/// Creates an array property with an optional 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: [Element]?,
name: NameSpecification,
parsingStrategy: ArrayParsingStrategy,
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 kind = ArgumentDefinition.Kind.name(key: key, specification: name)
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
var arg = ArgumentDefinition(
kind: kind,
help: help,
completion: completion ?? Element.defaultCompletionKind,
parsingStrategy: parsingStrategy.base,
update: .appendToArray(forType: Element.self, key: key),
initial: setInitialValue
)
arg.help.defaultValue = helpDefaultValue
return ArgumentSet(arg)
})
}

/// Creates an array property that reads its values from zero or more
/// labeled options.
///
Expand Down Expand Up @@ -508,6 +549,29 @@ extension Option {
)
}

/// Creates an optional array property that reads its values from zero or more
/// labeled options.
///
/// - 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<Element>(
name: NameSpecification = .long,
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where Element: ExpressibleByArgument, Value == Array<Element>? {
self.init(
initial: nil,
name: name,
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.
///
Expand Down Expand Up @@ -542,6 +606,64 @@ extension Option {
completion: completion ?? .default,
parsingStrategy: parsingStrategy.base,
update: .unary({ (origin, name, valueString, parsedValues) in
// First of all we need to create an empty array.
parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element]()) { _ in }
// Returns true anyway, because we always create an empty array above.
guard let valueString = valueString else { return true }
do {
let transformedElement = try transform(valueString)
parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
$0.append(transformedElement)
})
} catch {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
return true
}),
initial: setInitialValue
)
arg.help.defaultValue = helpDefaultValue
return ArgumentSet(arg)
})
}

/// 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: [Element]?,
name: NameSpecification,
parsingStrategy: ArrayParsingStrategy,
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 kind = ArgumentDefinition.Kind.name(key: key, specification: name)
let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key)
var arg = ArgumentDefinition(
kind: kind,
help: help,
completion: completion ?? .default,
parsingStrategy: parsingStrategy.base,
update: .unary({ (origin, name, valueString, parsedValues) in
// First of all we need to create an empty array.
parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element]()) { _ in }
// Returns true anyway, because we always create an empty array above.
guard let valueString = valueString else { return true }
do {
let transformedElement = try transform(valueString)
parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
Expand All @@ -550,6 +672,7 @@ extension Option {
} catch {
throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error)
}
return true
}),
initial: setInitialValue
)
Expand Down Expand Up @@ -591,6 +714,38 @@ extension Option {
)
}

/// Creates an optional array property that reads its values from zero or more
/// labeled options, parsing with the given closure.
///
/// 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.
/// - transform: A closure that converts a string into this property's
/// element type or throws an error.
public init<Element>(
name: NameSpecification = .long,
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @escaping (String) throws -> Element
) where Value == Array<Element>? {
self.init(
initial: nil,
name: name,
parsingStrategy: parsingStrategy,
help: help,
completion: completion,
transform: transform
)
}

/// Creates an array property with no default value that reads its values from zero or more labeled options, parsing each element with the given closure.
///
/// This method is called to initialize an array `Option` with no default value such as:
Expand Down
13 changes: 10 additions & 3 deletions Sources/ArgumentParser/Parsing/ArgumentDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ struct ArgumentDefinition {
/// argument's value.
enum Update {
typealias Nullary = (InputOrigin, Name?, inout ParsedValues) throws -> Void
typealias Unary = (InputOrigin, Name?, String, inout ParsedValues) throws -> Void
/// - Returns: true if value was updated, otherwise false
typealias Unary = (InputOrigin, Name?, String?, inout ParsedValues) throws -> Bool

/// An argument that gets its value solely from its presence.
case nullary(Nullary)
Expand Down Expand Up @@ -199,12 +200,18 @@ extension ArgumentDefinition.Update {
static func appendToArray<A: ExpressibleByArgument>(forType type: A.Type, key: InputKey) -> ArgumentDefinition.Update {
return ArgumentDefinition.Update.unary {
(origin, name, value, values) in
// First of all we need to create an empty array.
values.update(forKey: key, inputOrigin: origin, initial: [A]()) { _ in }
// Returns true anyway, because we always create an empty array above.
guard let value = value else { return true }

guard let v = A(argument: value) else {
throw ParserError.unableToParseValue(origin, name, value, forKey: key)
}
values.update(forKey: key, inputOrigin: origin, initial: [A](), closure: {
values.update(forKey: key, inputOrigin: origin, initial: [A]()) {
$0.append(v)
})
}
return true
}
}
}
Expand Down