Skip to content

Allow @Options with tuple types #691

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ extension ArgumentDefinition {

let inputs: String
switch update {
case .unary:
case .unary, .tuplary:
inputs = ":\(valueName):\(zshActionString(commands))"
case .nullary:
inputs = ""
Expand Down
142 changes: 142 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -861,3 +861,145 @@ extension Option {
})
}
}

// MARK: - @Option tuples

extension Option {
private init<
First: ExpressibleByArgument,
Second: ExpressibleByArgument,
each Rest: ExpressibleByArgument
>(
wrappedValue: Value?,
name: NameSpecification,
parsing parsingStrategy: SingleValueParsingStrategy,
help: ArgumentHelp?,
completion: CompletionKind?
) where Value == (First, Second, repeat each Rest) {
self.init(_parsedValue: .init { key in
let arg = ArgumentDefinition.tupleOption(
key: key,
name: name,
parsingStrategy: parsingStrategy,
help: help,
completion: completion,
valueCount: 2 + packCount(repeat (each Rest).self),
update: { origin, name, values, parsedValues in
switch initVariadicValues(First.self, Second.self, repeat (each Rest).self, with: values) {
case .success(let value):
parsedValues.set(value, forKey: key, inputOrigin: origin)
case .failure(let error):
throw ParserError.unableToParseValue(
origin, name, values[error.index], forKey: key, originalError: nil)
}
},
initial: { origin, parsedValues in
parsedValues.set(wrappedValue, forKey: key, inputOrigin: origin)
})
return ArgumentSet(arg)
})
}

/// Creates a labeled option with multiple values.
///
/// Use this `@Option` property wrapper when you have a value that requires
/// more than one input for a key. For example, you could use this property
/// wrapper to capture the three dimensions for a package:
///
/// struct Package: ParsableArguments {
/// @Option
/// var dimensions: (Double, Double, Double)
/// }
///
/// A user would then specify the three dimensions after the `--dimensions`
/// key:
///
/// $ package --dimensions 5 4 12
///
/// - Parameters:
/// - name: A specification for what names are allowed for this option.
/// - parsingStrategy: The behavior to use when parsing the elements for
/// this option.
/// - help: Information about how to use this option.
/// - completion: The type of command-line completion provided for this
/// option.
public init<
First: ExpressibleByArgument,
Second: ExpressibleByArgument,
each Rest: ExpressibleByArgument
>(
name: NameSpecification = .long,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where Value == (First, Second, repeat each Rest) {
self.init(
wrappedValue: nil,
name: name,
parsing: parsingStrategy,
help: help,
completion: completion
)
}

public init<
First: ExpressibleByArgument,
Second: ExpressibleByArgument,
each Rest: ExpressibleByArgument
>(
wrappedValue: (First, Second, repeat each Rest),
name: NameSpecification = .long,
parsing parsingStrategy: SingleValueParsingStrategy = .next,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where Value == (First, Second, repeat each Rest) {
self.init(
// This version is okay:
wrappedValue: .some(wrappedValue),
// This version crashes:
// wrappedValue: wrappedValue,
name: name,
parsing: parsingStrategy,
help: help,
completion: completion
)
}
}

// MARK: Variadic tuple support

fileprivate struct InitFailure: Error {
var index: Int
}

/// Returns a variadic tuple, with values generated by zipping the provided
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

variadic generic code is damn near impossible to read...

/// variadic type arguments with the array of strings.
///
/// Each string in `arr` is used to generate one value, and the whole `each T`
/// group is either returned, or a failure is diagnosed by the first value
/// where `init(argument:)` fails.
fileprivate func initVariadicValues<each T: ExpressibleByArgument>(
_ elem: repeat (each T).Type, with arr: [String]
) -> Result<(repeat each T), InitFailure> {
var arr = arr[...]
func pairYs<V: ExpressibleByArgument>(_ v: V.Type) throws -> V {
guard let value = V.init(argument: arr.popFirst()!) else {
throw InitFailure(index: arr.startIndex - 1)
}
return value
}

do {
return .success(try (repeat pairYs(each elem)))
} catch {
return .failure(error as! InitFailure)
}
}

/// Returns the number of elements in `each T`.
fileprivate func packCount<each T>(_ el: repeat each T) -> Int {
var count = 0
func increment<U>(_ u: U) { count += 1 }
repeat (increment(each el))
return count
}
65 changes: 58 additions & 7 deletions Sources/ArgumentParser/Parsing/ArgumentDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ struct ArgumentDefinition {
enum Update {
typealias Nullary = (InputOrigin, Name?, inout ParsedValues) throws -> Void
typealias Unary = (InputOrigin, Name?, String, inout ParsedValues) throws -> Void

typealias Tuplary = (InputOrigin, Name?, [String], inout ParsedValues) throws -> Void

/// An argument that gets its value solely from its presence.
case nullary(Nullary)

/// An argument that takes a string as its value.
case unary(Unary)

/// An argument that takes two or more strings to create its value.
case tuplary(Int, Tuplary)
}

typealias Initial = (InputOrigin, inout ParsedValues) throws -> Void
Expand All @@ -43,6 +47,7 @@ struct ArgumentDefinition {

static let isOptional = Options(rawValue: 1 << 0)
static let isRepeating = Options(rawValue: 1 << 1)
static let isComposite = Options(rawValue: 1 << 2)
}

var options: Options
Expand Down Expand Up @@ -113,14 +118,33 @@ struct ArgumentDefinition {
}
}

var preferredValueName: String {
names.preferredName?.valueString
?? help.keys.first?.name.convertedToSnakeCase(separator: "-")
?? "value"
}

var valueName: String {
help.valueName.mapEmpty {
names.preferredName?.valueString
?? help.keys.first?.name.convertedToSnakeCase(separator: "-")
?? "value"
preferredValueName
}
}

var formattedValueName: String {
let defaultName = valueName

switch update {
case .tuplary(let count, _):
let parts = defaultName.split(separator: " ").prefix(count)
let missingCount = count - parts.count
let missingParts: [Substring] = zip((parts.count + 1)..., repeatElement(preferredValueName[...], count: missingCount))
.map { "\($1)-\($0)" }
return (parts + missingParts).map { "<\($0)>" }.joined(separator: " ")
default:
return "<\(defaultName)>"
}
}

init(
kind: Kind,
help: Help,
Expand Down Expand Up @@ -149,13 +173,13 @@ extension ArgumentDefinition: CustomDebugStringConvertible {
return names
.map { $0.synopsisString }
.joined(separator: ",")
case (.named(let names), .unary):
case (.named(let names), .unary), (.named(let names), .tuplary):
return names
.map { $0.synopsisString }
.joined(separator: ",")
+ " <\(valueName)>"
+ " \(formattedValueName)"
case (.positional, _):
return "<\(valueName)>"
return formattedValueName
case (.default, _):
return ""
}
Expand Down Expand Up @@ -334,6 +358,33 @@ extension ArgumentDefinition {
values.set(initial, forKey: key, inputOrigin: inputOrigin)
})
}

static func tupleOption(
key: InputKey,
name: NameSpecification,
parsingStrategy: SingleValueParsingStrategy,
help: ArgumentHelp?,
completion: CompletionKind?,
valueCount: Int,
update: @escaping Update.Tuplary,
initial: @escaping Initial = { _, _ in }
) -> Self {
var def = ArgumentDefinition(
kind: .name(key: key, specification: name),
help: .init(
allValueStrings: [],
options: [],
help: help,
defaultValue: nil,
key: key,
isComposite: false),
completion: completion ?? .default,
parsingStrategy: parsingStrategy.base,
update: .tuplary(valueCount, update),
initial: initial)
def.help.options.insert(.isComposite)
return def
}
}

// MARK: - Abstraction over T, Option<T>, Array<T>
Expand Down
64 changes: 64 additions & 0 deletions Sources/ArgumentParser/Parsing/ArgumentSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,68 @@ struct LenientParser {
}
}

mutating func parseTuplaryValue(
_ argument: ArgumentDefinition,
_ parsed: ParsedArgument,
_ valueCount: Int,
_ originElement: InputOrigin.Element,
_ update: ArgumentDefinition.Update.Tuplary,
_ result: inout ParsedValues,
_ usedOrigins: inout InputOrigin
) throws {
var origins = InputOrigin(elements: [originElement])
var values: [String] = []

// Use an attached value if it exists...
if let value = parsed.value {
values.append(value)
origins.insert(originElement)
} else if argument.allowsJoinedValue,
let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) {
// Found a joined argument
origins.insert(origin2)
values.append(String(value))
}

// ...and then consume the arguments until hitting an option
switch argument.parsingStrategy {
case .default:
while let (origin2, value) = inputArguments.popNextElementIfValue(),
values.count < valueCount
{
origins.insert(origin2)
values.append(value)
}
case .scanningForValue:
while let (origin2, value) = inputArguments.popNextValue(after: originElement),
values.count < valueCount
{
origins.insert(origin2)
values.append(value)
}

case .unconditional:
while let (origin2, value) = inputArguments.popNextElementAsValue(after: originElement),
values.count < valueCount
{
origins.insert(origin2)
values.append(value)
}

case .upToNextOption, .allRemainingInput, .postTerminator, .allUnrecognized:
fatalError()
}


guard valueCount == values.count else {
throw ParserError.insufficientValuesForOption(
origins, parsed.name, expected: valueCount, given: values.count)
}

try update(origins, parsed.name, values, &result)
usedOrigins.formUnion(origins)
}

mutating func parsePositionalValues(
from unusedInput: SplitArguments,
into result: inout ParsedValues
Expand Down Expand Up @@ -539,6 +601,8 @@ struct LenientParser {
usedOrigins.insert(origin)
case let .unary(update):
try parseValue(argument, parsed, origin, update, &result, &usedOrigins)
case let .tuplary(count, update):
try parseTuplaryValue(argument, parsed, count, origin, update, &result, &usedOrigins)
}
case .terminator:
// Ignore the terminator, it might get picked up as a positional value later.
Expand Down
1 change: 1 addition & 0 deletions Sources/ArgumentParser/Parsing/ParserError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum ParserError: Error {
case nonAlphanumericShortOption(Character)
/// The option was there, but its value is missing, e.g. `--name` but no value for the `name`.
case missingValueForOption(InputOrigin, Name)
case insufficientValuesForOption(InputOrigin, Name, expected: Int, given: Int)
case unexpectedValueForOption(InputOrigin.Element, Name, String)
case unexpectedExtraValues([(InputOrigin, String)])
case duplicateExclusiveValues(previous: InputOrigin, duplicate: InputOrigin, originalInput: [String])
Expand Down
2 changes: 1 addition & 1 deletion Sources/ArgumentParser/Usage/DumpHelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ fileprivate extension ArgumentInfoV0.KindV0 {
switch argument.update {
case .nullary:
self = .flag
case .unary:
case .unary, .tuplary:
self = .option
}
case .positional:
Expand Down
Loading