Skip to content

Commit d3593e3

Browse files
committed
Augment Option to support default as flag option
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: json) ```` Note the `(default as flag: ...)` text instead of regular `(default: ...)`, and the optional value syntax `[<value>]` instead of required `<value>`. Fixes: #829
1 parent 1fb5308 commit d3593e3

24 files changed

+1881
-14
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import ArgumentParser
13+
14+
@main
15+
struct DefaultAsFlag: ParsableCommand {
16+
static let configuration = CommandConfiguration(
17+
abstract: "A utility demonstrating defaultAsFlag options.",
18+
discussion: """
19+
This command shows how defaultAsFlag options can work both as flags
20+
and as options with values.
21+
"""
22+
)
23+
24+
@Option(defaultAsFlag: "default", help: "A string option with defaultAsFlag.")
25+
var stringFlag: String?
26+
27+
@Option(defaultAsFlag: 42, help: "An integer option with defaultAsFlag.")
28+
var numberFlag: Int?
29+
30+
@Option(defaultAsFlag: true, help: "A boolean option with defaultAsFlag.")
31+
var boolFlag: Bool?
32+
33+
@Option(
34+
defaultAsFlag: "transformed",
35+
help: "A string option with transform and defaultAsFlag.",
36+
transform: { $0.uppercased() },
37+
)
38+
var transformFlag: String?
39+
40+
@Option(name: .shortAndLong, help: "A regular option for comparison.")
41+
var regular: String?
42+
43+
@Argument
44+
var additionalArgs: [String] = []
45+
46+
func run() {
47+
print("String flag: \(stringFlag?.description ?? "nil")")
48+
print("Number flag: \(numberFlag?.description ?? "nil")")
49+
print("Bool flag: \(boolFlag?.description ?? "nil")")
50+
print("Transform flag: \(transformFlag?.description ?? "nil")")
51+
print("Regular option: \(regular?.description ?? "nil")")
52+
print("Additional args: \(additionalArgs)")
53+
}
54+
}

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ var package = Package(
7878
name: "color",
7979
dependencies: ["ArgumentParser"],
8080
path: "Examples/color"),
81+
.executableTarget(
82+
name: "default-as-flag",
83+
dependencies: ["ArgumentParser"],
84+
path: "Examples/default-as-flag",
85+
),
8186

8287
// Tools
8388
.executableTarget(

Package@swift-5.8.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ var package = Package(
7979
name: "color",
8080
dependencies: ["ArgumentParser"],
8181
path: "Examples/color"),
82+
.executableTarget(
83+
name: "default-as-flag",
84+
dependencies: ["ArgumentParser"],
85+
path: "Examples/default-as-flag",
86+
),
8287

8388
// Tools
8489
.executableTarget(

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ that you need to collect from the command line.
77
Decorate each stored property with one of `ArgumentParser`'s property wrappers,
88
and then declare conformance to `ParsableCommand` and add the `@main` attribute.
99
(Note, for `async` renditions of `run`, conform to `AsyncParsableCommand` rather
10-
than `ParsableCommand`.)
10+
than `ParsableCommand`.)
1111
Finally, implement your command's logic in the `run()` method.
1212

1313
```swift
@@ -70,7 +70,7 @@ OPTIONS:
7070

7171
## Documentation
7272

73-
For guides, articles, and API documentation see the
73+
For guides, articles, and API documentation see the
7474
[library's documentation on the Web][docs] or in Xcode.
7575

7676
- [ArgumentParser documentation][docs]
@@ -88,6 +88,7 @@ This repository includes a few examples of using the library:
8888
- [`roll`](Examples/roll/main.swift) is a simple utility implemented as a straight-line script.
8989
- [`math`](Examples/math/Math.swift) is an annotated example of using nested commands and subcommands.
9090
- [`count-lines`](Examples/count-lines/CountLines.swift) uses `async`/`await` code in its implementation.
91+
- [`default-as-flag`](Examples/default-as-flag/DefaultAsFlag.swift) demonstrates hybrid options that can work both as flags and as options with values.
9192

9293
You can also see examples of `ArgumentParser` adoption among Swift project tools:
9394

@@ -104,7 +105,7 @@ The public API of version 1.0.0 of the `swift-argument-parser` package
104105
consists of non-underscored declarations that are marked public in the `ArgumentParser` module.
105106
Interfaces that aren't part of the public API may continue to change in any release,
106107
including the exact wording and formatting of the autogenerated help and error messages,
107-
as well as the package’s examples, tests, utilities, and documentation.
108+
as well as the package’s examples, tests, utilities, and documentation.
108109

109110
Future minor versions of the package may introduce changes to these rules as needed.
110111

@@ -115,7 +116,7 @@ Requiring a new Swift release will only require a minor version bump.
115116

116117
## Adding `ArgumentParser` as a Dependency
117118

118-
To use the `ArgumentParser` library in a SwiftPM project,
119+
To use the `ArgumentParser` library in a SwiftPM project,
119120
add it to the dependencies for your package and your command-line executable target:
120121

121122
```swift

Sources/ArgumentParser/Documentation.docc/Articles/DeclaringArguments.md

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ struct Lucky: ParsableCommand {
8181
```
8282

8383
```
84-
% lucky
84+
% lucky
8585
Your lucky numbers are:
8686
7 14 21
8787
% lucky 1 2 3
@@ -327,6 +327,89 @@ If a default is not specified, the user must provide a value for that argument/o
327327
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.
328328

329329

330+
### Creating hybrid flag/option behavior with defaultAsFlag
331+
332+
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.
333+
334+
```swift
335+
struct Example: ParsableCommand {
336+
@Option(defaultAsFlag: "default", help: "Set output format.")
337+
var format: String?
338+
339+
@Option(defaultAsFlag: 8080, help: "Server port.")
340+
var port: Int?
341+
342+
func run() {
343+
print("Format: \(format ?? "none")")
344+
print("Port: \(port ?? 3000)")
345+
}
346+
}
347+
```
348+
349+
**Command-line behavior:**
350+
```
351+
% example # format = nil, port = nil
352+
% example --format # format = "default", port = nil
353+
% example --format json # format = "json", port = nil
354+
% example --port # format = nil, port = 8080
355+
% example --port 9000 # format = nil, port = 9000
356+
```
357+
358+
The `defaultAsFlag` parameter creates a hybrid that supports both patterns:
359+
- **Flag behavior**: `--format` (sets format to "default")
360+
- **Option behavior**: `--format json` (sets format to "json")
361+
- **No usage**: format remains `nil`
362+
363+
#### Type requirements
364+
365+
- The property **must** be optional (`T?`)
366+
- The `defaultAsFlag` value must be of the unwrapped type (`T`)
367+
- All standard `ExpressibleByArgument` types are supported (String, Int, Bool, Double, etc.)
368+
369+
#### Advanced usage
370+
371+
You can combine `defaultAsFlag` with transform functions:
372+
373+
```swift
374+
@Option(
375+
defaultAsFlag: "info",
376+
help: "Set log level.",
377+
transform: { $0.uppercased() }
378+
)
379+
var logLevel: String?
380+
```
381+
382+
**Behavior:**
383+
```
384+
% app --log-level # logLevel = "INFO" (transformed default)
385+
% app --log-level debug # logLevel = "DEBUG" (transformed input)
386+
```
387+
388+
#### Help display
389+
390+
DefaultAsFlag options show special help formatting to distinguish them from regular defaults:
391+
392+
```
393+
OPTIONS:
394+
--format [<format>] Set output format. (default as flag: json)
395+
--port [<port>] Server port. (default as flag: 8080)
396+
```
397+
398+
Note the `(default as flag: ...)` text instead of regular `(default: ...)`, and the optional value syntax `[<value>]` instead of required `<value>`.
399+
400+
#### Value detection
401+
402+
The parser determines whether a value follows the option:
403+
404+
1. **Next argument is a value** if it doesn't start with `-` and isn't another known option
405+
2. **No value available**: Use the `defaultAsFlag` value
406+
3. **Explicit value provided**: Parse and use that value
407+
408+
This works with all parsing strategies (`.next`, `.scanningForValue`, `.unconditional`), though `.unconditional` defeats the purpose by always requiring a value.
409+
410+
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.
411+
412+
330413
### Specifying a parsing strategy
331414

332415
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 +562,7 @@ When appropriate, you can process supported arguments and ignore unknown ones by
479562
```swift
480563
struct Example: ParsableCommand {
481564
@Flag var verbose = false
482-
565+
483566
@Argument(parsing: .allUnrecognized)
484567
var unknowns: [String] = []
485568

0 commit comments

Comments
 (0)