Augment Option to support default as flag option (#830)

In some use cases, there is a need to have an option argument behave
like a flag.

This change introduced 4 new intialiazers to `Option` that accept a
`defaultAsFlag` value.

With the following usage:

```swift
struct Example: ParsableCommand {
    @Option(defaultAsFlag: "default", help: "Set output format.")
    var format: String?
    func run() {
        print("Format: \(format ?? "none")")
    }
}
```

The `defaultAsFlag` parameter creates a hybrid that supports both patterns:
  - **Flag behavior**: `--format` (sets format to "default")
  - **Option behavior**: `--format json` (sets format to "json")
  - **No usage**: format remains `nil`

As a user of the command line tool, the `--help` output clearly distinguishes
between the the hybrid and regular usages.

```
OPTIONS:
  --format [<format>]     Set output format. (default as flag: default)
````

Note the `(default as flag: ...)` text instead of regular `(default: ...)`,
and the optional value syntax `[<value>]` instead of required `<value>`.

Fixes: #829
This commit is contained in:
Bassam (Sam) Khouri
2026-03-23 15:47:26 -04:00
committed by GitHub
parent 747565c049
commit a78d98b92d
23 changed files with 2025 additions and 25 deletions
@@ -0,0 +1,54 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
import ArgumentParser
@main
struct DefaultAsFlag: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "A utility demonstrating defaultAsFlag options.",
discussion: """
This command shows how defaultAsFlag options can work both as flags
and as options with values.
"""
)
@Option(defaultAsFlag: "default", help: "A string option with defaultAsFlag.")
var stringFlag: String?
@Option(defaultAsFlag: 42, help: "An integer option with defaultAsFlag.")
var numberFlag: Int?
@Option(defaultAsFlag: true, help: "A boolean option with defaultAsFlag.")
var boolFlag: Bool?
@Option(
defaultAsFlag: "transformed",
help: "A string option with transform and defaultAsFlag.",
transform: { $0.uppercased() }
)
var transformFlag: String?
@Option(name: .shortAndLong, help: "A regular option for comparison.")
var regular: String?
@Argument
var additionalArgs: [String] = []
func run() {
print("String flag: \(stringFlag?.description ?? "nil")")
print("Number flag: \(numberFlag?.description ?? "nil")")
print("Bool flag: \(boolFlag?.description ?? "nil")")
print("Transform flag: \(transformFlag?.description ?? "nil")")
print("Regular option: \(regular?.description ?? "nil")")
print("Additional args: \(additionalArgs)")
}
}
+5
View File
@@ -78,6 +78,11 @@ var package = Package(
name: "color",
dependencies: ["ArgumentParser"],
path: "Examples/color"),
.executableTarget(
name: "default-as-flag",
dependencies: ["ArgumentParser"],
path: "Examples/default-as-flag"
),
// Tools
.executableTarget(
+5 -4
View File
@@ -7,7 +7,7 @@ that you need to collect from the command line.
Decorate each stored property with one of `ArgumentParser`'s property wrappers,
and then declare conformance to `ParsableCommand` and add the `@main` attribute.
(Note, for `async` renditions of `run`, conform to `AsyncParsableCommand` rather
than `ParsableCommand`.)
than `ParsableCommand`.)
Finally, implement your command's logic in the `run()` method.
```swift
@@ -70,7 +70,7 @@ OPTIONS:
## Documentation
For guides, articles, and API documentation see the
For guides, articles, and API documentation see the
[library's documentation on the Web][docs] or in Xcode.
- [ArgumentParser documentation][docs]
@@ -88,6 +88,7 @@ This repository includes a few examples of using the library:
- [`roll`](Examples/roll/main.swift) is a simple utility implemented as a straight-line script.
- [`math`](Examples/math/Math.swift) is an annotated example of using nested commands and subcommands.
- [`count-lines`](Examples/count-lines/CountLines.swift) uses `async`/`await` code in its implementation.
- [`default-as-flag`](Examples/default-as-flag/DefaultAsFlag.swift) demonstrates hybrid options that can work both as flags and as options with values.
You can also see examples of `ArgumentParser` adoption among Swift project tools:
@@ -104,7 +105,7 @@ The public API of version 1.0.0 of the `swift-argument-parser` package
consists of non-underscored declarations that are marked public in the `ArgumentParser` module.
Interfaces that aren't part of the public API may continue to change in any release,
including the exact wording and formatting of the autogenerated help and error messages,
as well as the packages examples, tests, utilities, and documentation.
as well as the packages examples, tests, utilities, and documentation.
Future minor versions of the package may introduce changes to these rules as needed.
@@ -115,7 +116,7 @@ Requiring a new Swift release will only require a minor version bump.
## Adding `ArgumentParser` as a Dependency
To use the `ArgumentParser` library in a SwiftPM project,
To use the `ArgumentParser` library in a SwiftPM project,
add it to the dependencies for your package and your command-line executable target:
```swift
@@ -81,7 +81,7 @@ struct Lucky: ParsableCommand {
```
```
% lucky
% lucky
Your lucky numbers are:
7 14 21
% lucky 1 2 3
@@ -327,6 +327,83 @@ If a default is not specified, the user must provide a value for that argument/o
You must also always specify a default of `false` for a non-optional `Bool` flag, as in the example above. This makes the behavior consistent with both normal Swift properties (which either must be explicitly initialized or optional to initialize a `struct`/`class` containing them) and the other property types.
### Creating hybrid flag/option behavior with defaultAsFlag
The `defaultAsFlag` parameter allows you to create options that can work both as flags (without values) and as options (with values). This provides flexible command-line interfaces where users can choose between concise flag usage or explicit value specification.
```swift
struct Example: ParsableCommand {
@Option(defaultAsFlag: "json", help: "Set the export format.")
var export: String?
func run() {
print("Export: \(format ?? "<don't export>")")
}
}
```
**Command-line behavior:**
```
% example # export = nil
% example --export # export = "json"
% example --export yaml # format = "yaml"
```
The `defaultAsFlag` parameter creates a hybrid that supports both patterns:
- **Flag behavior**: `--export` (sets format to "json")
- **Option behavior**: `--export yaml` (sets format to "yaml")
- **No usage**: `export` remains `nil`
#### Type requirements
- The property **must** be optional (`T?`)
- The `defaultAsFlag` value must be of the unwrapped type (`T`)
- All standard `ExpressibleByArgument` types are supported (String, Int, Bool, Double, etc.)
#### Advanced usage
You can combine `defaultAsFlag` with transform functions:
```swift
@Option(
defaultAsFlag: "info",
help: "Set log level.",
transform: { $0.uppercased() }
)
var logLevel: String?
```
**Behavior:**
```
% app --log-level # logLevel = "INFO" (transformed default)
% app --log-level debug # logLevel = "DEBUG" (transformed input)
```
#### Help display
DefaultAsFlag options show special help formatting to distinguish them from regular defaults:
```
OPTIONS:
--format [<format>] Set output format. (default as flag: json)
--port [<port>] Server port. (default as flag: 8080)
```
Note the `(default as flag: ...)` text instead of regular `(default: ...)`, and the optional value syntax `[<value>]` instead of required `<value>`.
#### Value detection
The parser determines whether a value follows the option:
1. **Next argument is a value** if it doesn't start with `-` and isn't another known option
2. **No value available**: Use the `defaultAsFlag` value
3. **Explicit value provided**: Parse and use that value
This works with parsing strategies `.next` and `.scanningForValue`. The `.unconditional` parsing strategy defeats the purpose by always requiring a value.
For complete examples and API reference, see the [`default-as-flag`](https://github.com/apple/swift-argument-parser/tree/main/Examples/default-as-flag) example.
### Specifying a parsing strategy
When parsing a list of command-line inputs, `ArgumentParser` distinguishes between dash-prefixed keys and un-prefixed values. When looking for the value for a key, only an un-prefixed value will be selected by default.
@@ -479,7 +556,7 @@ When appropriate, you can process supported arguments and ignore unknown ones by
```swift
struct Example: ParsableCommand {
@Flag var verbose = false
@Argument(parsing: .allUnrecognized)
var unknowns: [String] = []
@@ -120,7 +120,7 @@ public struct SingleValueParsingStrategy: Hashable {
///
/// For inputs such as `--foo foo`, this would parse `foo` as the
/// value. However, the input `--foo --bar foo bar` would
/// result in an error. Even though two values are provided, they dont
/// result in an error. Even though two values are provided, they don't
/// succeed each option. Parsing would result in an error such as the following:
///
/// Error: Missing value for '--foo <foo>'
@@ -161,7 +161,46 @@ public struct SingleValueParsingStrategy: Hashable {
}
}
/// The strategy to use when parsing a single value from `@Option` arguments with `defaultAsFlag`.
///
/// This is a subset of `SingleValueParsingStrategy` that excludes strategies incompatible
/// with default-as-flag behavior.
public struct DefaultAsFlagParsingStrategy: Hashable {
internal var base: ArgumentDefinition.ParsingStrategy
/// Parse the input after the option and expect it to be a value.
///
/// For inputs such as `--foo foo`, this would parse `foo` as the
/// value. However, the input `--foo --bar foo bar` would
/// result in an error. Even though two values are provided, they don't
/// succeed each option. Parsing would result in an error such as the following:
///
/// Error: Missing value for '--foo <foo>'
/// Usage: command [--foo <foo>]
///
/// When used with `defaultAsFlag`, if no value is found, the default flag value is used.
public static var next: DefaultAsFlagParsingStrategy {
self.init(base: .default)
}
/// Parse the next input, as long as that input can't be interpreted as
/// an option or flag.
///
/// - Note: This will skip other options and _read ahead_ in the input
/// to find the next available value. This may be *unexpected* for users.
/// Use with caution.
///
/// For example, if `--foo` takes a value, then the input `--foo --bar bar`
/// would be parsed such that the value `bar` is used for `--foo`.
///
/// This is the **default behavior** for `defaultAsFlag` options.
public static var scanningForValue: DefaultAsFlagParsingStrategy {
self.init(base: .scanningForValue)
}
}
extension SingleValueParsingStrategy: Sendable {}
extension DefaultAsFlagParsingStrategy: Sendable {}
/// The strategy to use when parsing multiple values from `@Option` arguments into an
/// array.
@@ -208,7 +247,7 @@ public struct ArrayParsingStrategy: Hashable {
/// the input `--files foo bar` would result in the array
/// `["foo", "bar"]`.
///
/// Parsing stops as soon as theres another option in the input such that
/// Parsing stops as soon as there's another option in the input such that
/// `--files foo bar --verbose` would also set `files` to the array
/// `["foo", "bar"]`.
public static var upToNextOption: ArrayParsingStrategy {
@@ -503,6 +542,70 @@ extension Option {
})
}
/// Creates an optional property that reads its value from a labeled option,
/// with a default value when the flag is provided without a value.
///
/// This initializer allows providing a `defaultAsFlag` value that is used
/// when the flag is present but no value follows it:
///
/// ```swift
/// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path")
/// var showBinPath: String? = nil
/// ```
///
/// - Parameters:
/// - wrappedValue: A default value to use for this property, provided
/// implicitly by the compiler during property wrapper initialization.
/// - name: A specification for what names are allowed for this option.
/// - defaultAsFlag: The value to use when the flag is provided without a value.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
/// - completion: The type of command-line completion provided for this option.
public init<T>(
wrappedValue: _OptionalNilComparisonType,
name: NameSpecification = .long,
defaultAsFlag: T,
parsing parsingStrategy: DefaultAsFlagParsingStrategy = .scanningForValue,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where T: ExpressibleByArgument, Value == T? {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
kind: .name(key: key, specification: name),
help: .init(
allValueStrings: T.allValueStrings,
options: [.isOptional],
help: help,
defaultValue: String(describing: defaultAsFlag),
key: key,
isComposite: false),
completion: completion ?? T.defaultCompletionKind,
parsingStrategy: parsingStrategy.base,
update: .optionalUnary(
nullaryHandler: { (origin, name, parsedValues) in
// Act like a flag - when present without value, use defaultAsFlag
parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin)
},
unaryHandler: { (origin, name, valueString, parsedValues) in
// Parse the provided value
guard let parsedValue = T(argument: valueString) else {
throw ParserError.unableToParseValue(
origin, name, valueString, forKey: key, originalError: nil)
}
parsedValues.set(parsedValue, forKey: key, inputOrigin: origin)
}
),
initial: { origin, values in
values.set(
nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)
)
})
return ArgumentSet(arg)
})
}
@available(
*, deprecated,
message: """
@@ -580,6 +683,68 @@ extension Option {
return ArgumentSet(arg)
})
}
/// Creates an optional property that reads its value from a labeled option,
/// with a default value when the flag is provided without a value.
///
/// This initializer allows providing a `defaultAsFlag` value that is used
/// when the flag is present but no value follows it:
///
/// ```swift
/// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path")
/// var showBinPath: String?
/// ```
///
/// - Parameters:
/// - name: A specification for what names are allowed for this option.
/// - defaultAsFlag: The value to use when the flag is provided without a value.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
/// - completion: The type of command-line completion provided for this option.
public init<T>(
name: NameSpecification = .long,
defaultAsFlag: T,
parsing parsingStrategy: DefaultAsFlagParsingStrategy = .scanningForValue,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where T: ExpressibleByArgument, Value == T? {
// Implementation matching the first initializer - hybrid flag/option behavior
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
kind: .name(key: key, specification: name),
help: .init(
allValueStrings: T.allValueStrings,
options: [.isOptional],
help: help,
defaultValue: String(describing: defaultAsFlag),
key: key,
isComposite: false),
completion: completion ?? T.defaultCompletionKind,
parsingStrategy: parsingStrategy.base,
update: .optionalUnary(
nullaryHandler: { (origin, name, parsedValues) in
// Act like a flag - when present without value, use defaultAsFlag
parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin)
},
unaryHandler: { (origin, name, valueString, parsedValues) in
// Parse the provided value
guard let parsedValue = T(argument: valueString) else {
throw ParserError.unableToParseValue(
origin, name, valueString, forKey: key, originalError: nil)
}
parsedValues.set(parsedValue, forKey: key, inputOrigin: origin)
}
),
initial: { origin, values in
values.set(
nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)
)
})
return ArgumentSet(arg)
})
}
}
// MARK: - @Option Optional<T> Initializers
@@ -631,6 +796,78 @@ extension Option {
})
}
/// Creates an optional property that reads its value from a labeled option,
/// parsing with the given closure, with a default value when the flag is
/// provided without a value.
///
/// This initializer allows providing a `defaultAsFlag` value that is used
/// when the flag is present but no value follows it:
///
/// ```swift
/// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path", transform: { $0.uppercased() })
/// var showBinPath: String? = nil
/// ```
///
/// - Parameters:
/// - wrappedValue: A default value to use for this property, provided
/// implicitly by the compiler during property wrapper initialization.
/// - name: A specification for what names are allowed for this option.
/// - defaultAsFlag: The value to use when the flag is provided without a value.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
/// - completion: The type of command-line completion provided for this option.
/// - transform: A closure that converts a string into this property's
/// type, or else throws an error.
@preconcurrency
public init<T>(
wrappedValue: _OptionalNilComparisonType,
name: NameSpecification = .long,
defaultAsFlag: T,
parsing parsingStrategy: DefaultAsFlagParsingStrategy = .scanningForValue,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @Sendable @escaping (String) throws -> T
) where Value == T? {
// Implementation with hybrid flag/option behavior and transform
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
kind: .name(key: key, specification: name),
help: .init(
allValueStrings: [],
options: [.isOptional],
help: help,
defaultValue: String(describing: defaultAsFlag),
key: key,
isComposite: false),
completion: completion ?? .default,
parsingStrategy: parsingStrategy.base,
update: .optionalUnary(
nullaryHandler: { (origin, name, parsedValues) in
// Act like a flag - when present without value, use defaultAsFlag
parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin)
},
unaryHandler: { (origin, name, valueString, parsedValues) in
// Parse the provided value using the transform
do {
let parsedValue = try transform(valueString)
parsedValues.set(parsedValue, forKey: key, inputOrigin: origin)
} catch {
throw ParserError.unableToParseValue(
origin, name, valueString, forKey: key, originalError: error)
}
}
),
initial: { origin, values in
values.set(
nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)
)
})
return ArgumentSet(arg)
})
}
@available(
*, deprecated,
message: """
@@ -706,6 +943,75 @@ extension Option {
return ArgumentSet(arg)
})
}
/// Creates an optional property that reads its value from a labeled option,
/// parsing with the given closure, with a default value when the flag is
/// provided without a value.
///
/// This initializer allows providing a `defaultAsFlag` value that is used
/// when the flag is present but no value follows it:
///
/// ```swift
/// @Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path", transform: { $0.uppercased() })
/// var showBinPath: String?
/// ```
///
/// - Parameters:
/// - name: A specification for what names are allowed for this option.
/// - defaultAsFlag: The value to use when the flag is provided without a value.
/// - parsingStrategy: The behavior to use when looking for this option's value.
/// - help: Information about how to use this option.
/// - completion: The type of command-line completion provided for this option.
/// - transform: A closure that converts a string into this property's
/// type, or else throws an error.
@preconcurrency
public init<T>(
name: NameSpecification = .long,
defaultAsFlag: T,
parsing parsingStrategy: DefaultAsFlagParsingStrategy = .scanningForValue,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @Sendable @escaping (String) throws -> T
) where Value == T? {
// Implementation with hybrid flag/option behavior and transform
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
kind: .name(key: key, specification: name),
help: .init(
allValueStrings: [],
options: [.isOptional],
help: help,
defaultValue: String(describing: defaultAsFlag),
key: key,
isComposite: false),
completion: completion ?? .default,
parsingStrategy: parsingStrategy.base,
update: .optionalUnary(
nullaryHandler: { (origin, name, parsedValues) in
// Act like a flag - when present without value, use defaultAsFlag
parsedValues.set(defaultAsFlag, forKey: key, inputOrigin: origin)
},
unaryHandler: { (origin, name, valueString, parsedValues) in
// Parse the provided value using the transform
do {
let parsedValue = try transform(valueString)
parsedValues.set(parsedValue, forKey: key, inputOrigin: origin)
} catch {
throw ParserError.unableToParseValue(
origin, name, valueString, forKey: key, originalError: error)
}
}
),
initial: { origin, values in
values.set(
nil, forKey: key, inputOrigin: InputOrigin(element: .defaultValue)
)
})
return ArgumentSet(arg)
})
}
}
// MARK: - @Option Array<T: ExpressibleByArgument> Initializers
@@ -22,6 +22,10 @@ struct ArgumentDefinition {
/// An argument that takes a string as its value.
case unary(Unary)
/// An argument that can work as both a flag (nullary) and option (unary).
/// When no value follows, uses nullaryHandler. When a value is available, uses unaryHandler.
case optionalUnary(nullaryHandler: Nullary, unaryHandler: Unary)
}
typealias Initial = (InputOrigin, inout ParsedValues) throws -> Void
@@ -133,6 +137,9 @@ struct ArgumentDefinition {
if case (.positional, .nullary) = (kind, update) {
preconditionFailure("Can't create a nullary positional argument.")
}
if case (.positional, .optionalUnary) = (kind, update) {
preconditionFailure("Can't create an optionalUnary positional argument.")
}
self.kind = kind
self.help = help
@@ -157,6 +164,12 @@ extension ArgumentDefinition: CustomDebugStringConvertible {
.map { $0.synopsisString }
.joined(separator: ",")
+ " <\(valueName)>"
case (.named(let names), .optionalUnary):
return
names
.map { $0.synopsisString }
.joined(separator: ",")
+ " [<\(valueName)>]" // Optional value syntax
case (.positional, _):
return "<\(valueName)>"
case (.default, _):
@@ -192,9 +205,12 @@ extension ArgumentDefinition {
}
var isNullary: Bool {
if case .nullary = update {
switch update {
case .nullary:
return true
} else {
case .optionalUnary:
return true // Can behave as nullary
case .unary:
return false
}
}
@@ -348,6 +348,19 @@ struct LenientParser {
let origins = origin.inserting(origin2)
try update(origins, parsed.name, value, &result)
usedOrigins.formUnion(origins)
} else if let (_, element) = inputArguments.peekNext(),
!element.isTerminator,
case .option(let nextParsed) = element.value,
argumentSet.first(matching: nextParsed) == nil,
nextParsed.value != nil,
let (actualOrigin, value) = inputArguments.popNextElementAsValue(
after: originElement)
{
// For default-as-flag options with scanningForValue, try consuming option-like
// strings as values only if they are unrecognized options with explicit values (e.g., "--somearg=value")
let origins = origin.inserting(actualOrigin)
try update(origins, parsed.name, value, &result)
usedOrigins.formUnion(origins)
} else {
throw errorForMissingValue(originElement, parsed)
}
@@ -366,16 +379,15 @@ struct LenientParser {
let origins = origin.inserting(origin2)
try update(origins, parsed.name, String(value), &result)
usedOrigins.formUnion(origins)
} else {
guard
let (origin2, value) = inputArguments.popNextElementAsValue(
after: originElement)
else {
throw errorForMissingValue(originElement, parsed)
}
} else if let (origin2, value) = inputArguments.popNextElementAsValue(
after: originElement)
{
// Only consume if there's no terminator between option and value
let origins = origin.inserting(origin2)
try update(origins, parsed.name, value, &result)
usedOrigins.formUnion(origins)
} else {
throw errorForMissingValue(originElement, parsed)
}
case .allRemainingInput:
@@ -459,6 +471,61 @@ struct LenientParser {
}
}
mutating func parseOptionalUnaryValue(
_ argument: ArgumentDefinition,
_ parsed: ParsedArgument,
_ originElement: InputOrigin.Element,
_ nullaryHandler: ArgumentDefinition.Update.Nullary,
_ unaryHandler: ArgumentDefinition.Update.Unary,
_ result: inout ParsedValues,
_ usedOrigins: inout InputOrigin
) throws {
// For default-as-flag options with .scanningForValue, check if the next element
// is a recognized option and fall back to flag behavior if so
if case .scanningForValue = argument.parsingStrategy,
let (_, element) = inputArguments.peekNext(),
!element.isTerminator,
case .option(let nextParsed) = element.value,
argumentSet.first(matching: nextParsed) != nil
{
// Fall back to flag behavior when the next element is a recognized option
let origin = InputOrigin(elements: [originElement])
try nullaryHandler(origin, parsed.name, &result)
usedOrigins.formUnion(origin)
return
}
do {
// Try to parse as a unary value first using the main parseValue logic
try parseValue(
argument,
parsed,
originElement,
unaryHandler,
&result,
&usedOrigins
)
} catch let error as ParserError {
switch error {
case .missingValueForOption, .missingValueOrUnknownCompositeOption:
// Fall back to flag behavior when no value is available
let origin = InputOrigin(elements: [originElement])
try nullaryHandler(origin, parsed.name, &result)
usedOrigins.formUnion(origin)
case .unknownOption, .unableToParseValue:
// Fall back to flag behavior when parseValue fails to find a suitable value
// This handles cases where potential values like "--somearg=value" are rejected
// because they look like unknown options, or when transform functions fail
let origin: InputOrigin = InputOrigin(elements: [originElement])
try nullaryHandler(origin, parsed.name, &result)
usedOrigins.formUnion(origin)
default:
// Re-throw other parser errors
throw error
}
}
}
mutating func parsePositionalValues(
from unusedInput: SplitArguments,
into result: inout ParsedValues
@@ -629,7 +696,7 @@ struct LenientParser {
switch argument.update {
case .nullary(let update):
// We dont expect a value for this option.
// We don't expect a value for this option.
if let value = parsed.value {
throw ParserError.unexpectedValueForOption(
origin, parsed.name, value)
@@ -639,6 +706,12 @@ struct LenientParser {
case .unary(let update):
try parseValue(
argument, parsed, origin, update, &result, &usedOrigins)
case .optionalUnary(let nullaryHandler, let unaryHandler):
// Hybrid behavior: try to find a value, fall back to flag behavior
// For default-as-flag options, we need special handling in scanningForValue
try parseOptionalUnaryValue(
argument, parsed, origin, nullaryHandler, unaryHandler,
&result, &usedOrigins)
}
case .terminator:
// Ignore the terminator, it might get picked up as a positional value later.
@@ -336,6 +336,7 @@ extension SplitArguments {
/// value for `-f`, or `--foo name` where `name` is the value for `--foo`.
/// If `--foo` expects a value, an input of `--foo --bar name` will return
/// `nil`, since the option `--bar` comes before the value `name`.
/// Also returns `nil` if there's a terminator between the origin and the target value.
mutating func popNextElementIfValue(after origin: InputOrigin.Element) -> (
InputOrigin.Element, String
)? {
@@ -353,14 +354,41 @@ extension SplitArguments {
guard case .value(let value) = elements[elementIndex].value
else { return nil }
defer { remove(at: elementIndex) }
let matchedArgumentIndex = elements[elementIndex].index
return (.argumentIndex(matchedArgumentIndex), value)
let targetOrigin = InputOrigin.Element.argumentIndex(matchedArgumentIndex)
// Check if there's a terminator between the origin and target
if hasTerminatorBetween(origin, targetOrigin) {
return nil
}
defer { remove(at: elementIndex) }
return (targetOrigin, value)
}
/// Helper function to check if there's a terminator between two origins.
private func hasTerminatorBetween(
_ originElement: InputOrigin.Element,
_ targetOrigin: InputOrigin.Element
) -> Bool {
guard case .argumentIndex(let currentIndex) = originElement,
case .argumentIndex(let targetIndex) = targetOrigin
else { return false }
// Check if there's a terminator between current position and target position
let terminatorIndex = elements.firstIndex { element in
element.isTerminator
&& element.index.inputIndex > currentIndex.inputIndex
&& element.index.inputIndex < targetIndex.inputIndex
}
return terminatorIndex != nil
}
/// Pops the next `.value` after the given index.
///
/// This is used to get the next value in `-f -b name` where `name` is the value of `-f`.
/// Also returns `nil` if there's a terminator between the origin and the target value.
mutating func popNextValue(
after origin: InputOrigin.Element
) -> (InputOrigin.Element, String)? {
@@ -368,11 +396,19 @@ extension SplitArguments {
guard let resultIndex = elements[start...].firstIndex(where: { $0.isValue })
else { return nil }
let targetOrigin = InputOrigin.Element.argumentIndex(
elements[resultIndex].index)
// Check if there's a terminator between the origin and target
if hasTerminatorBetween(origin, targetOrigin) {
return nil
}
defer { remove(at: resultIndex) }
// swift-format-ignore: NeverForceUnwrap
// This is safe because we know `resultIndex` is refers to a value
return (
.argumentIndex(elements[resultIndex].index),
targetOrigin,
elements[resultIndex].value.valueString!
)
}
@@ -384,6 +420,7 @@ extension SplitArguments {
///
/// For an input such as `--a --b foo`, if passed the origin of `--a`,
/// this will first pop the value `--b`, then the value `foo`.
/// Also returns `nil` if there's a terminator between the origin and the target element.
mutating func popNextElementAsValue(after origin: InputOrigin.Element) -> (
InputOrigin.Element, String
)? {
@@ -395,11 +432,19 @@ extension SplitArguments {
$0.index.subIndex == .complete
})?.index
else { return nil }
let targetOrigin = InputOrigin.Element.argumentIndex(nextIndex)
// Check if there's a terminator between the origin and target
if hasTerminatorBetween(origin, targetOrigin) {
return nil
}
// Remove all elements with this `InputIndex`:
remove(at: nextIndex)
// Return the original input
return (
.argumentIndex(nextIndex), originalInput[nextIndex.inputIndex.rawValue]
targetOrigin, originalInput[nextIndex.inputIndex.rawValue]
)
}
@@ -142,6 +142,8 @@ extension ArgumentInfoV0.KindV0 {
self = .flag
case .unary:
self = .option
case .optionalUnary:
self = .option
}
case .positional:
self = .positional
@@ -232,10 +232,22 @@ internal struct HelpGenerator {
allAndDefaultValues =
"(values: \(allValueStrings.joined(separator: ", ")))"
case (false, true):
allAndDefaultValues = "(default: \(defaultValue))"
switch arg.update {
case .nullary, .unary:
allAndDefaultValues = "(default: \(defaultValue))"
case .optionalUnary:
allAndDefaultValues = "(default as flag: \(defaultValue))"
}
case (true, true):
allAndDefaultValues =
"(values: \(allValueStrings.joined(separator: ", ")); default: \(defaultValue))"
switch arg.update {
case .nullary, .unary:
allAndDefaultValues =
"(values: \(allValueStrings.joined(separator: ", ")); default: \(defaultValue))"
case .optionalUnary:
allAndDefaultValues =
"(values: \(allValueStrings.joined(separator: ", ")); default as flag: \(defaultValue))"
}
}
if arg.help.isComposite {
@@ -86,6 +86,8 @@ extension ArgumentDefinition {
return "\(joinedSynopsisString) <\(valueName)>"
case .nullary:
return joinedSynopsisString
case .optionalUnary:
return "\(joinedSynopsisString) [<\(valueName)>]"
}
case .positional:
return "<\(valueName)>"
@@ -106,6 +108,8 @@ extension ArgumentDefinition {
return "\(name.synopsisString) <\(valueName)>"
case .nullary:
return name.synopsisString
case .optionalUnary:
return "\(name.synopsisString) [<\(valueName)>]"
}
case .positional:
return "<\(valueName)>"
@@ -0,0 +1,315 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
import ArgumentParser
import ArgumentParserTestHelpers
import XCTest
final class DefaultAsFlagEndToEndTests: XCTestCase {}
// MARK: - Test Cases
extension DefaultAsFlagEndToEndTests {
// Test struct for defaultAsFlag without transform - explicit nil
private struct CommandWithDefaultAsFlagWithoutTransformExplicitNil:
ParsableCommand
{
@Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path")
var showBinPath: String? = nil
}
// Test struct for defaultAsFlag without transform - no explicit default
private struct CommandWithDefaultAsFlagWithoutTransformNoExplicitNil:
ParsableCommand
{
@Option(name: .customLong("bin-path"), defaultAsFlag: "/default/path")
var showBinPath: String?
}
// Test struct for defaultAsFlag with transform - explicit nil
private struct CommandWithDefaultAsFlagWithTransformExplicitNil:
ParsableCommand
{
@Option(
name: .customLong("bin-path"), defaultAsFlag: "/default/path",
transform: { $0.uppercased() })
var showBinPath: String? = nil
}
// Test struct for defaultAsFlag with transform - no explicit default
private struct CommandWithDefaultAsFlagWithTransformNoExplicitNil:
ParsableCommand
{
@Option(
name: .customLong("bin-path"), defaultAsFlag: "/default/path",
transform: { $0.uppercased() })
var showBinPath: String?
}
func testDefaultAsFlagWithExplicitNil() throws {
// When no argument is provided, should be nil
AssertParse(CommandWithDefaultAsFlagWithoutTransformExplicitNil.self, []) {
cmd in
XCTAssertNil(cmd.showBinPath)
}
// When flag is provided without value, should use defaultAsFlag
AssertParse(
CommandWithDefaultAsFlagWithoutTransformExplicitNil.self, ["--bin-path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/default/path")
}
// When flag is provided with value, should use provided value
AssertParse(
CommandWithDefaultAsFlagWithoutTransformExplicitNil.self,
["--bin-path", "/custom/path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/custom/path")
}
}
func testDefaultAsFlagWithoutExplicitDefault() throws {
// When no argument is provided, should be nil
AssertParse(CommandWithDefaultAsFlagWithoutTransformNoExplicitNil.self, [])
{ cmd in
XCTAssertNil(cmd.showBinPath)
}
// When flag is provided without value, should use defaultAsFlag
AssertParse(
CommandWithDefaultAsFlagWithoutTransformNoExplicitNil.self, ["--bin-path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/default/path")
}
// When flag is provided with value, should use provided value
AssertParse(
CommandWithDefaultAsFlagWithoutTransformNoExplicitNil.self,
["--bin-path", "/custom/path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/custom/path")
}
}
func testDefaultAsFlagWithTransformWithExplicitNil() throws {
// When no argument is provided, should be nil
AssertParse(CommandWithDefaultAsFlagWithTransformExplicitNil.self, []) {
cmd in
XCTAssertNil(cmd.showBinPath)
}
// When flag is provided without value, should use defaultAsFlag
AssertParse(
CommandWithDefaultAsFlagWithTransformExplicitNil.self, ["--bin-path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/default/path")
}
// When flag is provided with value, should use provided value with transform
AssertParse(
CommandWithDefaultAsFlagWithTransformExplicitNil.self,
["--bin-path", "/custom/path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/CUSTOM/PATH")
}
}
func testDefaultAsFlagWithTransformWithoutExplicitDefault() throws {
// When no argument is provided, should be nil
AssertParse(CommandWithDefaultAsFlagWithTransformNoExplicitNil.self, []) {
cmd in
XCTAssertNil(cmd.showBinPath)
}
// When flag is provided without value, should use defaultAsFlag
AssertParse(
CommandWithDefaultAsFlagWithTransformNoExplicitNil.self, ["--bin-path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/default/path")
}
// When flag is provided with value, should use provided value with transform
AssertParse(
CommandWithDefaultAsFlagWithTransformNoExplicitNil.self,
["--bin-path", "/custom/path"]
) { cmd in
XCTAssertEqual(cmd.showBinPath, "/CUSTOM/PATH")
}
}
// MARK: - Tests for -- terminator behavior
private struct CommandWithDefaultAsFlagAndArguments: ParsableCommand {
@Option(defaultAsFlag: "default")
var option: String?
@Argument
var files: [String] = []
}
func testDefaultAsFlagWithTerminatorFlagBeforeTerminator() throws {
// --option -- value
// Should use defaultAsFlag value, "value" becomes positional argument
AssertParse(
CommandWithDefaultAsFlagAndArguments.self, ["--option", "--", "value"]
) { cmd in
XCTAssertEqual(cmd.option, "default")
XCTAssertEqual(cmd.files, ["value"])
}
}
func testDefaultAsFlagWithTerminatorValueBeforeTerminator() throws {
// --option custom -- other
// Should use "custom" as option value, "other" becomes positional argument
AssertParse(
CommandWithDefaultAsFlagAndArguments.self,
["--option", "custom", "--", "other"]
) { cmd in
XCTAssertEqual(cmd.option, "custom")
XCTAssertEqual(cmd.files, ["other"])
}
}
func implTestDefaultAsFlagWithTerminatorValueBeforeTerminator(
optionValue: String,
file: StaticString = #file,
line: UInt = #line
) throws {
// --option custom -- other
// Should use "custom" as option value, "other" becomes positional argument
AssertParse(
CommandWithDefaultAsFlagAndArguments.self,
["--option", optionValue, "--", "other"]
) { cmd in
XCTAssertEqual(cmd.option, optionValue, file: file, line: line)
XCTAssertEqual(cmd.files, ["other"], file: file, line: line)
}
}
func testDefaultAsFlagWithValueWithTerminatorValueBeforeTerminator() throws {
try implTestDefaultAsFlagWithTerminatorValueBeforeTerminator(
optionValue: "custom")
}
func
testDefaultAsFlagWithCompleteValueAndWithTerminatorValueBeforeTerminator()
throws
{
try implTestDefaultAsFlagWithTerminatorValueBeforeTerminator(
optionValue: "--somearg=value")
}
func
testDefaultAsFlagWithCompleteValueAndWithTerminatorValueInQuotesBeforeTerminator()
throws
{
try implTestDefaultAsFlagWithTerminatorValueBeforeTerminator(
optionValue: "--somearg=\"value\"")
}
func testDefaultAsFlagWithTerminatorOptionAfterTerminator() throws {
// -- --option
// Should treat "--option" as positional argument, option should be nil
AssertParse(CommandWithDefaultAsFlagAndArguments.self, ["--", "--option"]) {
cmd in
XCTAssertNil(cmd.option)
XCTAssertEqual(cmd.files, ["--option"])
}
}
func testDefaultAsFlagWithTerminatorComplexScenario() throws {
// --option -- --another-option value
// Should use defaultAsFlag, everything after -- is positional
AssertParse(
CommandWithDefaultAsFlagAndArguments.self,
["--option", "--", "--another-option", "value"]
) { cmd in
XCTAssertEqual(cmd.option, "default")
XCTAssertEqual(cmd.files, ["--another-option", "value"])
}
}
func testDefaultAsFlagWithTerminatorValueAfterTerminatorNotConsumed() throws {
// --option -- value1 value2
// Should use defaultAsFlag, both values become positional arguments
AssertParse(
CommandWithDefaultAsFlagAndArguments.self,
["--option", "--", "value1", "value2"]
) { cmd in
XCTAssertEqual(cmd.option, "default")
XCTAssertEqual(cmd.files, ["value1", "value2"])
}
}
// MARK: - Tests for parsing strategy compilation restrictions
func testDefaultAsFlagCompilationRestrictionsWork() throws {
// This test verifies that DefaultAsFlagParsingStrategy only allows compatible strategies
// and prevents .unconditional at compile time
struct CommandWithAllowedStrategies: ParsableCommand {
// These should compile successfully
@Option(defaultAsFlag: "next", parsing: .next)
var nextStrategy: String?
@Option(defaultAsFlag: "scanning", parsing: .scanningForValue)
var scanningStrategy: String?
}
// Verify that the allowed strategies work correctly
AssertParse(CommandWithAllowedStrategies.self, ["--next-strategy"]) { cmd in
XCTAssertEqual(cmd.nextStrategy, "next")
XCTAssertNil(cmd.scanningStrategy)
}
AssertParse(CommandWithAllowedStrategies.self, ["--scanning-strategy"]) {
cmd in
XCTAssertNil(cmd.nextStrategy)
XCTAssertEqual(cmd.scanningStrategy, "scanning")
}
AssertParse(
CommandWithAllowedStrategies.self, ["--next-strategy", "custom"]
) { cmd in
XCTAssertEqual(cmd.nextStrategy, "custom")
XCTAssertNil(cmd.scanningStrategy)
}
AssertParse(
CommandWithAllowedStrategies.self,
["--scanning-strategy", "--next-strategy", "next-custom"]
) { cmd in
XCTAssertEqual(cmd.nextStrategy, "next-custom")
XCTAssertEqual(cmd.scanningStrategy, "scanning")
}
AssertParse(
CommandWithAllowedStrategies.self,
["--next-strategy", "next-custom", "--scanning-strategy"]
) { cmd in
XCTAssertEqual(cmd.nextStrategy, "next-custom")
XCTAssertEqual(cmd.scanningStrategy, "scanning")
}
AssertParse(
CommandWithAllowedStrategies.self,
[
"--scanning-strategy", "scanning-custom", "--next-strategy",
"next-custom",
]
) { cmd in
XCTAssertEqual(cmd.nextStrategy, "next-custom")
XCTAssertEqual(cmd.scanningStrategy, "scanning-custom")
}
}
}
@@ -56,4 +56,13 @@ final class GenerateManualTests: XCTestCase {
func testRollMultiPageManual() throws {
try assertGenerateManual(multiPage: true, command: "roll")
}
func testDefaultAsFlagSinglePageManual() throws {
try assertGenerateManual(multiPage: false, command: "default-as-flag")
}
func testDefaultAsFlagMultiPageManual() throws {
try assertGenerateManual(multiPage: true, command: "default-as-flag")
}
}
@@ -0,0 +1,77 @@
.\" "Generated by swift-argument-parser"
.Dd May 12, 1996
.Dt DEFAULT-AS-FLAG 9
.Os
.Sh NAME
.Nm default-as-flag
.Nd "A utility demonstrating defaultAsFlag options."
.Sh SYNOPSIS
.Nm
.Ar subcommand
.Op Fl -string-flag Ar string-flag
.Op Fl -number-flag Ar number-flag
.Op Fl -bool-flag Ar bool-flag
.Op Fl -transform-flag Ar transform-flag
.Op Fl -regular Ar regular
.Op Ar additional-args...
.Op Fl -help
.Sh DESCRIPTION
This command shows how defaultAsFlag options can work both as flags
and as options with values.
.Bl -tag -width 6n
.It Fl -string-flag Ar string-flag
A string option with defaultAsFlag.
.It Fl -number-flag Ar number-flag
An integer option with defaultAsFlag.
.It Fl -bool-flag Ar bool-flag
A boolean option with defaultAsFlag.
.It Fl -transform-flag Ar transform-flag
A string option with transform and defaultAsFlag.
.It Fl r , -regular Ar regular
A regular option for comparison.
.It Ar additional-args...
.It Fl h , -help
Show help information.
.El
.Sh "SEE ALSO"
.Xr default-as-flag.help 9
.Sh AUTHORS
The
.Nm
reference was written by
.An -nosplit
.An "Jane Appleseed" ,
.Mt johnappleseed@apple.com ,
and
.An -nosplit
.An "The Appleseeds"
.Ao
.Mt appleseeds@apple.com
.Ac .
.\" "Generated by swift-argument-parser"
.Dd May 12, 1996
.Dt DEFAULT-AS-FLAG.HELP 9
.Os
.Sh NAME
.Nm "default-as-flag help"
.Nd "Show subcommand help information."
.Sh SYNOPSIS
.Nm
.Op Ar subcommands...
.Sh DESCRIPTION
.Bl -tag -width 6n
.It Ar subcommands...
.El
.Sh AUTHORS
The
.Nm
reference was written by
.An -nosplit
.An "Jane Appleseed" ,
.Mt johnappleseed@apple.com ,
and
.An -nosplit
.An "The Appleseeds"
.Ao
.Mt appleseeds@apple.com
.Ac .
@@ -0,0 +1,53 @@
.\" "Generated by swift-argument-parser"
.Dd May 12, 1996
.Dt DEFAULT-AS-FLAG 9
.Os
.Sh NAME
.Nm default-as-flag
.Nd "A utility demonstrating defaultAsFlag options."
.Sh SYNOPSIS
.Nm
.Ar subcommand
.Op Fl -string-flag Ar string-flag
.Op Fl -number-flag Ar number-flag
.Op Fl -bool-flag Ar bool-flag
.Op Fl -transform-flag Ar transform-flag
.Op Fl -regular Ar regular
.Op Ar additional-args...
.Op Fl -help
.Sh DESCRIPTION
This command shows how defaultAsFlag options can work both as flags
and as options with values.
.Bl -tag -width 6n
.It Fl -string-flag Ar string-flag
A string option with defaultAsFlag.
.It Fl -number-flag Ar number-flag
An integer option with defaultAsFlag.
.It Fl -bool-flag Ar bool-flag
A boolean option with defaultAsFlag.
.It Fl -transform-flag Ar transform-flag
A string option with transform and defaultAsFlag.
.It Fl r , -regular Ar regular
A regular option for comparison.
.It Ar additional-args...
.It Fl h , -help
Show help information.
.It Em help
Show subcommand help information.
.Bl -tag -width 6n
.It Ar subcommands...
.El
.El
.Sh AUTHORS
The
.Nm
reference was written by
.An -nosplit
.An "Jane Appleseed" ,
.Mt johnappleseed@apple.com ,
and
.An -nosplit
.An "The Appleseeds"
.Ao
.Mt appleseeds@apple.com
.Ac .
@@ -0,0 +1,73 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
import ArgumentParserTestHelpers
import XCTest
@testable import ArgumentParser
final class DefaultAsFlagCompletionTests: XCTestCase {
func testDefaultAsFlagCompletion_Bash() throws {
let script = try CompletionsGenerator(
command: DefaultAsFlagCommand.self, shell: .bash
)
.generateCompletionScript()
try assertSnapshot(actual: script, extension: "bash")
}
func testDefaultAsFlagCompletion_Zsh() throws {
let script = try CompletionsGenerator(
command: DefaultAsFlagCommand.self, shell: .zsh
)
.generateCompletionScript()
try assertSnapshot(actual: script, extension: "zsh")
}
func testDefaultAsFlagCompletion_Fish() throws {
let script = try CompletionsGenerator(
command: DefaultAsFlagCommand.self, shell: .fish
)
.generateCompletionScript()
try assertSnapshot(actual: script, extension: "fish")
}
}
extension DefaultAsFlagCompletionTests {
struct DefaultAsFlagCommand: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "defaultasflag-test",
abstract:
"A command with defaultAsFlag options for testing completion scripts."
)
@Option(defaultAsFlag: "/usr/bin", completion: .directory)
var binPath: String? = nil
@Option(defaultAsFlag: 42)
var count: Int?
@Option(defaultAsFlag: true)
var verbose: Bool?
@Option(
defaultAsFlag: "INFO",
completion: .list(["DEBUG", "INFO", "WARN", "ERROR"]),
transform: { $0.uppercased() }
)
var logLevel: String?
@Flag
var help: Bool = false
@Argument(completion: .file())
var input: String
}
}
@@ -0,0 +1,69 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
import ArgumentParserTestHelpers
import XCTest
@testable import ArgumentParser
final class DefaultAsFlagDumpHelpTests: XCTestCase {
func testDefaultAsFlagDumpHelp() throws {
try assertDumpHelp(type: DefaultAsFlagCommand.self)
}
func testDefaultAsFlagWithTransformDumpHelp() throws {
try assertDumpHelp(type: DefaultAsFlagWithTransformCommand.self)
}
}
extension DefaultAsFlagDumpHelpTests {
struct DefaultAsFlagCommand: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "A command with defaultAsFlag options for testing dump help."
)
@Option(name: .customLong("binary-path"), defaultAsFlag: "/usr/bin")
var binPath: String? = nil
@Option(name: .long, defaultAsFlag: 42)
var count: Int?
@Option(name: .long, defaultAsFlag: true)
var verbose: Bool?
@Argument
var input: String
}
struct DefaultAsFlagWithTransformCommand: ParsableCommand {
static let configuration = CommandConfiguration(
abstract:
"A command with defaultAsFlag options using transforms for testing dump help."
)
@Option(
name: .customLong("output-dir"),
defaultAsFlag: "/default/output",
transform: { $0.uppercased() }
)
var outputDir: String? = nil
@Option(
name: .long,
defaultAsFlag: "INFO",
transform: { $0.lowercased() }
)
var level: String?
@Flag
var debug: Bool = false
}
}
@@ -0,0 +1,123 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2025 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
//
//===----------------------------------------------------------------------===//
import ArgumentParserTestHelpers
import XCTest
@testable import ArgumentParser
extension HelpGenerationTests {
struct BasicDefaultAsFlag: ParsableArguments {
@Option(
defaultAsFlag: "default", help: "A string option with defaultAsFlag.")
var stringFlag: String?
@Option(defaultAsFlag: 42, help: "An integer option with defaultAsFlag.")
var numberFlag: Int?
@Option(defaultAsFlag: true, help: "A boolean option with defaultAsFlag.")
var boolFlag: Bool?
@Option(
defaultAsFlag: "transformed",
help: "A string option with defaultAsFlag and transform.")
var transformFlag: String?
@Option(name: .shortAndLong, help: "A regular option for comparison.")
var regular: String?
}
func testDefaultAsFlagHelpOutput() {
AssertHelp(
.default, for: BasicDefaultAsFlag.self,
equals: """
USAGE: basic_default_as_flag [--string-flag [<string-flag>]] [--number-flag [<number-flag>]] [--bool-flag [<bool-flag>]] [--transform-flag [<transform-flag>]] [--regular <regular>]
OPTIONS:
--string-flag [<string-flag>]
A string option with defaultAsFlag. (default as flag:
default)
--number-flag [<number-flag>]
An integer option with defaultAsFlag. (default as
flag: 42)
--bool-flag [<bool-flag>]
A boolean option with defaultAsFlag. (default as
flag: true)
--transform-flag [<transform-flag>]
A string option with defaultAsFlag and transform.
(default as flag: transformed)
-r, --regular <regular> A regular option for comparison.
-h, --help Show help information.
""")
}
struct DefaultAsFlagWithShortNames: ParsableArguments {
@Option(
name: .shortAndLong, defaultAsFlag: "short", help: "Short and long names."
)
var shortAndLong: String?
@Option(
name: [.customShort("o")], defaultAsFlag: "s",
help: "Different short name.")
var shortOnly: String?
}
func testDefaultAsFlagWithShortNames() {
AssertHelp(
.default, for: DefaultAsFlagWithShortNames.self,
equals: """
USAGE: default_as_flag_with_short_names [--short-and-long [<short-and-long>]] [-o [<o>]]
OPTIONS:
-s, --short-and-long [<short-and-long>]
Short and long names. (default as flag: short)
-o [<o>] Different short name. (default as flag: s)
-h, --help Show help information.
""")
}
struct MixedOptionTypes: ParsableArguments {
@Flag(help: "A regular flag.")
var flag: Bool = false
@Option(defaultAsFlag: "mixed", help: "A defaultAsFlag option.")
var defaultAsFlag: String?
@Option(help: "A regular option.")
var regular: String?
@Argument(help: "A positional argument.")
var positional: String?
}
func testMixedOptionTypes() {
AssertHelp(
.default, for: MixedOptionTypes.self,
equals: """
USAGE: mixed_option_types [--flag] [--default-as-flag [<default-as-flag>]] [--regular <regular>] [<positional>]
ARGUMENTS:
<positional> A positional argument.
OPTIONS:
--flag A regular flag.
--default-as-flag [<default-as-flag>]
A defaultAsFlag option. (default as flag: mixed)
--regular <regular> A regular option.
-h, --help Show help information.
""")
}
}
@@ -0,0 +1,217 @@
#!/bin/bash
__defaultasflag-test_cursor_index_in_current_word() {
local remaining="${COMP_LINE}"
local word
for word in "${COMP_WORDS[@]::COMP_CWORD}"; do
remaining="${remaining##*([[:space:]])"${word}"*([[:space:]])}"
done
local -ir index="$((COMP_POINT - ${#COMP_LINE} + ${#remaining}))"
if [[ "${index}" -le 0 ]]; then
printf 0
else
printf %s "${index}"
fi
}
# positional arguments:
#
# - 1: the current (sub)command's count of positional arguments
#
# required variables:
#
# - repeating_flags: the repeating flags that the current (sub)command can accept
# - non_repeating_flags: the non-repeating flags that the current (sub)command can accept
# - repeating_options: the repeating options that the current (sub)command can accept
# - non_repeating_options: the non-repeating options that the current (sub)command can accept
# - positional_number: value ignored
# - unparsed_words: unparsed words from the current command line
#
# modified variables:
#
# - non_repeating_flags: remove flags for this (sub)command that are already on the command line
# - non_repeating_options: remove options for this (sub)command that are already on the command line
# - positional_number: set to the current positional number
# - unparsed_words: remove all flags, options, and option values for this (sub)command
__defaultasflag-test_offer_flags_options() {
local -ir positional_count="${1}"
positional_number=0
local was_flag_option_terminator_seen=false
local is_parsing_option_value=false
local -ar unparsed_word_indices=("${!unparsed_words[@]}")
local -i word_index
for word_index in "${unparsed_word_indices[@]}"; do
if "${is_parsing_option_value}"; then
# This word is an option value:
# Reset marker for next word iff not currently the last word
[[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]] && is_parsing_option_value=false
unset "unparsed_words[${word_index}]"
# Do not process this word as a flag or an option
continue
fi
local word="${unparsed_words["${word_index}"]}"
if ! "${was_flag_option_terminator_seen}"; then
case "${word}" in
--)
unset "unparsed_words[${word_index}]"
# by itself -- is a flag/option terminator, but if it is the last word, it is the start of a completion
if [[ "${word_index}" -ne "${unparsed_word_indices[${#unparsed_word_indices[@]} - 1]}" ]]; then
was_flag_option_terminator_seen=true
fi
continue
;;
-*)
# ${word} is a flag or an option
# If ${word} is an option, mark that the next word to be parsed is an option value
local option
for option in "${repeating_options[@]}" "${non_repeating_options[@]}"; do
[[ "${word}" = "${option}" ]] && is_parsing_option_value=true && break
done
# Remove ${word} from ${non_repeating_flags} or ${non_repeating_options} so it isn't offered again
local not_found=true
local -i index
for index in "${!non_repeating_flags[@]}"; do
if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then
unset "non_repeating_flags[${index}]"
non_repeating_flags=("${non_repeating_flags[@]}")
not_found=false
break
fi
done
if "${not_found}"; then
for index in "${!non_repeating_flags[@]}"; do
if [[ "${non_repeating_flags[${index}]}" = "${word}" ]]; then
unset "non_repeating_flags[${index}]"
non_repeating_flags=("${non_repeating_flags[@]}")
break
fi
done
fi
unset "unparsed_words[${word_index}]"
continue
;;
esac
fi
# ${word} is neither a flag, nor an option, nor an option value
if [[ "${positional_number}" -lt "${positional_count}" || "${positional_count}" -lt 0 ]]; then
# ${word} is a positional
((positional_number++))
unset "unparsed_words[${word_index}]"
else
if [[ -z "${word}" ]]; then
# Could be completing a flag, option, or subcommand
positional_number=-1
else
# ${word} is a subcommand or invalid, so stop processing this (sub)command
positional_number=-2
fi
break
fi
done
unparsed_words=("${unparsed_words[@]}")
if\
! "${was_flag_option_terminator_seen}"\
&& ! "${is_parsing_option_value}"\
&& [[ ("${cur}" = -* && "${positional_number}" -ge 0) || "${positional_number}" -eq -1 ]]
then
COMPREPLY+=($(compgen -W "${repeating_flags[*]} ${non_repeating_flags[*]} ${repeating_options[*]} ${non_repeating_options[*]}" -- "${cur}"))
fi
}
__defaultasflag-test_add_completions() {
local completion
while IFS='' read -r completion; do
COMPREPLY+=("${completion}")
done < <(IFS=$'\n' compgen "${@}" -- "${cur}")
}
__defaultasflag-test_custom_complete() {
if [[ -n "${cur}" || -z ${COMP_WORDS[${COMP_CWORD}]} || "${COMP_LINE:${COMP_POINT}:1}" != ' ' ]]; then
local -ar words=("${COMP_WORDS[@]}")
else
local -ar words=("${COMP_WORDS[@]::${COMP_CWORD}}" '' "${COMP_WORDS[@]:${COMP_CWORD}}")
fi
"${COMP_WORDS[0]}" "${@}" "${words[@]}"
}
_defaultasflag-test() {
local state
state="$(shopt -p;shopt -po)"
trap "${state//$'\n'/;}" RETURN
shopt -s extglob
set +o history +o posix
local -xr SAP_SHELL=bash
local -x SAP_SHELL_VERSION
SAP_SHELL_VERSION="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")"
local -r SAP_SHELL_VERSION
local -r cur="${2}"
local -r prev="${3}"
local -i positional_number
local -a unparsed_words=("${COMP_WORDS[@]:1:${COMP_CWORD}}")
local -a repeating_flags=()
local -a non_repeating_flags=(--help -h --help)
local -a repeating_options=()
local -a non_repeating_options=(--bin-path --count --verbose --log-level)
__defaultasflag-test_offer_flags_options 1
# Offer option value completions
case "${prev}" in
'--bin-path')
__defaultasflag-test_add_completions -d
return
;;
'--count')
return
;;
'--verbose')
return
;;
'--log-level')
__defaultasflag-test_add_completions -W 'DEBUG'$'\n''INFO'$'\n''WARN'$'\n''ERROR'
return
;;
esac
# Offer positional completions
case "${positional_number}" in
1)
__defaultasflag-test_add_completions -f
return
;;
esac
# Offer subcommand / subcommand argument completions
local -r subcommand="${unparsed_words[0]}"
unset 'unparsed_words[0]'
unparsed_words=("${unparsed_words[@]}")
case "${subcommand}" in
help)
# Offer subcommand argument completions
"_defaultasflag-test_${subcommand}"
;;
*)
# Offer subcommand completions
COMPREPLY+=($(compgen -W 'help' -- "${cur}"))
;;
esac
}
_defaultasflag-test_help() {
:
}
complete -o filenames -F _defaultasflag-test defaultasflag-test
@@ -0,0 +1,101 @@
function __defaultasflag-test_should_offer_completions_for_flags_or_options -a expected_commands
set -l non_repeating_flags_or_options $argv[2..]
set -l non_repeating_flags_or_options_absent 0
set -l positional_index 0
set -l commands
__defaultasflag-test_parse_tokens
test "$commands" = "$expected_commands"; and return $non_repeating_flags_or_options_absent
end
function __defaultasflag-test_should_offer_completions_for_positional -a expected_commands expected_positional_index positional_index_comparison
if test -z $positional_index_comparison
set positional_index_comparison -eq
end
set -l non_repeating_flags_or_options
set -l non_repeating_flags_or_options_absent 0
set -l positional_index 0
set -l commands
__defaultasflag-test_parse_tokens
test "$commands" = "$expected_commands" -a \( "$positional_index" "$positional_index_comparison" "$expected_positional_index" \)
end
function __defaultasflag-test_parse_tokens -S
set -l unparsed_tokens (__defaultasflag-test_tokens -pc)
set -l present_flags_and_options
switch $unparsed_tokens[1]
case 'defaultasflag-test'
__defaultasflag-test_parse_subcommand 1 'bin-path=' 'count=' 'verbose=' 'log-level=' 'help' 'h/help'
switch $unparsed_tokens[1]
case 'help'
__defaultasflag-test_parse_subcommand -r 1
end
end
end
function __defaultasflag-test_tokens
if test (string split -m 1 -f 1 -- . "$FISH_VERSION") -gt 3
commandline --tokens-raw $argv
else
commandline -o $argv
end
end
function __defaultasflag-test_parse_subcommand -S -a positional_count
argparse -s r -- $argv
set -l option_specs $argv[2..]
set -a commands $unparsed_tokens[1]
set -e unparsed_tokens[1]
set positional_index 0
while true
argparse -sn "$commands" $option_specs -- $unparsed_tokens 2> /dev/null
set unparsed_tokens $argv
set positional_index (math $positional_index + 1)
for non_repeating_flag_or_option in $non_repeating_flags_or_options
if set -ql _flag_$non_repeating_flag_or_option
set non_repeating_flags_or_options_absent 1
break
end
end
if test (count $unparsed_tokens) -eq 0 -o \( -z "$_flag_r" -a "$positional_index" -gt "$positional_count" \)
break
end
set -e unparsed_tokens[1]
end
end
function __defaultasflag-test_complete_directories
set -l token (commandline -t)
string match -- '*/' $token
set -l subdirs $token*/
printf '%s\n' $subdirs
end
function __defaultasflag-test_custom_completion
set -x SAP_SHELL fish
set -x SAP_SHELL_VERSION $FISH_VERSION
set -l tokens (__defaultasflag-test_tokens -p)
if test -z (__defaultasflag-test_tokens -t)
set -l index (count (__defaultasflag-test_tokens -pc))
set tokens $tokens[..$index] \'\' $tokens[(math $index + 1)..]
end
command $tokens[1] $argv $tokens
end
complete -c 'defaultasflag-test' -f
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_flags_or_options "defaultasflag-test" bin-path' -l 'bin-path' -rfa '(__defaultasflag-test_complete_directories)'
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_flags_or_options "defaultasflag-test" count' -l 'count' -rfka ''
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_flags_or_options "defaultasflag-test" verbose' -l 'verbose' -rfka ''
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_flags_or_options "defaultasflag-test" log-level' -l 'log-level' -rfka 'DEBUG INFO WARN ERROR'
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_flags_or_options "defaultasflag-test" help' -l 'help'
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_positional "defaultasflag-test" 1' -F
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_flags_or_options "defaultasflag-test" h help' -s 'h' -l 'help' -d 'Show help information.'
complete -c 'defaultasflag-test' -n '__defaultasflag-test_should_offer_completions_for_positional "defaultasflag-test" 2' -fa 'help' -d 'Show subcommand help information.'
@@ -0,0 +1,89 @@
#compdef defaultasflag-test
__defaultasflag-test_complete() {
local -ar non_empty_completions=("${@:#(|:*)}")
local -ar empty_completions=("${(M)@:#(|:*)}")
_describe -V '' non_empty_completions -- empty_completions -P $'\'\''
}
__defaultasflag-test_custom_complete() {
local -a completions
completions=("${(@f)"$("${command_name}" "${@}" "${command_line[@]}")"}")
if [[ "${#completions[@]}" -gt 1 ]]; then
__defaultasflag-test_complete "${completions[@]:0:-1}"
fi
}
__defaultasflag-test_cursor_index_in_current_word() {
if [[ -z "${QIPREFIX}${IPREFIX}${PREFIX}" ]]; then
printf 0
else
printf %s "${#${(z)LBUFFER}[-1]}"
fi
}
_defaultasflag-test() {
emulate -RL zsh -G
setopt extendedglob nullglob numericglobsort
unsetopt aliases banghist
local -xr SAP_SHELL=zsh
local -x SAP_SHELL_VERSION
SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')"
local -r SAP_SHELL_VERSION
local context state state_descr line
local -A opt_args
local -r command_name="${words[1]}"
local -ar command_line=("${words[@]}")
local -ir current_word_index="$((CURRENT - 1))"
local -i ret=1
local -ar ___log_level=('DEBUG' 'INFO' 'WARN' 'ERROR')
local -ar arg_specs=(
'--bin-path:bin-path:_files -/'
'--count:count:'
'--verbose:verbose:'
'--log-level:log-level:{__defaultasflag-test_complete "${___log_level[@]}"}'
'--help'
':input:_files'
'(-h --help)'{-h,--help}'[Show help information.]'
'(-): :->command'
'(-)*:: :->arg'
)
_arguments -w -s -S : "${arg_specs[@]}" && ret=0
case "${state}" in
command)
local -ar subcommands=(
'help:Show subcommand help information.'
)
_describe -V subcommand subcommands && ret=0
;;
arg)
case "${words[1]}" in
help)
"_defaultasflag-test_${words[1]}" && ret=0
;;
esac
;;
esac
return "${ret}"
}
_defaultasflag-test_help() {
local -i ret=1
local -ar arg_specs=(
'*:subcommands:'
)
_arguments -w -s -S : "${arg_specs[@]}" && ret=0
return "${ret}"
}
if [[ "${funcstack[1]}" = _defaultasflag-test ]]; then
_defaultasflag-test "${@}"
else
compdef _defaultasflag-test defaultasflag-test
fi
@@ -0,0 +1,144 @@
{
"command" : {
"abstract" : "A command with defaultAsFlag options for testing dump help.",
"arguments" : [
{
"defaultValue" : "\/usr\/bin",
"isOptional" : true,
"isRepeating" : false,
"kind" : "option",
"names" : [
{
"kind" : "long",
"name" : "binary-path"
}
],
"parsingStrategy" : "scanningForValue",
"preferredName" : {
"kind" : "long",
"name" : "binary-path"
},
"shouldDisplay" : true,
"valueName" : "binary-path"
},
{
"defaultValue" : "42",
"isOptional" : true,
"isRepeating" : false,
"kind" : "option",
"names" : [
{
"kind" : "long",
"name" : "count"
}
],
"parsingStrategy" : "scanningForValue",
"preferredName" : {
"kind" : "long",
"name" : "count"
},
"shouldDisplay" : true,
"valueName" : "count"
},
{
"defaultValue" : "true",
"isOptional" : true,
"isRepeating" : false,
"kind" : "option",
"names" : [
{
"kind" : "long",
"name" : "verbose"
}
],
"parsingStrategy" : "scanningForValue",
"preferredName" : {
"kind" : "long",
"name" : "verbose"
},
"shouldDisplay" : true,
"valueName" : "verbose"
},
{
"isOptional" : false,
"isRepeating" : false,
"kind" : "positional",
"parsingStrategy" : "default",
"shouldDisplay" : true,
"valueName" : "input"
},
{
"abstract" : "Show help information.",
"isOptional" : true,
"isRepeating" : false,
"kind" : "flag",
"names" : [
{
"kind" : "short",
"name" : "h"
},
{
"kind" : "long",
"name" : "help"
}
],
"parsingStrategy" : "default",
"preferredName" : {
"kind" : "long",
"name" : "help"
},
"shouldDisplay" : true,
"valueName" : "help"
}
],
"commandName" : "default-as-flag-command",
"shouldDisplay" : true,
"subcommands" : [
{
"abstract" : "Show subcommand help information.",
"arguments" : [
{
"isOptional" : true,
"isRepeating" : true,
"kind" : "positional",
"parsingStrategy" : "default",
"shouldDisplay" : true,
"valueName" : "subcommands"
},
{
"isOptional" : true,
"isRepeating" : false,
"kind" : "flag",
"names" : [
{
"kind" : "short",
"name" : "h"
},
{
"kind" : "long",
"name" : "help"
},
{
"kind" : "longWithSingleDash",
"name" : "help"
}
],
"parsingStrategy" : "default",
"preferredName" : {
"kind" : "long",
"name" : "help"
},
"shouldDisplay" : false,
"valueName" : "help"
}
],
"commandName" : "help",
"shouldDisplay" : true,
"superCommands" : [
"default-as-flag-command"
]
}
]
},
"serializationVersion" : 0
}
@@ -0,0 +1,135 @@
{
"command" : {
"abstract" : "A command with defaultAsFlag options using transforms for testing dump help.",
"arguments" : [
{
"defaultValue" : "\/default\/output",
"isOptional" : true,
"isRepeating" : false,
"kind" : "option",
"names" : [
{
"kind" : "long",
"name" : "output-dir"
}
],
"parsingStrategy" : "scanningForValue",
"preferredName" : {
"kind" : "long",
"name" : "output-dir"
},
"shouldDisplay" : true,
"valueName" : "output-dir"
},
{
"defaultValue" : "INFO",
"isOptional" : true,
"isRepeating" : false,
"kind" : "option",
"names" : [
{
"kind" : "long",
"name" : "level"
}
],
"parsingStrategy" : "scanningForValue",
"preferredName" : {
"kind" : "long",
"name" : "level"
},
"shouldDisplay" : true,
"valueName" : "level"
},
{
"isOptional" : true,
"isRepeating" : false,
"kind" : "flag",
"names" : [
{
"kind" : "long",
"name" : "debug"
}
],
"parsingStrategy" : "default",
"preferredName" : {
"kind" : "long",
"name" : "debug"
},
"shouldDisplay" : true,
"valueName" : "debug"
},
{
"abstract" : "Show help information.",
"isOptional" : true,
"isRepeating" : false,
"kind" : "flag",
"names" : [
{
"kind" : "short",
"name" : "h"
},
{
"kind" : "long",
"name" : "help"
}
],
"parsingStrategy" : "default",
"preferredName" : {
"kind" : "long",
"name" : "help"
},
"shouldDisplay" : true,
"valueName" : "help"
}
],
"commandName" : "default-as-flag-with-transform-command",
"shouldDisplay" : true,
"subcommands" : [
{
"abstract" : "Show subcommand help information.",
"arguments" : [
{
"isOptional" : true,
"isRepeating" : true,
"kind" : "positional",
"parsingStrategy" : "default",
"shouldDisplay" : true,
"valueName" : "subcommands"
},
{
"isOptional" : true,
"isRepeating" : false,
"kind" : "flag",
"names" : [
{
"kind" : "short",
"name" : "h"
},
{
"kind" : "long",
"name" : "help"
},
{
"kind" : "longWithSingleDash",
"name" : "help"
}
],
"parsingStrategy" : "default",
"preferredName" : {
"kind" : "long",
"name" : "help"
},
"shouldDisplay" : false,
"valueName" : "help"
}
],
"commandName" : "help",
"shouldDisplay" : true,
"superCommands" : [
"default-as-flag-with-transform-command"
]
}
]
},
"serializationVersion" : 0
}