Skip to content

Commit 0e86c69

Browse files
committed
Add aliases support for sub commands
This adds support for aliases for subcommands via a new parameter to CommandConfigurations constructors. The aliases are passed as an array of strings, where the default is just an empty array that signifies there are no aliases. The aliases are supported regardless of if a different commandName is chosen or not. This also updates how subcommands show up in the help text. Any aliases are now displayed to the right of the original command. In addition to the functionality itself, this change: 1. Updates some of the EndToEnd parsing tests to make sure they function while using aliases. 2. Sprinkles mentions where I saw fit in the documentation. 3. Updates the Math example to have aliases for `math stats average` (`math stats avg`), and `math multiply` (`math mul`). `math`'s help text now looks like the below: ``` ~ math --help OVERVIEW: A utility for performing maths. USAGE: math <subcommand> OPTIONS: --version Show the version. -h, --help Show help information. SUBCOMMANDS: add (default) Print the sum of the values. multiply, mul Print the product of the values. stats Calculate descriptive statistics. See 'math help <subcommand>' for detailed help. ~ math stats --help OVERVIEW: Calculate descriptive statistics. USAGE: math stats <subcommand> OPTIONS: --version Show the version. -h, --help Show help information. SUBCOMMANDS: average, avg Print the average of the values. stdev Print the standard deviation of the values. quantiles Print the quantiles of the values (TBD). See 'math help stats <subcommand>' for detailed help. ``` and use of the aliases: ``` ~ math mul 10 10 100 ~ math stats avg 10 20 15.0 ``` This change does NOT add any updates to the shell completion logic for this feature. Fixes #248
1 parent 4698969 commit 0e86c69

File tree

9 files changed

+122
-18
lines changed

9 files changed

+122
-18
lines changed

Examples/math/Math.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@ extension Math {
6464
}
6565

6666
struct Multiply: ParsableCommand {
67-
static let configuration =
68-
CommandConfiguration(abstract: "Print the product of the values.")
67+
static let configuration = CommandConfiguration(
68+
abstract: "Print the product of the values.",
69+
aliases: ["mul"])
6970

7071
@OptionGroup var options: Options
7172

@@ -92,7 +93,8 @@ extension Math.Statistics {
9293
struct Average: ParsableCommand {
9394
static let configuration = CommandConfiguration(
9495
abstract: "Print the average of the values.",
95-
version: "1.5.0-alpha")
96+
version: "1.5.0-alpha",
97+
aliases: ["avg"])
9698

9799
enum Kind: String, ExpressibleByArgument, CaseIterable {
98100
case mean, median, mode

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ OPTIONS:
2828
-h, --help Show help information.
2929
3030
SUBCOMMANDS:
31-
average Print the average of the values.
31+
average, avg Print the average of the values.
3232
stdev Print the standard deviation of the values.
3333
quantiles Print the quantiles of the values (TBD).
3434
@@ -84,8 +84,9 @@ extension Math {
8484
}
8585

8686
struct Multiply: ParsableCommand {
87-
static let configuration
88-
= CommandConfiguration(abstract: "Print the product of the values.")
87+
static let configuration = CommandConfiguration(
88+
abstract: "Print the product of the values.",
89+
aliases: ["mul"])
8990

9091
@OptionGroup var options: Math.Options
9192

@@ -97,6 +98,17 @@ extension Math {
9798
}
9899
```
99100

101+
One thing to note is the aliases parameter for `CommandConfiguration`. This is useful for subcommands
102+
to define alternative names that can be used to invoke them. In this case we've defined a shorthand
103+
for multiply named mul, so you could invoke the `Multiply` command for our program by either of the below:
104+
105+
```
106+
% math multiply 10 15 7
107+
1050
108+
% math mul 10 15 7
109+
1050
110+
```
111+
100112
Next, we'll define `Statistics`, the third subcommand of `Math`. The `Statistics` command specifies a custom command name (`stats`) in its configuration, overriding the default derived from the type name (`statistics`). It also declares two additional subcommands, meaning that it acts as a forked branch in the command tree, and not a leaf.
101113

102114
```swift
@@ -116,7 +128,8 @@ Let's finish our subcommands with the `Average` and `StandardDeviation` types. E
116128
extension Math.Statistics {
117129
struct Average: ParsableCommand {
118130
static let configuration = CommandConfiguration(
119-
abstract: "Print the average of the values.")
131+
abstract: "Print the average of the values.",
132+
aliases: ["avg"])
120133

121134
enum Kind: String, ExpressibleByArgument {
122135
case mean, median, mode

Sources/ArgumentParser/Documentation.docc/Extensions/CommandConfiguration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Creating a Configuration
66

7-
- ``init(commandName:abstract:usage:discussion:version:shouldDisplay:subcommands:defaultSubcommand:helpNames:)``
7+
- ``init(commandName:abstract:usage:discussion:version:shouldDisplay:subcommands:defaultSubcommand:helpNames:aliases:)``
88

99
### Customizing the Help Screen
1010

@@ -23,4 +23,4 @@
2323
- ``commandName``
2424
- ``version``
2525
- ``shouldDisplay``
26-
26+
- ``aliases``

Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ public struct CommandConfiguration: Sendable {
5454

5555
/// Flag names to be used for help.
5656
public var helpNames: NameSpecification?
57-
57+
58+
/// An array of aliases for the command's name.
59+
public var aliases: [String]
60+
5861
/// Creates the configuration for a command.
5962
///
6063
/// - Parameters:
@@ -80,6 +83,7 @@ public struct CommandConfiguration: Sendable {
8083
/// with a simulated Boolean property named `help`. If `helpNames` is
8184
/// `nil`, the names are inherited from the parent command, if any, or
8285
/// are `-h` and `--help`.
86+
/// - aliases: An array of aliases for the command's name.
8387
public init(
8488
commandName: String? = nil,
8589
abstract: String = "",
@@ -89,7 +93,8 @@ public struct CommandConfiguration: Sendable {
8993
shouldDisplay: Bool = true,
9094
subcommands: [ParsableCommand.Type] = [],
9195
defaultSubcommand: ParsableCommand.Type? = nil,
92-
helpNames: NameSpecification? = nil
96+
helpNames: NameSpecification? = nil,
97+
aliases: [String] = []
9398
) {
9499
self.commandName = commandName
95100
self.abstract = abstract
@@ -100,6 +105,7 @@ public struct CommandConfiguration: Sendable {
100105
self.subcommands = subcommands
101106
self.defaultSubcommand = defaultSubcommand
102107
self.helpNames = helpNames
108+
self.aliases = aliases
103109
}
104110

105111
/// Creates the configuration for a command with a "super-command".
@@ -114,7 +120,8 @@ public struct CommandConfiguration: Sendable {
114120
shouldDisplay: Bool = true,
115121
subcommands: [ParsableCommand.Type] = [],
116122
defaultSubcommand: ParsableCommand.Type? = nil,
117-
helpNames: NameSpecification? = nil
123+
helpNames: NameSpecification? = nil,
124+
aliases: [String] = []
118125
) {
119126
self.commandName = commandName
120127
self._superCommandName = _superCommandName
@@ -126,11 +133,12 @@ public struct CommandConfiguration: Sendable {
126133
self.subcommands = subcommands
127134
self.defaultSubcommand = defaultSubcommand
128135
self.helpNames = helpNames
136+
self.aliases = aliases
129137
}
130138
}
131139

132140
extension CommandConfiguration {
133-
@available(*, deprecated, message: "Use the memberwise initializer with the usage parameter.")
141+
@available(*, deprecated, message: "Use the memberwise initializer with the usage and aliases parameters.")
134142
public init(
135143
commandName _commandName: String?,
136144
abstract: String,
@@ -150,6 +158,7 @@ extension CommandConfiguration {
150158
shouldDisplay: shouldDisplay,
151159
subcommands: subcommands,
152160
defaultSubcommand: defaultSubcommand,
153-
helpNames: helpNames)
161+
helpNames: helpNames,
162+
aliases: [])
154163
}
155164
}

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,10 @@ struct LenientParser {
495495
// parsing to skip over unrecognized input, but if the current
496496
// command or the matched subcommand captures all remaining input,
497497
// then we want to break out of parsing at this point.
498-
if let matchedSubcommand = subcommands.first(where: { $0._commandName == argument }) {
498+
let matchedSubcommand = subcommands.first(where: {
499+
$0._commandName == argument || $0.configuration.aliases.contains(argument)
500+
})
501+
if let matchedSubcommand {
499502
if !matchedSubcommand.includesPassthroughArguments && defaultCapturesForPassthrough {
500503
continue ArgumentLoop
501504
} else if matchedSubcommand.includesPassthroughArguments {

Sources/ArgumentParser/Usage/HelpGenerator.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,9 @@ internal struct HelpGenerator {
215215
configuration.subcommands.compactMap { command in
216216
guard command.configuration.shouldDisplay else { return nil }
217217
var label = command._commandName
218+
for alias in command.configuration.aliases {
219+
label += ", \(alias)"
220+
}
218221
if command == configuration.defaultSubcommand {
219222
label += " (default)"
220223
}

Sources/ArgumentParser/Utilities/Tree.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ extension Tree where Element == ParsableCommand.Type {
8585
}
8686

8787
func firstChild(withName name: String) -> Tree? {
88-
children.first(where: { $0.element._commandName == name })
88+
children.first(where: {
89+
$0.element._commandName == name || $0.element.configuration.aliases.contains(name)
90+
})
8991
}
9092

9193
convenience init(root command: ParsableCommand.Type) throws {

Tests/ArgumentParserEndToEndTests/NestedCommandEndToEndTests.swift

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ fileprivate struct Foo: ParsableCommand {
3434

3535
struct Package: ParsableCommand {
3636
static let configuration =
37-
CommandConfiguration(subcommands: [Clean.self, Config.self])
37+
CommandConfiguration(
38+
subcommands: [Clean.self, Config.self],
39+
aliases: ["pkg"])
3840

3941
@Flag(name: .short)
4042
var force: Bool = false
@@ -45,6 +47,7 @@ fileprivate struct Foo: ParsableCommand {
4547
}
4648

4749
struct Config: ParsableCommand {
50+
static let configuration = CommandConfiguration(aliases: ["cfg"])
4851
@OptionGroup() var foo: Foo
4952
@OptionGroup() var package: Package
5053
}
@@ -62,60 +65,119 @@ extension NestedCommandEndToEndTests {
6265
XCTAssertFalse(package.force)
6366
}
6467

68+
AssertParseFooCommand(Foo.Package.self, ["pkg"]) { package in
69+
XCTAssertFalse(package.force)
70+
}
71+
6572
AssertParseFooCommand(Foo.Package.Clean.self, ["package", "clean"]) { clean in
6673
XCTAssertEqual(clean.foo.verbose, false)
6774
XCTAssertEqual(clean.package.force, false)
6875
}
6976

77+
AssertParseFooCommand(Foo.Package.Clean.self, ["pkg", "clean"]) { clean in
78+
XCTAssertEqual(clean.foo.verbose, false)
79+
XCTAssertEqual(clean.package.force, false)
80+
}
81+
7082
AssertParseFooCommand(Foo.Package.Clean.self, ["package", "-f", "clean"]) { clean in
7183
XCTAssertEqual(clean.foo.verbose, false)
7284
XCTAssertEqual(clean.package.force, true)
7385
}
7486

87+
AssertParseFooCommand(Foo.Package.Clean.self, ["pkg", "-f", "clean"]) { clean in
88+
XCTAssertEqual(clean.foo.verbose, false)
89+
XCTAssertEqual(clean.package.force, true)
90+
}
91+
7592
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-v", "config"]) { config in
7693
XCTAssertEqual(config.foo.verbose, true)
7794
XCTAssertEqual(config.package.force, false)
7895
}
7996

97+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "-v", "cfg"]) { config in
98+
XCTAssertEqual(config.foo.verbose, true)
99+
XCTAssertEqual(config.package.force, false)
100+
}
101+
80102
AssertParseFooCommand(Foo.Package.Config.self, ["package", "config", "-v"]) { config in
81103
XCTAssertEqual(config.foo.verbose, true)
82104
XCTAssertEqual(config.package.force, false)
83105
}
84106

107+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "cfg", "-v"]) { config in
108+
XCTAssertEqual(config.foo.verbose, true)
109+
XCTAssertEqual(config.package.force, false)
110+
}
111+
85112
AssertParseFooCommand(Foo.Package.Config.self, ["-v", "package", "config"]) { config in
86113
XCTAssertEqual(config.foo.verbose, true)
87114
XCTAssertEqual(config.package.force, false)
88115
}
89116

117+
AssertParseFooCommand(Foo.Package.Config.self, ["-v", "pkg", "cfg"]) { config in
118+
XCTAssertEqual(config.foo.verbose, true)
119+
XCTAssertEqual(config.package.force, false)
120+
}
121+
90122
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-f", "config"]) { config in
91123
XCTAssertEqual(config.foo.verbose, false)
92124
XCTAssertEqual(config.package.force, true)
93125
}
94126

127+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "-f", "cfg"]) { config in
128+
XCTAssertEqual(config.foo.verbose, false)
129+
XCTAssertEqual(config.package.force, true)
130+
}
131+
95132
AssertParseFooCommand(Foo.Package.Config.self, ["package", "config", "-f"]) { config in
96133
XCTAssertEqual(config.foo.verbose, false)
97134
XCTAssertEqual(config.package.force, true)
98135
}
99136

137+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "cfg", "-f"]) { config in
138+
XCTAssertEqual(config.foo.verbose, false)
139+
XCTAssertEqual(config.package.force, true)
140+
}
141+
100142
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-v", "config", "-f"]) { config in
101143
XCTAssertEqual(config.foo.verbose, true)
102144
XCTAssertEqual(config.package.force, true)
103145
}
104146

147+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "-v", "cfg", "-f"]) { config in
148+
XCTAssertEqual(config.foo.verbose, true)
149+
XCTAssertEqual(config.package.force, true)
150+
}
151+
105152
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-f", "config", "-v"]) { config in
106153
XCTAssertEqual(config.foo.verbose, true)
107154
XCTAssertEqual(config.package.force, true)
108155
}
109156

157+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "-f", "cfg", "-v"]) { config in
158+
XCTAssertEqual(config.foo.verbose, true)
159+
XCTAssertEqual(config.package.force, true)
160+
}
161+
110162
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-vf", "config"]) { config in
111163
XCTAssertEqual(config.foo.verbose, true)
112164
XCTAssertEqual(config.package.force, true)
113165
}
114166

167+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "-vf", "cfg"]) { config in
168+
XCTAssertEqual(config.foo.verbose, true)
169+
XCTAssertEqual(config.package.force, true)
170+
}
171+
115172
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-fv", "config"]) { config in
116173
XCTAssertEqual(config.foo.verbose, true)
117174
XCTAssertEqual(config.package.force, true)
118175
}
176+
177+
AssertParseFooCommand(Foo.Package.Config.self, ["pkg", "-fv", "cfg"]) { config in
178+
XCTAssertEqual(config.foo.verbose, true)
179+
XCTAssertEqual(config.package.force, true)
180+
}
119181
}
120182

121183
func testParsing_build() throws {
@@ -127,19 +189,29 @@ extension NestedCommandEndToEndTests {
127189

128190
func testParsing_fails() throws {
129191
XCTAssertThrowsError(try Foo.parseAsRoot(["clean", "package"]))
192+
XCTAssertThrowsError(try Foo.parseAsRoot(["clean", "pkg"]))
130193
XCTAssertThrowsError(try Foo.parseAsRoot(["config", "package"]))
194+
XCTAssertThrowsError(try Foo.parseAsRoot(["cfg", "pkg"]))
131195
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "c"]))
196+
XCTAssertThrowsError(try Foo.parseAsRoot(["pkg", "c"]))
132197
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "build"]))
198+
XCTAssertThrowsError(try Foo.parseAsRoot(["pkg", "build"]))
133199
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "build", "clean"]))
200+
XCTAssertThrowsError(try Foo.parseAsRoot(["pkg", "build", "clean"]))
134201
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "clean", "foo"]))
202+
XCTAssertThrowsError(try Foo.parseAsRoot(["pkg", "clean", "foo"]))
135203
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "config", "bar"]))
204+
XCTAssertThrowsError(try Foo.parseAsRoot(["pkg", "cfg", "bar"]))
136205
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "clean", "build"]))
206+
XCTAssertThrowsError(try Foo.parseAsRoot(["pkg", "clean", "build"]))
137207
XCTAssertThrowsError(try Foo.parseAsRoot(["build"]))
138208
XCTAssertThrowsError(try Foo.parseAsRoot(["build", "-f"]))
139209
XCTAssertThrowsError(try Foo.parseAsRoot(["build", "--build"]))
140210
XCTAssertThrowsError(try Foo.parseAsRoot(["build", "--build", "12"]))
141211
XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "package", "clean"]))
212+
XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "pkg", "clean"]))
142213
XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "package", "config"]))
214+
XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "pkg", "config"]))
143215
}
144216
}
145217

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ final class MathExampleTests: XCTestCase {
3737
3838
SUBCOMMANDS:
3939
add (default) Print the sum of the values.
40-
multiply Print the product of the values.
40+
multiply, mul Print the product of the values.
4141
stats Calculate descriptive statistics.
4242
4343
See 'math help <subcommand>' for detailed help.

0 commit comments

Comments
 (0)