Skip to content

Commit 83c5134

Browse files
authored
Add ability to provide descriptions for CaseEnumerable @Option values (#647)
Since `ExpressibleByArgument` already maintains a list of enumerable values for an argument, we can extend this to serve as an ordered list for a new dictionary property that maps the value name to its description, if applicable. The new property is a static variable on `ExpressibleByArgument` labelled `allValueDescriptions`. If the description string for a value is the same as the value string, it's assumed that the description is not implemented. The new value strings are used in the help screen, in the dump-help JSON output, and in the generated manual.
1 parent 7f9f965 commit 83c5134

File tree

18 files changed

+1313
-74
lines changed

18 files changed

+1313
-74
lines changed

Examples/color/Color.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import ArgumentParser
14+
15+
@main
16+
struct Color: ParsableCommand {
17+
@Option(help: "Your favorite color.")
18+
var fav: ColorOptions
19+
20+
@Option(help: .init("Your second favorite color.", discussion: "This is optional."))
21+
var second: ColorOptions?
22+
23+
func run() {
24+
print("My favorite color is \(fav.rawValue)")
25+
if let second {
26+
print("...And my second favorite is \(second.rawValue)!")
27+
}
28+
}
29+
}
30+
31+
public enum ColorOptions: String, CaseIterable, ExpressibleByArgument {
32+
case red
33+
case blue
34+
case yellow
35+
36+
public var defaultValueDescription: String {
37+
switch self {
38+
case .red:
39+
return "A red color."
40+
case .blue:
41+
return "A blue color."
42+
case .yellow:
43+
return "A yellow color."
44+
}
45+
}
46+
47+
public var description: String {
48+
switch self {
49+
case .red:
50+
return "A red color."
51+
case .blue:
52+
return "A blue color."
53+
case .yellow:
54+
return "A yellow color."
55+
}
56+
}
57+
}

Package.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ var package = Package(
6060
name: "repeat",
6161
dependencies: ["ArgumentParser"],
6262
path: "Examples/repeat"),
63+
.executableTarget(
64+
name: "color",
65+
dependencies: ["ArgumentParser"],
66+
path: "Examples/color")
6367

6468
// Tools
6569
.executableTarget(

[email protected]

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ var package = Package(
6363
name: "repeat",
6464
dependencies: ["ArgumentParser"],
6565
path: "Examples/repeat"),
66+
.executableTarget(
67+
name: "color",
68+
dependencies: ["ArgumentParser"],
69+
path: "Examples/color"),
6670

6771
// Tools
6872
.executableTarget(
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2024 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+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
/// A structure that contains an extended description of the argument.
14+
///
15+
/// For `EnumerableOptionValue` types, the `.enumerated` case encapsulates the necessary information
16+
/// to list each of the possible values and their descriptions. Optionally, users can add a discussion preamble that
17+
/// will be appended to the beginning of the value list section.
18+
///
19+
/// For example, the following `EnumerableOptionValue` type defined in a command could contain an
20+
/// additional discussion block defined in its `ArgumentHelp`:
21+
///
22+
/// ```swift
23+
/// enum Color: String, EnumerableOptionValue {
24+
/// case red
25+
/// case blue
26+
/// case yellow
27+
///
28+
/// public var description: String {
29+
/// switch self {
30+
/// case .red:
31+
/// return "A red color."
32+
/// case. blue:
33+
/// return "A blue color."
34+
/// case .yellow:
35+
/// return "A yellow color."
36+
/// }
37+
/// }
38+
/// }
39+
///
40+
/// struct Example: ParsableCommand {
41+
/// @Option(help: ArgumentHelp(discussion: "A set of available colors."))
42+
/// var color: Color
43+
/// }
44+
/// ```
45+
///
46+
/// To which the printed usage would look like the following:
47+
///
48+
/// ```
49+
/// USAGE: example --color <color>
50+
///
51+
/// OPTIONS:
52+
/// --color <color>
53+
/// A set of available colors.
54+
/// Values:
55+
/// red - A red color.
56+
/// blue - A blue color.
57+
/// yellow - A yellow color.
58+
/// -h, --help Show help information
59+
/// ```
60+
///
61+
/// Without the additional discussion text:
62+
///
63+
/// ```swift
64+
/// @Option var color: Color
65+
/// ```
66+
///
67+
/// The printed usage would look like the following:
68+
///
69+
/// ```
70+
/// USAGE: example --color <color>
71+
///
72+
/// OPTIONS:
73+
/// --color <color>
74+
/// red - A red color.
75+
/// blue - A blue color.
76+
/// yellow - A yellow color.
77+
/// -h, --help Show help information
78+
/// ```
79+
///
80+
/// In any case where the argument type is not `EnumerableOptionValue`, the default implementation
81+
/// will use the `.staticText` case and will print a block of discussion text.
82+
enum ArgumentDiscussion {
83+
case staticText(String)
84+
case enumerated(preamble: String? = nil, any ExpressibleByArgument.Type)
85+
86+
init?(_ text: String? = nil, _ options: (any ExpressibleByArgument.Type)? = nil) {
87+
switch (text, options) {
88+
case (.some(let text), .some(let options)):
89+
guard !options.allValueDescriptions.isEmpty else {
90+
self = .staticText(text)
91+
return
92+
}
93+
self = .enumerated(preamble: text, options)
94+
case (.some(let text), .none):
95+
self = .staticText(text)
96+
case (.none, .some(let options)):
97+
guard !options.allValueDescriptions.isEmpty else {
98+
return nil
99+
}
100+
self = .enumerated(options)
101+
default:
102+
return nil
103+
}
104+
}
105+
106+
var isEnumerated: Bool {
107+
if case .enumerated = self {
108+
return true
109+
}
110+
111+
return false
112+
}
113+
}
114+
115+
extension ArgumentDiscussion: Sendable { }
116+
117+
extension ArgumentDiscussion: Hashable {
118+
static func == (lhs: ArgumentDiscussion, rhs: ArgumentDiscussion) -> Bool {
119+
switch (lhs, rhs) {
120+
case (.staticText(let lhsText), .staticText(let rhsText)):
121+
return lhsText == rhsText
122+
case (.enumerated(let lhsPreamble, let lhsOption), .enumerated(preamble: let rhsPreamble, let rhsOption)):
123+
return (lhsPreamble == rhsPreamble) && (lhsOption == rhsOption)
124+
default:
125+
return false
126+
}
127+
}
128+
129+
func hash(into hasher: inout Hasher) {
130+
switch self {
131+
case .staticText(let text):
132+
hasher.combine(text)
133+
case .enumerated(preamble: let text, let options):
134+
hasher.combine(text)
135+
hasher.combine(ObjectIdentifier(options))
136+
}
137+
}
138+
}

Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ public struct ArgumentHelp {
1515
public var abstract: String = ""
1616

1717
/// An expanded description of the argument, in plain text form.
18-
public var discussion: String = ""
19-
18+
public var discussion: String?
19+
2020
/// An alternative name to use for the argument's value when showing usage
2121
/// information.
2222
///
@@ -39,12 +39,16 @@ public struct ArgumentHelp {
3939
visibility = newValue ? .default : .hidden
4040
}
4141
}
42-
42+
43+
/// A property of meta type `any ExpressibleByArgument.Type` that serves to retain
44+
/// information about any arguments that have enumerable values and their descriptions.
45+
public var argumentType: (any ExpressibleByArgument.Type)?
46+
4347
/// Creates a new help instance.
4448
@available(*, deprecated, message: "Use init(_:discussion:valueName:visibility:) instead.")
4549
public init(
4650
_ abstract: String = "",
47-
discussion: String = "",
51+
discussion: String? = nil,
4852
valueName: String? = nil,
4953
shouldDisplay: Bool)
5054
{
@@ -57,14 +61,16 @@ public struct ArgumentHelp {
5761
/// Creates a new help instance.
5862
public init(
5963
_ abstract: String = "",
60-
discussion: String = "",
64+
discussion: String? = nil,
6165
valueName: String? = nil,
62-
visibility: ArgumentVisibility = .default)
66+
visibility: ArgumentVisibility = .default,
67+
argumentType: (any ExpressibleByArgument.Type)? = nil)
6368
{
6469
self.abstract = abstract
6570
self.discussion = discussion
6671
self.valueName = valueName
6772
self.visibility = visibility
73+
self.argumentType = argumentType
6874
}
6975

7076
/// A `Help` instance that shows an argument only in the extended help display.

Sources/ArgumentParser/Parsable Properties/Option.swift

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,13 @@ extension Option where Value: ExpressibleByArgument {
272272
container: Bare<Value>.self,
273273
key: key,
274274
kind: .name(key: key, specification: name),
275-
help: help,
275+
help: .init(
276+
help?.abstract ?? "",
277+
discussion: help?.discussion,
278+
valueName: help?.valueName,
279+
visibility: help?.visibility ?? .default,
280+
argumentType: Value.self
281+
),
276282
parsingStrategy: parsingStrategy.base,
277283
initial: wrappedValue,
278284
completion: completion)
@@ -326,7 +332,13 @@ extension Option where Value: ExpressibleByArgument {
326332
container: Bare<Value>.self,
327333
key: key,
328334
kind: .name(key: key, specification: name),
329-
help: help,
335+
help: .init(
336+
help?.abstract ?? "",
337+
discussion: help?.discussion,
338+
valueName: help?.valueName,
339+
visibility: help?.visibility ?? .default,
340+
argumentType: Value.self
341+
),
330342
parsingStrategy: parsingStrategy.base,
331343
initial: nil,
332344
completion: completion)
@@ -429,6 +441,7 @@ extension Option {
429441
}
430442
}
431443

444+
432445
// MARK: - @Option Optional<T: ExpressibleByArgument> Initializers
433446
extension Option {
434447
/// Creates an optional property that reads its value from a labeled option,
@@ -460,7 +473,13 @@ extension Option {
460473
container: Optional<T>.self,
461474
key: key,
462475
kind: .name(key: key, specification: name),
463-
help: help,
476+
help: .init(
477+
help?.abstract ?? "",
478+
discussion: help?.discussion,
479+
valueName: help?.valueName,
480+
visibility: help?.visibility ?? .default,
481+
argumentType: T.self
482+
),
464483
parsingStrategy: parsingStrategy.base,
465484
initial: nil,
466485
completion: completion)
@@ -485,7 +504,13 @@ extension Option {
485504
container: Optional<T>.self,
486505
key: key,
487506
kind: .name(key: key, specification: name),
488-
help: help,
507+
help: .init(
508+
help?.abstract ?? "",
509+
discussion: help?.discussion,
510+
valueName: help?.valueName,
511+
visibility: help?.visibility ?? .default,
512+
argumentType: T.self
513+
),
489514
parsingStrategy: parsingStrategy.base,
490515
initial: _wrappedValue,
491516
completion: completion)
@@ -521,7 +546,13 @@ extension Option {
521546
container: Optional<T>.self,
522547
key: key,
523548
kind: .name(key: key, specification: name),
524-
help: help,
549+
help: .init(
550+
help?.abstract ?? "",
551+
discussion: help?.discussion,
552+
valueName: help?.valueName,
553+
visibility: help?.visibility ?? .default,
554+
argumentType: T.self
555+
),
525556
parsingStrategy: parsingStrategy.base,
526557
initial: nil,
527558
completion: completion)

Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ public struct CommandConfiguration: Sendable {
2323
/// with a common dash-prefix, like `git`'s and `swift`'s constellation of
2424
/// independent commands.
2525
public var _superCommandName: String?
26-
26+
2727
/// A one-line description of this command.
2828
public var abstract: String
29-
29+
3030
/// A customized usage string to be shown in the help display and error
3131
/// messages.
3232
///
@@ -37,15 +37,19 @@ public struct CommandConfiguration: Sendable {
3737

3838
/// A longer description of this command, to be shown in the extended help
3939
/// display.
40+
///
41+
/// Can include specific abstracts about the argument's possible values (e.g.
42+
/// for a custom `EnumerableOptionValue` type), or can describe
43+
/// a static block of text that extends the description of the argument.
4044
public var discussion: String
41-
45+
4246
/// Version information for this command.
4347
public var version: String
4448

4549
/// A Boolean value indicating whether this command should be shown in
4650
/// the extended help display.
4751
public var shouldDisplay: Bool
48-
52+
4953
/// An array of the types that define subcommands for this command.
5054
///
5155
/// This property "flattens" the grouping structure of the subcommands.
@@ -70,7 +74,7 @@ public struct CommandConfiguration: Sendable {
7074

7175
/// The default command type to run if no subcommand is given.
7276
public var defaultSubcommand: ParsableCommand.Type?
73-
77+
7478
/// Flag names to be used for help.
7579
public var helpNames: NameSpecification?
7680

0 commit comments

Comments
 (0)