Skip to content

Commit

Permalink
Adds a new CommandConfiguration property, addressing apple#295
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
smvz committed Oct 4, 2022
1 parent b80fb05 commit c07660f
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 3 deletions.
14 changes: 14 additions & 0 deletions Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ public struct CommandConfiguration {
/// the command type to hyphen-separated lowercase words.
public var commandName: String?

/// A Boolean value indicating whether to use the executable's file name
/// for the command name.
///
/// If `commandName` or `_superCommandName` are non-`nil`, this
/// value is ignored.
public var shouldUseExecutableName: Bool

/// The name of this command's "super-command". (experimental)
///
/// Use this when a command is part of a group of commands that are installed
Expand Down Expand Up @@ -61,6 +68,9 @@ public struct CommandConfiguration {
/// - commandName: The name of the command to use on the command line. If
/// `commandName` is `nil`, the command name is derived by converting
/// the name of the command type to hyphen-separated lowercase words.
/// - shouldUseExecutableName: A Boolean value indicating whether to
/// use the executable's file name for the command name. If `commandName`
/// is non-`nil`, this value is ignored.
/// - abstract: A one-line description of the command.
/// - usage: A custom usage description for the command. When you provide
/// a non-`nil` string, the argument parser uses `usage` instead of
Expand All @@ -82,6 +92,7 @@ public struct CommandConfiguration {
/// are `-h` and `--help`.
public init(
commandName: String? = nil,
shouldUseExecutableName: Bool = false,
abstract: String = "",
usage: String? = nil,
discussion: String = "",
Expand All @@ -92,6 +103,7 @@ public struct CommandConfiguration {
helpNames: NameSpecification? = nil
) {
self.commandName = commandName
self.shouldUseExecutableName = shouldUseExecutableName
self.abstract = abstract
self.usage = usage
self.discussion = discussion
Expand All @@ -106,6 +118,7 @@ public struct CommandConfiguration {
/// (experimental)
public init(
commandName: String? = nil,
shouldUseExecutableName: Bool = false,
_superCommandName: String,
abstract: String = "",
usage: String? = nil,
Expand All @@ -117,6 +130,7 @@ public struct CommandConfiguration {
helpNames: NameSpecification? = nil
) {
self.commandName = commandName
self.shouldUseExecutableName = shouldUseExecutableName
self._superCommandName = _superCommandName
self.abstract = abstract
self.usage = usage
Expand Down
4 changes: 3 additions & 1 deletion Sources/ArgumentParser/Parsable Types/ParsableCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ public protocol ParsableCommand: ParsableArguments {
extension ParsableCommand {
public static var _commandName: String {
configuration.commandName ??
String(describing: Self.self).convertedToSnakeCase(separator: "-")
(configuration.shouldUseExecutableName && configuration._superCommandName == nil
? UsageGenerator.executableName
: String(describing: Self.self).convertedToSnakeCase(separator: "-"))
}

public static var configuration: CommandConfiguration {
Expand Down
17 changes: 15 additions & 2 deletions Sources/ArgumentParser/Usage/UsageGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ struct UsageGenerator {

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

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

extension UsageGenerator {
/// Will generate a tool name from the name of the executed file if possible.
///
/// If no tool name can be generated, `"<command>"` will be returned.
static var executableName: String {
if let name = CommandLine.arguments[0].split(separator: "/").last.map(String.init) {
// We quote the name if it contains whitespace to avoid confusion with
// subcommands but otherwise leave properly quoting/escaping the command
// up to the user running the tool
return name.quotedIfContains(.whitespaces)
} else {
return "<command>"
}
}

/// The tool synopsis.
///
/// In `roff`.
Expand Down
28 changes: 28 additions & 0 deletions Sources/ArgumentParser/Utilities/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
//
//===----------------------------------------------------------------------===//

@_implementationOnly import Foundation

extension StringProtocol where SubSequence == Substring {
func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String {
let columns = columns - wrappingIndent
Expand Down Expand Up @@ -120,6 +122,32 @@ extension StringProtocol where SubSequence == Substring {
return result
}

/// Returns a new single-quoted string if this string contains any characters
/// from the specified character set. Any existing occurrences of the `'`
/// character will be escaped.
///
/// Examples:
///
/// "alone".quotedIfContains(.whitespaces)
/// // alone
/// "with space".quotedIfContains(.whitespaces)
/// // 'with space'
/// "with'quote".quotedIfContains(.whitespaces)
/// // with'quote
/// "with'quote and space".quotedIfContains(.whitespaces)
/// // 'with\'quote and space'
func quotedIfContains(_ chars: CharacterSet) -> String {
guard !isEmpty else { return "" }

if self.rangeOfCharacter(from: chars) != nil {
// Prepend and append a single quote to self, escaping any other occurrences of the character
let quote = "'"
return quote + self.replacingOccurrences(of: quote, with: "\\\(quote)") + quote
}

return String(self)
}

/// Returns the edit distance between this string and the provided target string.
///
/// Uses the Levenshtein distance algorithm internally.
Expand Down

0 comments on commit c07660f

Please sign in to comment.