Skip to content

Commit d08351c

Browse files
committed
Add new FlagOption type
Add a new FlagOption type that can be used as a flag or as an option. Fixes: #829
1 parent 1fb5308 commit d08351c

File tree

5 files changed

+1429
-0
lines changed

5 files changed

+1429
-0
lines changed

.github/workflows/pull_request.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
2626
tests:
2727
name: Test
28+
needs: [soundness, validate_format_config]
2829
uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
2930
with:
3031
windows_exclude_swift_versions: "[{\"swift_version\": \"5.9\"}]"
@@ -35,6 +36,7 @@ jobs:
3536

3637
cmake-build:
3738
name: CMake Build
39+
needs: [soundness, validate_format_config]
3840
runs-on: ubuntu-latest
3941
container:
4042
image: swift:6.0-jammy
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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+
/// A property wrapper that represents a command-line argument that can work as both a flag and an option.
13+
///
14+
/// Use the `@FlagOption` wrapper to define a property that can be used in two ways:
15+
/// 1. As a flag (without a value): `--show-bin-path` - uses the default value
16+
/// 2. As an option (with a value): `--show-bin-path json` - uses the provided value
17+
///
18+
/// This provides backward compatibility for flags while allowing optional values.
19+
///
20+
/// For example, the following program declares a flag-option that defaults to "text"
21+
/// when used as a flag, but accepts custom values when used as an option:
22+
///
23+
/// ```swift
24+
/// @main
25+
/// struct Tool: ParsableCommand {
26+
/// @FlagOption(defaultValue: "text")
27+
/// var showBinPath: String
28+
///
29+
/// mutating func run() {
30+
/// print("Format: \(showBinPath)")
31+
/// }
32+
/// }
33+
/// ```
34+
///
35+
/// This allows both usage patterns:
36+
/// - `tool --show-bin-path` outputs "Format: text"
37+
/// - `tool --show-bin-path json` outputs "Format: json"
38+
///
39+
@propertyWrapper
40+
public struct FlagOption<Value>: Decodable, ParsedWrapper {
41+
internal var _parsedValue: Parsed<Value>
42+
43+
internal init(_parsedValue: Parsed<Value>) {
44+
self._parsedValue = _parsedValue
45+
}
46+
47+
public init(from _decoder: Decoder) throws {
48+
try self.init(_decoder: _decoder)
49+
}
50+
51+
/// This initializer works around a quirk of property wrappers, where the
52+
/// compiler will not see no-argument initializers in extensions.
53+
@available(
54+
*, unavailable,
55+
message: "A default value must be provided for FlagOption."
56+
)
57+
public init() {
58+
fatalError("unavailable")
59+
}
60+
61+
/// The value presented by this property wrapper.
62+
public var wrappedValue: Value {
63+
get {
64+
switch _parsedValue {
65+
case .value(let v):
66+
return v
67+
case .definition:
68+
configurationFailure(directlyInitializedError)
69+
}
70+
}
71+
set {
72+
_parsedValue = .value(newValue)
73+
}
74+
}
75+
}
76+
77+
extension FlagOption: CustomStringConvertible {
78+
public var description: String {
79+
switch _parsedValue {
80+
case .value(let v):
81+
return String(describing: v)
82+
case .definition:
83+
return "FlagOption(*definition*)"
84+
}
85+
}
86+
}
87+
88+
extension FlagOption: Sendable where Value: Sendable {}
89+
extension FlagOption: DecodableParsedWrapper where Value: Decodable {}
90+
91+
// MARK: - @FlagOption T: ExpressibleByArgument Initializers
92+
93+
extension FlagOption where Value: ExpressibleByArgument {
94+
/// Creates a flag-option property with a default value that is used when
95+
/// the argument is specified as a flag without a value.
96+
///
97+
/// - Parameters:
98+
/// - defaultValue: The value to use when the argument is specified as a flag
99+
/// - wrappedValue: The initial value for the property
100+
/// - name: A specification for what names are allowed for this flag-option
101+
/// - help: Information about how to use this flag-option
102+
/// - completion: The type of command-line completion provided for this option
103+
public init(
104+
defaultValue: Value,
105+
wrappedValue: Value,
106+
name: NameSpecification = .long,
107+
help: ArgumentHelp? = nil,
108+
completion: CompletionKind? = nil
109+
) {
110+
self.init(
111+
_parsedValue: .init { key in
112+
let arg = ArgumentDefinition(
113+
container: Bare<Value>.self,
114+
key: key,
115+
kind: .name(key: key, specification: name),
116+
help: .init(
117+
help?.abstract ?? "",
118+
discussion: help?.discussion,
119+
valueName: help?.valueName,
120+
visibility: help?.visibility ?? .default,
121+
argumentType: Value.self
122+
),
123+
parsingStrategy: .scanningForValue,
124+
initial: wrappedValue,
125+
completion: completion)
126+
127+
return ArgumentSet(arg)
128+
})
129+
}
130+
131+
/// Creates a flag-option property with a default value.
132+
///
133+
/// - Parameters:
134+
/// - defaultValue: The value to use when the argument is specified as a flag
135+
/// - name: A specification for what names are allowed for this flag-option
136+
/// - help: Information about how to use this flag-option
137+
/// - completion: The type of command-line completion provided for this option
138+
public init(
139+
defaultValue: Value,
140+
name: NameSpecification = .long,
141+
help: ArgumentHelp? = nil,
142+
completion: CompletionKind? = nil
143+
) {
144+
self.init(
145+
_parsedValue: .init { key in
146+
let arg = ArgumentDefinition(
147+
container: Bare<Value>.self,
148+
key: key,
149+
kind: .name(key: key, specification: name),
150+
help: .init(
151+
help?.abstract ?? "",
152+
discussion: help?.discussion,
153+
valueName: help?.valueName,
154+
visibility: help?.visibility ?? .default,
155+
argumentType: Value.self
156+
),
157+
parsingStrategy: .scanningForValue,
158+
initial: nil,
159+
completion: completion)
160+
161+
return ArgumentSet(arg)
162+
})
163+
}
164+
}
165+
166+
// MARK: - @FlagOption T Initializers (with transform)
167+
168+
extension FlagOption {
169+
/// Creates a flag-option property with a transform closure and default value.
170+
///
171+
/// - Parameters:
172+
/// - defaultValue: The value to use when the argument is specified as a flag
173+
/// - wrappedValue: The initial value for the property
174+
/// - name: A specification for what names are allowed for this flag-option
175+
/// - help: Information about how to use this flag-option
176+
/// - completion: The type of command-line completion provided for this option
177+
/// - transform: A closure that converts a string into this property's type
178+
@preconcurrency
179+
public init(
180+
defaultValue: Value,
181+
wrappedValue: Value,
182+
name: NameSpecification = .long,
183+
help: ArgumentHelp? = nil,
184+
completion: CompletionKind? = nil,
185+
transform: @Sendable @escaping (String) throws -> Value
186+
) {
187+
self.init(
188+
_parsedValue: .init { key in
189+
let arg = ArgumentDefinition(
190+
container: Bare<Value>.self,
191+
key: key,
192+
kind: .name(key: key, specification: name),
193+
help: help,
194+
parsingStrategy: .scanningForValue,
195+
transform: transform,
196+
initial: wrappedValue,
197+
completion: completion)
198+
199+
return ArgumentSet(arg)
200+
})
201+
}
202+
203+
/// Creates a flag-option property with a transform closure and default value.
204+
///
205+
/// - Parameters:
206+
/// - defaultValue: The value to use when the argument is specified as a flag
207+
/// - name: A specification for what names are allowed for this flag-option
208+
/// - help: Information about how to use this flag-option
209+
/// - completion: The type of command-line completion provided for this option
210+
/// - transform: A closure that converts a string into this property's type
211+
@preconcurrency
212+
public init(
213+
defaultValue: Value,
214+
name: NameSpecification = .long,
215+
help: ArgumentHelp? = nil,
216+
completion: CompletionKind? = nil,
217+
transform: @Sendable @escaping (String) throws -> Value
218+
) {
219+
self.init(
220+
_parsedValue: .init { key in
221+
let arg = ArgumentDefinition(
222+
container: Bare<Value>.self,
223+
key: key,
224+
kind: .name(key: key, specification: name),
225+
help: help,
226+
parsingStrategy: .scanningForValue,
227+
transform: transform,
228+
initial: nil,
229+
completion: completion)
230+
231+
return ArgumentSet(arg)
232+
})
233+
}
234+
}
235+
236+
// MARK: - @FlagOption Optional<T> Initializers
237+
238+
extension FlagOption {
239+
/// Creates an optional flag-option property with a default value.
240+
///
241+
/// - Parameters:
242+
/// - defaultValue: The value to use when the argument is specified as a flag
243+
/// - wrappedValue: The initial value for the property
244+
/// - name: A specification for what names are allowed for this flag-option
245+
/// - help: Information about how to use this flag-option
246+
/// - completion: The type of command-line completion provided for this option
247+
public init<T>(
248+
defaultValue: T,
249+
wrappedValue: T?,
250+
name: NameSpecification = .long,
251+
help: ArgumentHelp? = nil,
252+
completion: CompletionKind? = nil
253+
) where T: ExpressibleByArgument, Value == T? {
254+
self.init(
255+
_parsedValue: .init { key in
256+
let arg = ArgumentDefinition(
257+
container: Optional<T>.self,
258+
key: key,
259+
kind: .name(key: key, specification: name),
260+
help: .init(
261+
help?.abstract ?? "",
262+
discussion: help?.discussion,
263+
valueName: help?.valueName,
264+
visibility: help?.visibility ?? .default,
265+
argumentType: T.self
266+
),
267+
parsingStrategy: .scanningForValue,
268+
initial: wrappedValue,
269+
completion: completion)
270+
271+
return ArgumentSet(arg)
272+
})
273+
}
274+
275+
/// Creates an optional flag-option property with a default value.
276+
///
277+
/// - Parameters:
278+
/// - defaultValue: The value to use when the argument is specified as a flag
279+
/// - name: A specification for what names are allowed for this flag-option
280+
/// - help: Information about how to use this flag-option
281+
/// - completion: The type of command-line completion provided for this option
282+
public init<T>(
283+
defaultValue: T,
284+
name: NameSpecification = .long,
285+
help: ArgumentHelp? = nil,
286+
completion: CompletionKind? = nil
287+
) where T: ExpressibleByArgument, Value == T? {
288+
self.init(
289+
_parsedValue: .init { key in
290+
let arg = ArgumentDefinition(
291+
container: Optional<T>.self,
292+
key: key,
293+
kind: .name(key: key, specification: name),
294+
help: .init(
295+
help?.abstract ?? "",
296+
discussion: help?.discussion,
297+
valueName: help?.valueName,
298+
visibility: help?.visibility ?? .default,
299+
argumentType: T.self
300+
),
301+
parsingStrategy: .scanningForValue,
302+
initial: nil,
303+
completion: completion)
304+
305+
return ArgumentSet(arg)
306+
})
307+
}
308+
}

0 commit comments

Comments
 (0)