Skip to content

Commit f44210e

Browse files
author
Johannes Weiss
committed
Support for ordered, heterogeneous repeating options
1 parent 1e77425 commit f44210e

File tree

3 files changed

+380
-0
lines changed

3 files changed

+380
-0
lines changed

Sources/ArgumentParser/Parsable Properties/Option.swift

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,105 @@ extension Option {
454454
}
455455
}
456456

457+
// MARK: - @Option T Initializers (name-aware transform)
458+
extension Option {
459+
/// Creates a property with a default value that reads its value from a
460+
/// labeled option, parsing with a closure that receives the matched option
461+
/// name along with the value string.
462+
///
463+
/// This initializer is useful when a single `@Option` property has multiple
464+
/// names and the transform needs to know which name was used. For example:
465+
///
466+
/// ```swift
467+
/// @Option(
468+
/// name: [.customLong("resize"), .customLong("crop")],
469+
/// transformIncludingName: { name, value in
470+
/// switch name {
471+
/// case "resize": return .resize(value)
472+
/// case "crop": return .crop(value)
473+
/// default: fatalError()
474+
/// }
475+
/// }
476+
/// ) var operations: [Operation] = []
477+
/// ```
478+
///
479+
/// - Parameters:
480+
/// - wrappedValue: The default value to use for this property, provided
481+
/// implicitly by the compiler during property wrapper initialization.
482+
/// - name: A specification for what names are allowed for this option.
483+
/// - parsingStrategy: The behavior to use when looking for this option's
484+
/// value.
485+
/// - help: Information about how to use this option.
486+
/// - completion: The type of command-line completion provided for this
487+
/// option.
488+
/// - transformIncludingName: A closure that receives the matched option
489+
/// name (without leading dashes) and the value string, and returns this
490+
/// property's type or throws an error.
491+
@preconcurrency
492+
public init(
493+
wrappedValue: Value,
494+
name: NameSpecification = .long,
495+
parsing parsingStrategy: SingleValueParsingStrategy = .next,
496+
help: ArgumentHelp? = nil,
497+
completion: CompletionKind? = nil,
498+
transformIncludingName: @Sendable @escaping (_ optionName: String, _ value: String) throws -> Value
499+
) {
500+
self.init(
501+
_parsedValue: .init { key in
502+
let arg = ArgumentDefinition(
503+
container: Bare<Value>.self,
504+
key: key,
505+
kind: .name(key: key, specification: name),
506+
help: help,
507+
parsingStrategy: parsingStrategy.base,
508+
transformIncludingName: transformIncludingName,
509+
initial: wrappedValue,
510+
completion: completion)
511+
512+
return ArgumentSet(arg)
513+
})
514+
}
515+
516+
/// Creates a required property that reads its value from a labeled option,
517+
/// parsing with a closure that receives the matched option name along with
518+
/// the value string.
519+
///
520+
/// - Parameters:
521+
/// - name: A specification for what names are allowed for this option.
522+
/// - parsingStrategy: The behavior to use when looking for this option's
523+
/// value.
524+
/// - help: Information about how to use this option.
525+
/// - completion: The type of command-line completion provided for this
526+
/// option.
527+
/// - transformIncludingName: A closure that receives the matched option
528+
/// name (without leading dashes) and the value string, and returns this
529+
/// property's type or throws an error.
530+
@preconcurrency
531+
@_disfavoredOverload
532+
public init(
533+
name: NameSpecification = .long,
534+
parsing parsingStrategy: SingleValueParsingStrategy = .next,
535+
help: ArgumentHelp? = nil,
536+
completion: CompletionKind? = nil,
537+
transformIncludingName: @Sendable @escaping (_ optionName: String, _ value: String) throws -> Value
538+
) {
539+
self.init(
540+
_parsedValue: .init { key in
541+
let arg = ArgumentDefinition(
542+
container: Bare<Value>.self,
543+
key: key,
544+
kind: .name(key: key, specification: name),
545+
help: help,
546+
parsingStrategy: parsingStrategy.base,
547+
transformIncludingName: transformIncludingName,
548+
initial: nil,
549+
completion: completion)
550+
551+
return ArgumentSet(arg)
552+
})
553+
}
554+
}
555+
457556
// MARK: - @Option Optional<T: ExpressibleByArgument> Initializers
458557
extension Option {
459558
/// Creates an optional property that reads its value from a labeled option,
@@ -911,3 +1010,184 @@ extension Option {
9111010
})
9121011
}
9131012
}
1013+
1014+
// MARK: - @Option Optional<T> Initializers (name-aware transform)
1015+
extension Option {
1016+
/// Creates an optional property that reads its value from a labeled option,
1017+
/// parsing with a closure that receives the matched option name, with an
1018+
/// explicit `nil` default.
1019+
///
1020+
/// - Parameters:
1021+
/// - wrappedValue: A default value to use for this property, provided
1022+
/// implicitly by the compiler during property wrapper initialization.
1023+
/// - name: A specification for what names are allowed for this option.
1024+
/// - parsingStrategy: The behavior to use when looking for this option's
1025+
/// value.
1026+
/// - help: Information about how to use this option.
1027+
/// - completion: The type of command-line completion provided for this
1028+
/// option.
1029+
/// - transformIncludingName: A closure that receives the matched option
1030+
/// name (without leading dashes) and the value string, and returns this
1031+
/// property's type or throws an error.
1032+
@preconcurrency
1033+
public init<T>(
1034+
wrappedValue: _OptionalNilComparisonType,
1035+
name: NameSpecification = .long,
1036+
parsing parsingStrategy: SingleValueParsingStrategy = .next,
1037+
help: ArgumentHelp? = nil,
1038+
completion: CompletionKind? = nil,
1039+
transformIncludingName: @Sendable @escaping (_ optionName: String, _ value: String) throws -> T
1040+
) where Value == T? {
1041+
self.init(
1042+
_parsedValue: .init { key in
1043+
let arg = ArgumentDefinition(
1044+
container: Optional<T>.self,
1045+
key: key,
1046+
kind: .name(key: key, specification: name),
1047+
help: help,
1048+
parsingStrategy: parsingStrategy.base,
1049+
transformIncludingName: transformIncludingName,
1050+
initial: nil,
1051+
completion: completion)
1052+
1053+
return ArgumentSet(arg)
1054+
})
1055+
}
1056+
1057+
/// Creates an optional property that reads its value from a labeled option,
1058+
/// parsing with a closure that receives the matched option name.
1059+
///
1060+
/// - Parameters:
1061+
/// - name: A specification for what names are allowed for this option.
1062+
/// - parsingStrategy: The behavior to use when looking for this option's
1063+
/// value.
1064+
/// - help: Information about how to use this option.
1065+
/// - completion: The type of command-line completion provided for this
1066+
/// option.
1067+
/// - transformIncludingName: A closure that receives the matched option
1068+
/// name (without leading dashes) and the value string, and returns this
1069+
/// property's type or throws an error.
1070+
@preconcurrency
1071+
public init<T>(
1072+
name: NameSpecification = .long,
1073+
parsing parsingStrategy: SingleValueParsingStrategy = .next,
1074+
help: ArgumentHelp? = nil,
1075+
completion: CompletionKind? = nil,
1076+
transformIncludingName: @Sendable @escaping (_ optionName: String, _ value: String) throws -> T
1077+
) where Value == T? {
1078+
self.init(
1079+
_parsedValue: .init { key in
1080+
let arg = ArgumentDefinition(
1081+
container: Optional<T>.self,
1082+
key: key,
1083+
kind: .name(key: key, specification: name),
1084+
help: help,
1085+
parsingStrategy: parsingStrategy.base,
1086+
transformIncludingName: transformIncludingName,
1087+
initial: nil,
1088+
completion: completion)
1089+
1090+
return ArgumentSet(arg)
1091+
})
1092+
}
1093+
}
1094+
1095+
// MARK: - @Option Array<T> Initializers (name-aware transform)
1096+
extension Option {
1097+
/// Creates an array property that reads its values from zero or more labeled
1098+
/// options, parsing each element with a closure that receives the matched
1099+
/// option name along with the value string.
1100+
///
1101+
/// This is particularly useful when multiple option names map to a single
1102+
/// array property and the relative ordering of different options matters:
1103+
///
1104+
/// ```swift
1105+
/// @Option(
1106+
/// name: [.customLong("resize"), .customLong("crop")],
1107+
/// transformIncludingName: { name, value in
1108+
/// switch name {
1109+
/// case "resize": return .resize(value)
1110+
/// case "crop": return .crop(value)
1111+
/// default: fatalError()
1112+
/// }
1113+
/// }
1114+
/// ) var operations: [Operation] = []
1115+
/// ```
1116+
///
1117+
/// - Parameters:
1118+
/// - wrappedValue: A default value to use for this property, provided
1119+
/// implicitly by the compiler during property wrapper initialization.
1120+
/// If this initial value is non-empty, elements passed from the command
1121+
/// line are appended to the original contents.
1122+
/// - name: A specification for what names are allowed for this option.
1123+
/// - parsingStrategy: The behavior to use when parsing the elements for
1124+
/// this option.
1125+
/// - help: Information about how to use this option.
1126+
/// - completion: The type of command-line completion provided for this
1127+
/// option.
1128+
/// - transformIncludingName: A closure that receives the matched option
1129+
/// name (without leading dashes) and the value string, and returns the
1130+
/// element type or throws an error.
1131+
@preconcurrency
1132+
public init<T>(
1133+
wrappedValue: [T],
1134+
name: NameSpecification = .long,
1135+
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
1136+
help: ArgumentHelp? = nil,
1137+
completion: CompletionKind? = nil,
1138+
transformIncludingName: @Sendable @escaping (_ optionName: String, _ value: String) throws -> T
1139+
) where Value == [T] {
1140+
self.init(
1141+
_parsedValue: .init { key in
1142+
let arg = ArgumentDefinition(
1143+
container: Array<T>.self,
1144+
key: key,
1145+
kind: .name(key: key, specification: name),
1146+
help: help,
1147+
parsingStrategy: parsingStrategy.base,
1148+
transformIncludingName: transformIncludingName,
1149+
initial: wrappedValue,
1150+
completion: completion)
1151+
1152+
return ArgumentSet(arg)
1153+
})
1154+
}
1155+
1156+
/// Creates a required array property that reads its values from zero or more
1157+
/// labeled options, parsing each element with a closure that receives the
1158+
/// matched option name along with the value string.
1159+
///
1160+
/// - Parameters:
1161+
/// - name: A specification for what names are allowed for this option.
1162+
/// - parsingStrategy: The behavior to use when parsing the elements for
1163+
/// this option.
1164+
/// - help: Information about how to use this option.
1165+
/// - completion: The type of command-line completion provided for this
1166+
/// option.
1167+
/// - transformIncludingName: A closure that receives the matched option
1168+
/// name (without leading dashes) and the value string, and returns the
1169+
/// element type or throws an error.
1170+
@preconcurrency
1171+
public init<T>(
1172+
name: NameSpecification = .long,
1173+
parsing parsingStrategy: ArrayParsingStrategy = .singleValue,
1174+
help: ArgumentHelp? = nil,
1175+
completion: CompletionKind? = nil,
1176+
transformIncludingName: @Sendable @escaping (_ optionName: String, _ value: String) throws -> T
1177+
) where Value == [T] {
1178+
self.init(
1179+
_parsedValue: .init { key in
1180+
let arg = ArgumentDefinition(
1181+
container: Array<T>.self,
1182+
key: key,
1183+
kind: .name(key: key, specification: name),
1184+
help: help,
1185+
parsingStrategy: parsingStrategy.base,
1186+
transformIncludingName: transformIncludingName,
1187+
initial: nil,
1188+
completion: completion)
1189+
1190+
return ArgumentSet(arg)
1191+
})
1192+
}
1193+
}

Sources/ArgumentParser/Parsing/ArgumentDefinition.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,36 @@ extension ArgumentDefinition {
296296
completion: completion)
297297
}
298298

299+
init<Container>(
300+
container: Container.Type,
301+
key: InputKey,
302+
kind: ArgumentDefinition.Kind,
303+
help: ArgumentHelp?,
304+
parsingStrategy: ParsingStrategy,
305+
transformIncludingName: @escaping (String, String) throws -> Container.Contained,
306+
initial: Container.Initial?,
307+
completion: CompletionKind?
308+
) where Container: ArgumentDefinitionContainer {
309+
self.init(
310+
container: Container.self,
311+
key: key,
312+
kind: kind,
313+
allValueStrings: [],
314+
help: help,
315+
defaultValueDescription: nil,
316+
parsingStrategy: parsingStrategy,
317+
parser: { (key, origin, name, valueString) -> Container.Contained in
318+
do {
319+
return try transformIncludingName(name?.valueString ?? "", valueString)
320+
} catch {
321+
throw ParserError.unableToParseValue(
322+
origin, name, valueString, forKey: key, originalError: error)
323+
}
324+
},
325+
initial: initial,
326+
completion: completion)
327+
}
328+
299329
private init<Container>(
300330
container: Container.Type,
301331
key: InputKey,

0 commit comments

Comments
 (0)