Skip to content

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

Description

@weissi

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
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions