Skip to content

Commit 2b96293

Browse files
authored
Add aliases support for sub commands (#627)
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 2b96293

File tree

11 files changed

+182
-19
lines changed

11 files changed

+182
-19
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: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,14 @@ 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+
///
60+
/// All of the aliases MUST not match the actual command's name,
61+
/// whether that be the derived name if `commandName` is not provided,
62+
/// or `commandName` itself if provided.
63+
public var aliases: [String]
64+
5865
/// Creates the configuration for a command.
5966
///
6067
/// - Parameters:
@@ -80,6 +87,9 @@ public struct CommandConfiguration: Sendable {
8087
/// with a simulated Boolean property named `help`. If `helpNames` is
8188
/// `nil`, the names are inherited from the parent command, if any, or
8289
/// are `-h` and `--help`.
90+
/// - aliases: An array of aliases for the command's name. All of the aliases
91+
/// MUST not match the actual command name, whether that be the derived name
92+
/// if `commandName` is not provided, or `commandName` itself if provided.
8393
public init(
8494
commandName: String? = nil,
8595
abstract: String = "",
@@ -89,7 +99,8 @@ public struct CommandConfiguration: Sendable {
8999
shouldDisplay: Bool = true,
90100
subcommands: [ParsableCommand.Type] = [],
91101
defaultSubcommand: ParsableCommand.Type? = nil,
92-
helpNames: NameSpecification? = nil
102+
helpNames: NameSpecification? = nil,
103+
aliases: [String] = []
93104
) {
94105
self.commandName = commandName
95106
self.abstract = abstract
@@ -100,6 +111,7 @@ public struct CommandConfiguration: Sendable {
100111
self.subcommands = subcommands
101112
self.defaultSubcommand = defaultSubcommand
102113
self.helpNames = helpNames
114+
self.aliases = aliases
103115
}
104116

105117
/// Creates the configuration for a command with a "super-command".
@@ -114,7 +126,8 @@ public struct CommandConfiguration: Sendable {
114126
shouldDisplay: Bool = true,
115127
subcommands: [ParsableCommand.Type] = [],
116128
defaultSubcommand: ParsableCommand.Type? = nil,
117-
helpNames: NameSpecification? = nil
129+
helpNames: NameSpecification? = nil,
130+
aliases: [String] = []
118131
) {
119132
self.commandName = commandName
120133
self._superCommandName = _superCommandName
@@ -126,11 +139,37 @@ public struct CommandConfiguration: Sendable {
126139
self.subcommands = subcommands
127140
self.defaultSubcommand = defaultSubcommand
128141
self.helpNames = helpNames
142+
self.aliases = aliases
129143
}
130144
}
131145

132146
extension CommandConfiguration {
133-
@available(*, deprecated, message: "Use the memberwise initializer with the usage parameter.")
147+
@available(*, deprecated, message: "Use the memberwise initializer with the aliases parameter.")
148+
public init(
149+
commandName: String? = nil,
150+
abstract: String = "",
151+
usage: String? = nil,
152+
discussion: String = "",
153+
version: String = "",
154+
shouldDisplay: Bool = true,
155+
subcommands: [ParsableCommand.Type] = [],
156+
defaultSubcommand: ParsableCommand.Type? = nil,
157+
helpNames: NameSpecification? = nil
158+
) {
159+
self.init(
160+
commandName: commandName,
161+
abstract: abstract,
162+
usage: usage,
163+
discussion: discussion,
164+
version: version,
165+
shouldDisplay: shouldDisplay,
166+
subcommands: subcommands,
167+
defaultSubcommand: defaultSubcommand,
168+
helpNames: helpNames,
169+
aliases: [])
170+
}
171+
172+
@available(*, deprecated, message: "Use the memberwise initializer with the usage and aliases parameters.")
134173
public init(
135174
commandName _commandName: String?,
136175
abstract: String,
@@ -150,6 +189,7 @@ extension CommandConfiguration {
150189
shouldDisplay: shouldDisplay,
151190
subcommands: subcommands,
152191
defaultSubcommand: defaultSubcommand,
153-
helpNames: helpNames)
192+
helpNames: helpNames,
193+
aliases: [])
154194
}
155195
}

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/Parsing/CommandParser.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ struct CommandParser {
4141
self.commandTree = try Tree(root: rootCommand)
4242
} catch Tree<ParsableCommand.Type>.InitializationError.recursiveSubcommand(let command) {
4343
fatalError("The ParsableCommand \"\(command)\" can't have itself as its own subcommand.")
44+
} catch Tree<ParsableCommand.Type>.InitializationError.aliasMatchingCommand(let command) {
45+
fatalError("The ParsableCommand \"\(command)\" can't have an alias with the same name as the command itself.")
4446
} catch {
4547
fatalError("Unexpected error: \(error).")
4648
}
4749
self.currentNode = commandTree
48-
50+
4951
// A command tree that has a depth greater than zero gets a `help`
5052
// subcommand.
5153
if !commandTree.isLeaf {

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: 8 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 {
@@ -94,11 +96,16 @@ extension Tree where Element == ParsableCommand.Type {
9496
if subcommand == command {
9597
throw InitializationError.recursiveSubcommand(subcommand)
9698
}
99+
// We don't allow an alias that has the same name as the command itself.
100+
if subcommand.configuration.aliases.contains(subcommand._commandName) {
101+
throw InitializationError.aliasMatchingCommand(subcommand)
102+
}
97103
try addChild(Tree(root: subcommand))
98104
}
99105
}
100106

101107
enum InitializationError: Error {
102108
case recursiveSubcommand(ParsableCommand.Type)
109+
case aliasMatchingCommand(ParsableCommand.Type)
103110
}
104111
}

0 commit comments

Comments
 (0)