Skip to content

Commit c07660f

Browse files
committed
Adds a new CommandConfiguration property, addressing apple#295
This adds the `shouldUseExecutableName` property, allowing the command name to be derived from the executable's file name. The property defaults to false, both because subcommands using it is probably undesirable and to preserve existing behaviour after updating the package.
1 parent b80fb05 commit c07660f

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ public struct CommandConfiguration {
1717
/// the command type to hyphen-separated lowercase words.
1818
public var commandName: String?
1919

20+
/// A Boolean value indicating whether to use the executable's file name
21+
/// for the command name.
22+
///
23+
/// If `commandName` or `_superCommandName` are non-`nil`, this
24+
/// value is ignored.
25+
public var shouldUseExecutableName: Bool
26+
2027
/// The name of this command's "super-command". (experimental)
2128
///
2229
/// Use this when a command is part of a group of commands that are installed
@@ -61,6 +68,9 @@ public struct CommandConfiguration {
6168
/// - commandName: The name of the command to use on the command line. If
6269
/// `commandName` is `nil`, the command name is derived by converting
6370
/// the name of the command type to hyphen-separated lowercase words.
71+
/// - shouldUseExecutableName: A Boolean value indicating whether to
72+
/// use the executable's file name for the command name. If `commandName`
73+
/// is non-`nil`, this value is ignored.
6474
/// - abstract: A one-line description of the command.
6575
/// - usage: A custom usage description for the command. When you provide
6676
/// a non-`nil` string, the argument parser uses `usage` instead of
@@ -82,6 +92,7 @@ public struct CommandConfiguration {
8292
/// are `-h` and `--help`.
8393
public init(
8494
commandName: String? = nil,
95+
shouldUseExecutableName: Bool = false,
8596
abstract: String = "",
8697
usage: String? = nil,
8798
discussion: String = "",
@@ -92,6 +103,7 @@ public struct CommandConfiguration {
92103
helpNames: NameSpecification? = nil
93104
) {
94105
self.commandName = commandName
106+
self.shouldUseExecutableName = shouldUseExecutableName
95107
self.abstract = abstract
96108
self.usage = usage
97109
self.discussion = discussion
@@ -106,6 +118,7 @@ public struct CommandConfiguration {
106118
/// (experimental)
107119
public init(
108120
commandName: String? = nil,
121+
shouldUseExecutableName: Bool = false,
109122
_superCommandName: String,
110123
abstract: String = "",
111124
usage: String? = nil,
@@ -117,6 +130,7 @@ public struct CommandConfiguration {
117130
helpNames: NameSpecification? = nil
118131
) {
119132
self.commandName = commandName
133+
self.shouldUseExecutableName = shouldUseExecutableName
120134
self._superCommandName = _superCommandName
121135
self.abstract = abstract
122136
self.usage = usage

Sources/ArgumentParser/Parsable Types/ParsableCommand.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ public protocol ParsableCommand: ParsableArguments {
3737
extension ParsableCommand {
3838
public static var _commandName: String {
3939
configuration.commandName ??
40-
String(describing: Self.self).convertedToSnakeCase(separator: "-")
40+
(configuration.shouldUseExecutableName && configuration._superCommandName == nil
41+
? UsageGenerator.executableName
42+
: String(describing: Self.self).convertedToSnakeCase(separator: "-"))
4143
}
4244

4345
public static var configuration: CommandConfiguration {

Sources/ArgumentParser/Usage/UsageGenerator.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ struct UsageGenerator {
1818

1919
extension UsageGenerator {
2020
init(definition: ArgumentSet) {
21-
let toolName = CommandLine.arguments[0].split(separator: "/").last.map(String.init) ?? "<command>"
22-
self.init(toolName: toolName, definition: definition)
21+
self.init(toolName: Self.executableName, definition: definition)
2322
}
2423

2524
init(toolName: String, parsable: ParsableArguments, visibility: ArgumentVisibility, parent: InputKey.Parent) {
@@ -34,6 +33,20 @@ extension UsageGenerator {
3433
}
3534

3635
extension UsageGenerator {
36+
/// Will generate a tool name from the name of the executed file if possible.
37+
///
38+
/// If no tool name can be generated, `"<command>"` will be returned.
39+
static var executableName: String {
40+
if let name = CommandLine.arguments[0].split(separator: "/").last.map(String.init) {
41+
// We quote the name if it contains whitespace to avoid confusion with
42+
// subcommands but otherwise leave properly quoting/escaping the command
43+
// up to the user running the tool
44+
return name.quotedIfContains(.whitespaces)
45+
} else {
46+
return "<command>"
47+
}
48+
}
49+
3750
/// The tool synopsis.
3851
///
3952
/// In `roff`.

Sources/ArgumentParser/Utilities/StringExtensions.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
//
1010
//===----------------------------------------------------------------------===//
1111

12+
@_implementationOnly import Foundation
13+
1214
extension StringProtocol where SubSequence == Substring {
1315
func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String {
1416
let columns = columns - wrappingIndent
@@ -120,6 +122,32 @@ extension StringProtocol where SubSequence == Substring {
120122
return result
121123
}
122124

125+
/// Returns a new single-quoted string if this string contains any characters
126+
/// from the specified character set. Any existing occurrences of the `'`
127+
/// character will be escaped.
128+
///
129+
/// Examples:
130+
///
131+
/// "alone".quotedIfContains(.whitespaces)
132+
/// // alone
133+
/// "with space".quotedIfContains(.whitespaces)
134+
/// // 'with space'
135+
/// "with'quote".quotedIfContains(.whitespaces)
136+
/// // with'quote
137+
/// "with'quote and space".quotedIfContains(.whitespaces)
138+
/// // 'with\'quote and space'
139+
func quotedIfContains(_ chars: CharacterSet) -> String {
140+
guard !isEmpty else { return "" }
141+
142+
if self.rangeOfCharacter(from: chars) != nil {
143+
// Prepend and append a single quote to self, escaping any other occurrences of the character
144+
let quote = "'"
145+
return quote + self.replacingOccurrences(of: quote, with: "\\\(quote)") + quote
146+
}
147+
148+
return String(self)
149+
}
150+
123151
/// Returns the edit distance between this string and the provided target string.
124152
///
125153
/// Uses the Levenshtein distance algorithm internally.

0 commit comments

Comments
 (0)