apple / apple/swift-argument-parser
Support for ordered, heterogeneous repeating options (e.g. expose matched name to transform closure)
- Dominant language
- Swift
- Stars
- 3.8k
- Forks
- 411
- Avg merge
- 7d 13h
- Merged PRs (30d)
- 17
Description
Some CLI tools need multiple different option names whose values are collected into a single ordered array — where the relative ordering across different flags matters.
## Motivating example
Consider an image processing pipeline where transformations must be applied in the order the user specifies:
```
./image-tool input.png \
--resize 50% \
--blur 3 \
--crop 100x100 \
--rotate 90 \
--blur 1
```
The order matters: `--resize` then `--crop` produces a different result than `--crop` then `--resize`. Tools like ImageMagick and ffmpeg have this pattern.
## Current state
Today, you can declare separate `@Option var resize: [String]` and `@Option var crop: [String]` arrays, which each preserve their own insertion order — but there's no way to know the relative ordering across different option names.
The parser already tracks this information internally. In `ArgumentDefinition.swift`, the `Update.Unary` closure receives the matched `Name?`:
```
typealias Unary = (InputOrigin, Name?, String, inout ParsedValues) throws -> Void
```
And when building the update closure, name is available but not passed through to user code:
```
update: .unary({ (origin, name, valueString, parsedValues) in
let value = try parser(key, origin, name, valueString) // name available here
Container.update(
parsedValues: &parsedValues,
value: value,
key: key,
origin: origin) // ...but not passed to the container
})
```
The public transform closure only receives the `value` string, discarding the `name`.
## Proposed solution
Add `@Option` initialiser overloads where the transform closure receives the matched option name as a string, in addition to the value:
```
@Option(
name: [.customLong("resize"), .customLong("blur"), .customLong("crop"), .customLong("rotate")],
transform: { optionName, value in
switch optionName {
case "resize": return .resize(value)
case "blur": return .blur(Double(value)!)
case "crop": return .crop(value)
case "rotate": return .rotate(Double(value)!)
default: fatalError()
}
}
) var operations: [Operation]
```
This would allow:
```
enum Operation {
case resize(String)
case blur(Double)
case crop(String)
case rotate(Double)
}
struct ImageTool: ParsableCommand {
@Argument var input: String
@Option(
name: [.customLong("resize"), .customLong("blur"), .customLong("crop"), .customLong("rotate")],
transform: { optionName, value in
switch optionName {
case "resize": return .resize(value)
case "blur": return .blur(Double(value)!)
case "crop": return .crop(value)
case "rotate": return .rotate(Double(value)!)
default: fatalError()
}
}
) var operations: [Operation] = []
func run() {
for op in operations {
print(op) // Printed in the order the user specified
}
}
}
```
Contributor guide
Research direction
Start in ArgumentDefinition.swift, where Update.Unary receives the matched Name? and the option update closure currently passes only the parsed value to the container. Trace the @Option transform and container update paths to determine how a matched option name can reach user code. Done means heterogeneous option names can be collected in one ordered array while preserving the command-line order.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100