apple / apple/swift-argument-parser

Support for ordered, heterogeneous repeating options (e.g. expose matched name to transform closure)

Open
#862 2 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.