Skip to content

Commit

Permalink
Help text: Display argv[0] for root command by default
Browse files Browse the repository at this point in the history
Fixes: apple#633 apple#570 apple#295

This seems like the "correct" default. If your root command had the same name
as the resulting binary you produce, this was never an issue, however if
your root command was named anything else, then the current Usage/help text was
kind of odd.

If you don't specify an explicit commandName in CommandConfiguration, we would
take the root command name and generate a command name for you out of hyphens.
So, `RootCommand` would become `root-command` and this is what would be displayed
in the usage text for `/path/to/cool-binary --help`, even though intuitively you'd
expect to see `USAGE: cool-binary`.

The current behavior was also strange for binaries that are classically invoked
via symlinks as the same thing would happen. In this case imagine the struct name
matched the actual binaries name, but because the symlink is what users actually
invoke you could end up seeing an unfamiliar name in help text regardless. Using
argv[0] solves most of these problems.

The downside here is a LOT of tests need to change. I went with foregoing the
new approach if the user explicitly provides a command name, so most of the existing
tests that check for exact string matches of help text either need to replace the
command name with the first argument of the binary that is running the test (xctest,
but it doesn't really matter what it is as I've added a helper to plop in the first
argument for all of these tests), or they need to define
CommandConfiguration(commandName: "name-we-want"). For any tests that implemented
ParsableCommand I mostly went with the latter to make as few changes as possible.

Given this seems like mostly a UX change, I hope this seems sane.

Signed-off-by: Danny Canter <[email protected]>
  • Loading branch information
dcantah committed May 31, 2024
1 parent 0fbc884 commit 9fd1b90
Show file tree
Hide file tree
Showing 13 changed files with 178 additions and 31 deletions.
20 changes: 16 additions & 4 deletions Sources/ArgumentParser/Usage/HelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ internal struct HelpGenerator {
var commandStack: [ParsableCommand.Type]
var abstract: String
var usage: String
var commandNames: String
var sections: [Section]
var discussionSections: [DiscussionSection]

Expand All @@ -101,15 +102,22 @@ internal struct HelpGenerator {
self.commandStack = commandStack

// Build the tool name and subcommand name from the command configuration
var toolName = commandStack.map { $0._commandName }.joined(separator: " ")
if let superName = commandStack.first!.configuration._superCommandName {
toolName = "\(superName) \(toolName)"
self.commandNames = commandStack.map { $0._commandName }.joined(separator: " ")
self.commandNames = "\(superName) \(commandNames)"
} else {
// If the user explicitly provided a command name, use this. Otherwise default to showing argv[0].
let binaryName = CommandLine._staticArguments[0].split(separator: "/").last.map(String.init) ?? "<command>"
self.commandNames = commandStack[0].configuration.commandName ?? binaryName
if commandStack.count > 1 {
self.commandNames += " " + commandStack[1...].map { $0._commandName }.joined(separator: " ")
}
}

if let usage = currentCommand.configuration.usage {
self.usage = usage
} else {
var usage = UsageGenerator(toolName: toolName, definition: [currentArgSet])
var usage = UsageGenerator(toolName: self.commandNames, definition: [currentArgSet])
.synopsis
if !currentCommand.configuration.subcommands.isEmpty {
if usage.last != " " { usage += " " }
Expand Down Expand Up @@ -245,7 +253,11 @@ internal struct HelpGenerator {
guard !usage.isEmpty else { return "" }
return "Usage: \(usage.hangingIndentingEachLine(by: 7))"
}


func getCommandNames() -> String {
return self.commandNames
}

var includesSubcommands: Bool {
guard let subcommandSection = sections.first(where: { $0.header == .subcommands })
else { return false }
Expand Down
5 changes: 3 additions & 2 deletions Sources/ArgumentParser/Usage/MessageInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ enum MessageInfo {
parserError = .userValidationError(error)
}

var usage = HelpGenerator(commandStack: commandStack, visibility: .default).usageMessage()
let generator = HelpGenerator(commandStack: commandStack, visibility: .default)
var usage = generator.usageMessage()
let commandNames = generator.getCommandNames()

let commandNames = commandStack.map { $0._commandName }.joined(separator: " ")
if let helpName = commandStack.getPrimaryHelpName() {
if !usage.isEmpty {
usage += "\n"
Expand Down
4 changes: 4 additions & 0 deletions Sources/ArgumentParserTestHelpers/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ public func AssertParse<A>(_ type: A.Type, _ arguments: [String], file: StaticSt
}
}

public func getFirstArgument() -> String {
return CommandLine.arguments[0].split(separator: "/").last.map(String.init) ?? "<command>"
}

public func AssertParseCommand<A: ParsableCommand>(_ rootCommand: ParsableCommand.Type, _ type: A.Type, _ arguments: [String], file: StaticString = #file, line: UInt = #line, closure: (A) throws -> Void) {
do {
let command = try rootCommand.parseAsRoot(arguments)
Expand Down
4 changes: 3 additions & 1 deletion Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,9 @@ enum Shape: String, EnumerableFlag {
case oblong
}

fileprivate struct Baz: ParsableArguments {
fileprivate struct Baz: ParsableArguments, ParsableCommand {
static let configuration = CommandConfiguration(commandName: "baz")

@Flag()
var color: Color

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class SubcommandEndToEndTests: XCTestCase {

fileprivate struct Foo: ParsableCommand {
static let configuration =
CommandConfiguration(subcommands: [CommandA.self, CommandB.self])
CommandConfiguration(commandName: "foo", subcommands: [CommandA.self, CommandB.self])

@Option() var name: String
}
Expand Down
12 changes: 7 additions & 5 deletions Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ extension Convert {
fileprivate struct FooOption: Convert, ParsableArguments {

static var usageString: String = """
Usage: foo_option --string <int_str>
See 'foo_option --help' for more information.
Usage: \(getFirstArgument()) --string <int_str>
See '\(getFirstArgument()) --help' for more information.
"""
static var help: String = "Help: --string <int_str> Convert string to integer\n"

Expand All @@ -48,7 +48,8 @@ fileprivate struct FooOption: Convert, ParsableArguments {
}

fileprivate struct BarOption: Convert, ParsableCommand {

static var configuration = CommandConfiguration(commandName: "bar-option")

static var usageString: String = """
Usage: bar-option [--strings <int_str> ...]
See 'bar-option --help' for more information.
Expand Down Expand Up @@ -100,8 +101,8 @@ extension TransformEndToEndTests {
fileprivate struct FooArgument: Convert, ParsableArguments {

static var usageString: String = """
Usage: foo_argument <int_str>
See 'foo_argument --help' for more information.
Usage: \(getFirstArgument()) <int_str>
See '\(getFirstArgument()) --help' for more information.
"""
static var help: String = "Help: <int_str> Convert string to integer\n"

Expand All @@ -115,6 +116,7 @@ fileprivate struct FooArgument: Convert, ParsableArguments {
}

fileprivate struct BarArgument: Convert, ParsableCommand {
static var configuration = CommandConfiguration(commandName: "bar-argument")

static var usageString: String = """
Usage: bar-argument [<int_str> ...]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ fileprivate enum UserValidationError: LocalizedError {

fileprivate struct Foo: ParsableArguments {
static var usageString: String = """
Usage: foo [--count <count>] [<names> ...] [--version] [--throw]
See 'foo --help' for more information.
Usage: \(getFirstArgument()) [--count <count>] [<names> ...] [--version] [--throw]
See '\(getFirstArgument()) --help' for more information.
"""

static var helpString: String = """
USAGE: foo [--count <count>] [<names> ...] [--version] [--throw]
USAGE: \(getFirstArgument()) [--count <count>] [<names> ...] [--version] [--throw]
ARGUMENTS:
<names>
Expand Down
4 changes: 3 additions & 1 deletion Tests/ArgumentParserPackageManagerTests/HelpTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ struct Simple: ParsableArguments {
@Argument() var max: Int

static var helpText = """
USAGE: simple [--verbose] [--min <min>] <max>
USAGE: \(getFirstArgument()) [--verbose] [--min <min>] <max>
ARGUMENTS:
<max>
Expand All @@ -195,6 +195,7 @@ extension HelpTests {

struct CustomHelp: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "custom-help",
helpNames: [.customShort("?"), .customLong("show-help")]
)
}
Expand All @@ -216,6 +217,7 @@ extension HelpTests {

struct NoHelp: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "no-help",
helpNames: []
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ struct Options: ParsableArguments {

struct Package: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "package",
subcommands: [Clean.self, Config.self, Describe.self, GenerateXcodeProject.self, Hidden.self])
}

Expand Down
48 changes: 48 additions & 0 deletions Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,41 +22,57 @@ extension HelpGenerationTests {
struct A { }

struct BareNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "bare-no-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A
}

struct BareDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "bare-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A = A()
}

struct OptionalNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-no-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A?
}

struct OptionalDefaultNil: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-default-nil")

@Argument(help: "example", transform: { _ in A() })
var arg0: A? = nil
}

struct OptionalDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A? = A()
}

struct ArrayNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-no-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: [A]
}

struct ArrayDefaultEmpty: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-default-empty")

@Argument(help: "example", transform: { _ in A() })
var arg0: [A] = []
}

struct ArrayDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: [A] = [A()]
}
Expand Down Expand Up @@ -202,42 +218,58 @@ extension HelpGenerationTests {
}

struct BareNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "bare-no-default")

@Argument(help: "example")
var arg0: A
}

struct BareDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "bare-default")

@Argument(help: "example")
var arg0: A = A()
}

struct OptionalNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-no-default")

@Argument(help: "example")
var arg0: A?
}

struct OptionalDefaultNil: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-default-nil")

@Argument(help: "example")
var arg0: A? = nil
}

@available(*, deprecated, message: "Included for test coverage")
struct OptionalDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-default")

@Argument(help: "example")
var arg0: A? = A()
}

struct ArrayNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-no-default")

@Argument(help: "example")
var arg0: [A]
}

struct ArrayDefaultEmpty: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-default-empty")

@Argument(help: "example")
var arg0: [A] = []
}

struct ArrayDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-default")

@Argument(help: "example")
var arg0: [A] = [A()]
}
Expand Down Expand Up @@ -367,41 +399,57 @@ extension HelpGenerationTests {
}

struct BareNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "bare-no-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A
}

struct BareDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "bare-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A = A()
}

struct OptionalNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-no-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A?
}

struct OptionalDefaultNil: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-default-nil")

@Argument(help: "example", transform: { _ in A() })
var arg0: A? = nil
}

struct OptionalDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "optional-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: A? = A()
}

struct ArrayNoDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-no-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: [A]
}

struct ArrayDefaultEmpty: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-default-empty")

@Argument(help: "example", transform: { _ in A() })
var arg0: [A] = []
}

struct ArrayDefault: ParsableCommand {
static let configuration = CommandConfiguration(commandName: "array-default")

@Argument(help: "example", transform: { _ in A() })
var arg0: [A] = [A()]
}
Expand Down
Loading

0 comments on commit 9fd1b90

Please sign in to comment.