Skip to content

Commit e870503

Browse files
natecook1000Kyle Macomber
andauthored
Add an EnumerableFlag protocol (#65)
* Add EnumerableFlag protocol This addresses the need for providing name specifications for enum flags, since property wrappers can't be used for enum cases. * Incorporate updated flag-handling logic * Include test of multiple names for enumerable flags * Add documentation for EnumerableFlag protocol * Add `static func help(for:)` to EnumerableFlag * Update docs to cover `EnumerableFlag` * Update default value documentation * Revise the Flag type docs * Update Documentation/02 Arguments, Options, and Flags.md Co-authored-by: Kyle Macomber <[email protected]> Co-authored-by: Kyle Macomber <[email protected]>
1 parent 3166c45 commit e870503

File tree

9 files changed

+294
-32
lines changed

9 files changed

+294
-32
lines changed

Documentation/02 Arguments, Options, and Flags.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,15 @@ false false
211211
Error: Missing one of: '--enable-required-element', '--disable-required-element'
212212
```
213213

214-
You can also use flags with types that are `CaseIterable` and `RawRepresentable` with a string raw value. This is useful for providing custom names for a Boolean value, for an exclusive choice between more than two names, or for collecting multiple values from a set of defined choices.
214+
To create a flag with custom names for a Boolean value, to provide an exclusive choice between more than two names, or for collecting multiple values from a set of defined choices, define an enumeration that conforms to the `EnumerableFlag` protocol.
215215

216216
```swift
217-
enum CacheMethod: String, CaseIterable {
217+
enum CacheMethod: EnumerableFlag {
218218
case inMemoryCache
219219
case persistentCache
220220
}
221221

222-
enum Color: String, CaseIterable {
222+
enum Color: EnumerableFlag {
223223
case pink, purple, silver
224224
}
225225

@@ -235,7 +235,7 @@ struct Example: ParsableCommand {
235235
}
236236
```
237237

238-
The flag names in this case are drawn from the raw values:
238+
The flag names in this case are drawn from the raw values — for information about customizing the names and help text, see the [`EnumerableFlag` documentation](../Sources/ArgumentParser/Parsable%20Types/EnumerableFlag.swift).
239239

240240
```
241241
% example --in-memory-cache --pink --silver

Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public struct ArgumentHelp {
1919

2020
/// An alternative name to use for the argument's value when showing usage
2121
/// information.
22+
///
23+
/// - Note: This property is ignored when generating help for flags, since
24+
/// flags don't include a value.
2225
public var valueName: String?
2326

2427
/// A Boolean value indicating whether this argument should be shown in

Sources/ArgumentParser/Parsable Properties/Flag.swift

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@
2121
/// `verbose` has a default value of `false`, but becomes `true` if `--verbose`
2222
/// is provided on the command line.
2323
///
24-
/// A flag can have a value that is a `Bool`, an `Int`, or any `CaseIterable`
25-
/// type. When using a `CaseIterable` type as a flag, the individual cases
24+
/// A flag can have a value that is a `Bool`, an `Int`, or any `EnumerableFlag`
25+
/// type. When using an `EnumerableFlag` type as a flag, the individual cases
2626
/// form the flags that are used on the command line.
2727
///
2828
/// struct Options {
29-
/// enum Operation: CaseIterable, ... {
29+
/// enum Operation: EnumerableFlag {
3030
/// case add
3131
/// case multiply
3232
/// }
@@ -188,7 +188,9 @@ extension Flag where Value == Bool {
188188
///
189189
/// - Parameters:
190190
/// - name: A specification for what names are allowed for this flag.
191-
/// - initial: The default value for this flag.
191+
/// - initial: A default value to use for this property. If `initial` is
192+
/// `nil`, one of the flags declared by this `@Flag` attribute is required
193+
/// from the user.
192194
/// - inversion: The method for converting this flag's name into an on/off
193195
/// pair.
194196
/// - exclusivity: The behavior to use when an on/off pair of flags is
@@ -226,18 +228,20 @@ extension Flag where Value == Int {
226228
}
227229
}
228230

229-
extension Flag where Value: CaseIterable, Value: Equatable, Value: RawRepresentable, Value.RawValue == String {
231+
// - MARK: EnumerableFlag
232+
233+
extension Flag where Value: EnumerableFlag {
230234
/// Creates a property that gets its value from the presence of a flag,
231-
/// where the allowed flags are defined by a `CaseIterable` type.
235+
/// where the allowed flags are defined by an `EnumerableFlag` type.
232236
///
233237
/// - Parameters:
234238
/// - name: A specification for what names are allowed for this flag.
235239
/// - initial: A default value to use for this property. If `initial` is
236-
/// `nil`, this flag is required.
240+
/// `nil`, one of the flags declared by this `@Flag` attribute is required
241+
/// from the user.
237242
/// - exclusivity: The behavior to use when multiple flags are specified.
238243
/// - help: Information about how to use this flag.
239244
public init(
240-
name: NameSpecification = .long,
241245
default initial: Value? = nil,
242246
exclusivity: FlagExclusivity = .exclusive,
243247
help: ArgumentHelp? = nil
@@ -248,9 +252,14 @@ extension Flag where Value: CaseIterable, Value: Equatable, Value: RawRepresenta
248252
var hasUpdated = false
249253
let defaultValue = initial.map(String.init(describing:))
250254

251-
let args = Value.allCases.map { value -> ArgumentDefinition in
252-
let caseKey = InputKey(rawValue: value.rawValue)
253-
let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, defaultValue: defaultValue, key: key)
255+
let caseHelps = Value.allCases.map { Value.help(for: $0) }
256+
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
257+
258+
let args = Value.allCases.enumerated().map { (i, value) -> ArgumentDefinition in
259+
let caseKey = InputKey(rawValue: String(describing: value))
260+
let name = Value.name(for: value)
261+
let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help
262+
let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: helpForCase, defaultValue: defaultValue, key: key, isComposite: !hasCustomCaseHelp)
254263
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in
255264
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
256265
}))
@@ -264,15 +273,110 @@ extension Flag where Value: CaseIterable, Value: Equatable, Value: RawRepresenta
264273

265274
extension Flag {
266275
/// Creates a property that gets its value from the presence of a flag,
267-
/// where the allowed flags are defined by a `CaseIterable` type.
276+
/// where the allowed flags are defined by an `EnumerableFlag` type.
277+
public init<Element>(
278+
exclusivity: FlagExclusivity = .exclusive,
279+
help: ArgumentHelp? = nil
280+
) where Value == Element?, Element: EnumerableFlag {
281+
self.init(_parsedValue: .init { key in
282+
// This gets flipped to `true` the first time one of these flags is
283+
// encountered.
284+
var hasUpdated = false
285+
286+
let caseHelps = Element.allCases.map { Element.help(for: $0) }
287+
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
288+
289+
let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in
290+
let caseKey = InputKey(rawValue: String(describing: value))
291+
let name = Element.name(for: value)
292+
let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help
293+
let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp)
294+
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in
295+
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
296+
}))
297+
298+
}
299+
return exclusivity == .exclusive
300+
? ArgumentSet(exclusive: args)
301+
: ArgumentSet(additive: args)
302+
})
303+
}
304+
305+
/// Creates an array property that gets its values from the presence of
306+
/// zero or more flags, where the allowed flags are defined by an
307+
/// `EnumerableFlag` type.
268308
///
269-
/// This property has a default value of `nil`; specifying the flag in the
270-
/// command-line arguments is not required.
309+
/// This property has an empty array as its default value.
271310
///
272311
/// - Parameters:
273312
/// - name: A specification for what names are allowed for this flag.
313+
/// - help: Information about how to use this flag.
314+
public init<Element>(
315+
help: ArgumentHelp? = nil
316+
) where Value == Array<Element>, Element: EnumerableFlag {
317+
self.init(_parsedValue: .init { key in
318+
let caseHelps = Element.allCases.map { Element.help(for: $0) }
319+
let hasCustomCaseHelp = caseHelps.contains(where: { $0 != nil })
320+
321+
let args = Element.allCases.enumerated().map { (i, value) -> ArgumentDefinition in
322+
let caseKey = InputKey(rawValue: String(describing: value))
323+
let name = Element.name(for: value)
324+
let helpForCase = hasCustomCaseHelp ? (caseHelps[i] ?? help) : help
325+
let help = ArgumentDefinition.Help(options: .isOptional, help: helpForCase, key: key, isComposite: !hasCustomCaseHelp)
326+
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: [Element](), update: .nullary({ (origin, name, values) in
327+
values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
328+
$0.append(value)
329+
})
330+
}))
331+
}
332+
return ArgumentSet(additive: args)
333+
})
334+
}
335+
}
336+
337+
// - MARK: Deprecated CaseIterable/RawValue == String
338+
339+
extension Flag where Value: CaseIterable, Value: RawRepresentable, Value: Equatable, Value.RawValue == String {
340+
/// Creates a property that gets its value from the presence of a flag,
341+
/// where the allowed flags are defined by a case-iterable type.
342+
///
343+
/// - Parameters:
344+
/// - name: A specification for what names are allowed for this flag.
345+
/// - initial: A default value to use for this property. If `initial` is
346+
/// `nil`, this flag is required.
274347
/// - exclusivity: The behavior to use when multiple flags are specified.
275348
/// - help: Information about how to use this flag.
349+
@available(*, deprecated, message: "Add 'EnumerableFlag' conformance to your value type.")
350+
public init(
351+
name: NameSpecification = .long,
352+
default initial: Value? = nil,
353+
exclusivity: FlagExclusivity = .exclusive,
354+
help: ArgumentHelp? = nil
355+
) {
356+
self.init(_parsedValue: .init { key in
357+
// This gets flipped to `true` the first time one of these flags is
358+
// encountered.
359+
var hasUpdated = false
360+
let defaultValue = initial.map(String.init(describing:))
361+
362+
let args = Value.allCases.map { value -> ArgumentDefinition in
363+
let caseKey = InputKey(rawValue: value.rawValue)
364+
let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, defaultValue: defaultValue, key: key, isComposite: true)
365+
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: initial, update: .nullary({ (origin, name, values) in
366+
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
367+
}))
368+
}
369+
return exclusivity == .exclusive
370+
? ArgumentSet(exclusive: args)
371+
: ArgumentSet(additive: args)
372+
})
373+
}
374+
}
375+
376+
extension Flag {
377+
/// Creates a property that gets its value from the presence of a flag,
378+
/// where the allowed flags are defined by a case-iterable type.
379+
@available(*, deprecated, message: "Add 'EnumerableFlag' conformance to your value type.")
276380
public init<Element>(
277381
name: NameSpecification = .long,
278382
exclusivity: FlagExclusivity = .exclusive,
@@ -285,15 +389,15 @@ extension Flag {
285389

286390
let args = Element.allCases.map { value -> ArgumentDefinition in
287391
let caseKey = InputKey(rawValue: value.rawValue)
288-
let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key)
392+
let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key, isComposite: true)
289393
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: nil as Element?, update: .nullary({ (origin, name, values) in
290394
hasUpdated = try ArgumentSet.updateFlag(key: key, value: value, origin: origin, values: &values, hasUpdated: hasUpdated, exclusivity: exclusivity)
291395
}))
292396
}
293397
return exclusivity == .exclusive
294398
? ArgumentSet(exclusive: args)
295399
: ArgumentSet(additive: args)
296-
})
400+
})
297401
}
298402

299403
/// Creates an array property that gets its values from the presence of
@@ -305,22 +409,23 @@ extension Flag {
305409
/// - Parameters:
306410
/// - name: A specification for what names are allowed for this flag.
307411
/// - help: Information about how to use this flag.
412+
@available(*, deprecated, message: "Add 'EnumerableFlag' conformance to your value type.")
308413
public init<Element>(
309414
name: NameSpecification = .long,
310415
help: ArgumentHelp? = nil
311416
) where Value == Array<Element>, Element: CaseIterable, Element: RawRepresentable, Element.RawValue == String {
312417
self.init(_parsedValue: .init { key in
313418
let args = Element.allCases.map { value -> ArgumentDefinition in
314419
let caseKey = InputKey(rawValue: value.rawValue)
315-
let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key)
420+
let help = ArgumentDefinition.Help(options: .isOptional, help: help, key: key, isComposite: true)
316421
return ArgumentDefinition.flag(name: name, key: key, caseKey: caseKey, help: help, parsingStrategy: .nextAsValue, initialValue: [Element](), update: .nullary({ (origin, name, values) in
317422
values.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: {
318423
$0.append(value)
319424
})
320425
}))
321426
}
322427
return ArgumentSet(additive: args)
323-
})
428+
})
324429
}
325430
}
326431

Sources/ArgumentParser/Parsable Properties/Option.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ extension Option where Value: ExpressibleByArgument {
7878
///
7979
/// - Parameters:
8080
/// - name: A specification for what names are allowed for this flag.
81-
/// - initial: A default value to use for this property.
81+
/// - initial: A default value to use for this property. If `initial` is
82+
/// `nil`, this option and value are required from the user.
8283
/// - help: Information about how to use this option.
8384
public init(
8485
name: NameSpecification = .long,
@@ -224,7 +225,8 @@ extension Option {
224225
///
225226
/// - Parameters:
226227
/// - name: A specification for what names are allowed for this flag.
227-
/// - initial: A default value to use for this property.
228+
/// - initial: A default value to use for this property. If `initial` is
229+
/// `nil`, this option and value are required from the user.
228230
/// - help: Information about how to use this option.
229231
/// - transform: A closure that converts a string into this property's
230232
/// type or throws an error.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2020 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 type that represents the different possible flags to be used by a
13+
/// `@Flag` property.
14+
///
15+
/// For example, the `Size` enumeration declared here can be used as the type of
16+
/// a `@Flag` property:
17+
///
18+
/// enum Size: String, EnumerableFlag {
19+
/// case small, medium, large, extraLarge
20+
/// }
21+
///
22+
/// struct Example: ParsableCommand {
23+
/// @Flag() var sizes: [Size]
24+
///
25+
/// func run() {
26+
/// print(sizes)
27+
/// }
28+
/// }
29+
///
30+
/// By default, each case name is converted to a flag by using the `.long` name
31+
/// specification, so a user can call `example` like this:
32+
///
33+
/// $ example --small --large
34+
/// [.small, .large]
35+
///
36+
/// Provide alternative or additional name specifications for each case by
37+
/// implementing the `name(for:)` static method on your `EnumerableFlag` type.
38+
///
39+
/// extension Size {
40+
/// static func name(for value: Self) -> NameSpecification {
41+
/// switch value {
42+
/// case .extraLarge:
43+
/// return [.customShort("x"), .long]
44+
/// default:
45+
/// return .shortAndLong
46+
/// }
47+
/// }
48+
/// }
49+
///
50+
/// With this extension, a user can use short or long versions of the flags:
51+
///
52+
/// $ example -s -l -x --medium
53+
/// [.small, .large, .extraLarge, .medium]
54+
public protocol EnumerableFlag: CaseIterable, Equatable {
55+
/// Returns the name specification to use for the given flag.
56+
///
57+
/// The default implementation for this method always returns `.long`.
58+
/// Implement this method for your custom `EnumerableFlag` type to provide
59+
/// different name specifications for different cases.
60+
static func name(for value: Self) -> NameSpecification
61+
62+
/// Returns the help information to show for the given flag.
63+
///
64+
/// The default implementation for this method always returns `nil`, which
65+
/// groups the flags together with the help provided in the `@Flag`
66+
/// declaration. Implement this method for your custom type to provide
67+
/// different help information for each flag.
68+
static func help(for value: Self) -> ArgumentHelp?
69+
}
70+
71+
extension EnumerableFlag {
72+
public static func name(for value: Self) -> NameSpecification {
73+
.long
74+
}
75+
76+
public static func help(for value: Self) -> ArgumentHelp? {
77+
nil
78+
}
79+
}

Sources/ArgumentParser/Parsing/ArgumentDefinition.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct ArgumentDefinition {
3131
var discussion: String?
3232
var defaultValue: String?
3333
var keys: [InputKey]
34+
var isComposite: Bool
3435

3536
struct Options: OptionSet {
3637
var rawValue: UInt
@@ -39,11 +40,12 @@ struct ArgumentDefinition {
3940
static let isRepeating = Options(rawValue: 1 << 1)
4041
}
4142

42-
init(options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey) {
43+
init(options: Options = [], help: ArgumentHelp? = nil, defaultValue: String? = nil, key: InputKey, isComposite: Bool = false) {
4344
self.options = options
4445
self.help = help
4546
self.defaultValue = defaultValue
4647
self.keys = [key]
48+
self.isComposite = isComposite
4749
}
4850
}
4951

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ extension ArgumentSet {
130130
// The flag is required if initialValue is `nil`, otherwise it's optional
131131
let helpOptions: ArgumentDefinition.Help.Options = initialValue != nil ? .isOptional : []
132132

133-
let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key)
133+
let help = ArgumentDefinition.Help(options: helpOptions, help: help, defaultValue: initialValue.map(String.init), key: key, isComposite: true)
134134
let (enableNames, disableNames) = inversion.enableDisableNamePair(for: key, name: name)
135135

136136
var hasUpdated = false

0 commit comments

Comments
 (0)