From 9fd1b9073a704bddf02e6af72fd4000bb63ec649 Mon Sep 17 00:00:00 2001 From: Danny Canter Date: Fri, 31 May 2024 15:04:27 -0700 Subject: [PATCH 1/3] Help text: Display argv[0] for root command by default Fixes: #633 #570 #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 --- .../ArgumentParser/Usage/HelpGenerator.swift | 20 ++++++-- .../ArgumentParser/Usage/MessageInfo.swift | 5 +- .../TestHelpers.swift | 4 ++ .../FlagsEndToEndTests.swift | 4 +- .../SubcommandEndToEndTests.swift | 2 +- .../TransformEndToEndTests.swift | 12 +++-- .../ValidationEndToEndTests.swift | 6 +-- .../HelpTests.swift | 4 +- .../PackageManager/Options.swift | 1 + .../HelpGenerationTests+AtArgument.swift | 48 +++++++++++++++++++ .../HelpGenerationTests+AtOption.swift | 48 +++++++++++++++++++ .../HelpGenerationTests+GroupName.swift | 14 +++++- .../HelpGenerationTests.swift | 41 +++++++++++----- 13 files changed, 178 insertions(+), 31 deletions(-) diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 6dfb411a5..c7f7908fb 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -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] @@ -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) ?? "" + 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 += " " } @@ -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 } diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index bb9a4ea1d..7b86a0fed 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -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" diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index fb3019f0e..4a7a9b2ca 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -119,6 +119,10 @@ public func AssertParse(_ type: A.Type, _ arguments: [String], file: StaticSt } } +public func getFirstArgument() -> String { + return CommandLine.arguments[0].split(separator: "/").last.map(String.init) ?? "" +} + public func AssertParseCommand(_ 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) diff --git a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift index 89c16e3be..36c17bd8e 100644 --- a/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/FlagsEndToEndTests.swift @@ -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 diff --git a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift index 856ec6e93..e47897707 100644 --- a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift @@ -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 } diff --git a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift index 957eae5fe..1ef6fb080 100644 --- a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift @@ -37,8 +37,8 @@ extension Convert { fileprivate struct FooOption: Convert, ParsableArguments { static var usageString: String = """ - Usage: foo_option --string - See 'foo_option --help' for more information. + Usage: \(getFirstArgument()) --string + See '\(getFirstArgument()) --help' for more information. """ static var help: String = "Help: --string Convert string to integer\n" @@ -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 ...] See 'bar-option --help' for more information. @@ -100,8 +101,8 @@ extension TransformEndToEndTests { fileprivate struct FooArgument: Convert, ParsableArguments { static var usageString: String = """ - Usage: foo_argument - See 'foo_argument --help' for more information. + Usage: \(getFirstArgument()) + See '\(getFirstArgument()) --help' for more information. """ static var help: String = "Help: Convert string to integer\n" @@ -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 [ ...] diff --git a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift index b4222bc3c..c5f228176 100644 --- a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift @@ -29,12 +29,12 @@ fileprivate enum UserValidationError: LocalizedError { fileprivate struct Foo: ParsableArguments { static var usageString: String = """ - Usage: foo [--count ] [ ...] [--version] [--throw] - See 'foo --help' for more information. + Usage: \(getFirstArgument()) [--count ] [ ...] [--version] [--throw] + See '\(getFirstArgument()) --help' for more information. """ static var helpString: String = """ - USAGE: foo [--count ] [ ...] [--version] [--throw] + USAGE: \(getFirstArgument()) [--count ] [ ...] [--version] [--throw] ARGUMENTS: diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index cb67aadb7..07ace1eb5 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -169,7 +169,7 @@ struct Simple: ParsableArguments { @Argument() var max: Int static var helpText = """ - USAGE: simple [--verbose] [--min ] + USAGE: \(getFirstArgument()) [--verbose] [--min ] ARGUMENTS: @@ -195,6 +195,7 @@ extension HelpTests { struct CustomHelp: ParsableCommand { static let configuration = CommandConfiguration( + commandName: "custom-help", helpNames: [.customShort("?"), .customLong("show-help")] ) } @@ -216,6 +217,7 @@ extension HelpTests { struct NoHelp: ParsableCommand { static let configuration = CommandConfiguration( + commandName: "no-help", helpNames: [] ) diff --git a/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift b/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift index a14ac3ed6..686c4101c 100644 --- a/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift +++ b/Tests/ArgumentParserPackageManagerTests/PackageManager/Options.swift @@ -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]) } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift index fbda52028..bed1be9ed 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift @@ -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()] } @@ -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()] } @@ -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()] } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift index ad008ae9f..dc2bc7219 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift @@ -22,41 +22,57 @@ extension HelpGenerationTests { struct A { } struct BareNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "bare-no-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A } struct BareDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "bare-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A = A() } struct OptionalNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-no-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A? } struct OptionalDefaultNil: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-default-nil") + @Option(help: "example", transform: { _ in A() }) var arg0: A? = nil } struct OptionalDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A? = A() } struct ArrayNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-no-default") + @Option(help: "example", transform: { _ in A() }) var arg0: [A] } struct ArrayDefaultEmpty: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-default-empty") + @Option(help: "example", transform: { _ in A() }) var arg0: [A] = [] } struct ArrayDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-default") + @Option(help: "example", transform: { _ in A() }) var arg0: [A] = [A()] } @@ -162,42 +178,58 @@ extension HelpGenerationTests { } struct BareNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "bare-no-default") + @Option(help: "example") var arg0: A } struct BareDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "bare-default") + @Option(help: "example") var arg0: A = A() } struct OptionalNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-no-default") + @Option(help: "example") var arg0: A? } struct OptionalDefaultNil: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-default-nil") + @Option(help: "example") var arg0: A? = nil } @available(*, deprecated, message: "Included for test coverage") struct OptionalDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-default") + @Option(help: "example") var arg0: A? = A() } struct ArrayNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-no-default") + @Option(help: "example") var arg0: [A] } struct ArrayDefaultEmpty: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-default-empty") + @Option(help: "example") var arg0: [A] = [] } struct ArrayDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-default") + @Option(help: "example") var arg0: [A] = [A()] } @@ -292,41 +324,57 @@ extension HelpGenerationTests { } struct BareNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "bare-no-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A } struct BareDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "bare-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A = A() } struct OptionalNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-no-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A? } struct OptionalDefaultNil: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-default-nil") + @Option(help: "example", transform: { _ in A() }) var arg0: A? = nil } struct OptionalDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "optional-default") + @Option(help: "example", transform: { _ in A() }) var arg0: A? = A() } struct ArrayNoDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-no-default") + @Option(help: "example", transform: { _ in A() }) var arg0: [A] } struct ArrayDefaultEmpty: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-default-empty") + @Option(help: "example", transform: { _ in A() }) var arg0: [A] = [] } struct ArrayDefault: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "array-default") + @Option(help: "example", transform: { _ in A() }) var arg0: [A] = [A()] } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+GroupName.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+GroupName.swift index 62492057f..627bcda72 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests+GroupName.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+GroupName.swift @@ -53,6 +53,8 @@ extension HelpGenerationTests { } fileprivate struct AllVisible: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "all-visible") + @OptionGroup(title: "Flags Group") var flags: Flags @@ -67,6 +69,8 @@ extension HelpGenerationTests { } fileprivate struct ContainsOptionGroup: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "all-visible") + @OptionGroup(title: "Flags Group") var flags: Flags @@ -122,6 +126,8 @@ extension HelpGenerationTests { } fileprivate struct Combined: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "combined") + @OptionGroup(title: "Extras") var flags: Flags @@ -179,6 +185,8 @@ extension HelpGenerationTests { } fileprivate struct HiddenGroups: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "hidden-groups") + @OptionGroup(title: "Flags Group", visibility: .hidden) var flags: Flags @@ -222,7 +230,7 @@ extension HelpGenerationTests { fileprivate struct ParentWithGroups: ParsableCommand { static var configuration: CommandConfiguration { - .init(subcommands: [ChildWithGroups.self]) + .init(commandName: "parent-with-groups", subcommands: [ChildWithGroups.self]) } @OptionGroup(title: "Extras") @@ -328,6 +336,8 @@ extension HelpGenerationTests { } fileprivate struct GroupsWithUnnamedGroups: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "groups-with-unnamed-groups") + @OptionGroup var extras: ContainsOptionGroup } @@ -351,6 +361,8 @@ extension HelpGenerationTests { } fileprivate struct GroupsWithNamedGroups: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "groups-with-named-groups") + @OptionGroup(title: "Nested") var extras: ContainsOptionGroup } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index 047eae80c..01953359d 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -41,7 +41,7 @@ extension HelpGenerationTests { func testHelp() { AssertHelp(.default, for: A.self, equals: """ - USAGE: a --name [--title ] + USAGE: \(getFirstArgument()) --name <name> [--title <title>] OPTIONS: --name <name> Your name @@ -63,7 +63,7 @@ extension HelpGenerationTests { func testHelpWithHidden() { AssertHelp(.default, for: B.self, equals: """ - USAGE: b --name <name> [--title <title>] + USAGE: \(getFirstArgument()) --name <name> [--title <title>] OPTIONS: --name <name> Your name @@ -73,7 +73,7 @@ extension HelpGenerationTests { """) AssertHelp(.hidden, for: B.self, equals: """ - USAGE: b --name <name> [--title <title>] [<hidden-name>] [--hidden-title <hidden-title>] [--hidden-flag] [--hidden-inverted-flag] [--no-hidden-inverted-flag] + USAGE: \(getFirstArgument()) --name <name> [--title <title>] [<hidden-name>] [--hidden-title <hidden-title>] [--hidden-flag] [--hidden-inverted-flag] [--no-hidden-inverted-flag] ARGUMENTS: <hidden-name> @@ -98,7 +98,7 @@ extension HelpGenerationTests { func testHelpWithDiscussion() { AssertHelp(.default, for: C.self, equals: """ - USAGE: c --name <name> + USAGE: \(getFirstArgument()) --name <name> OPTIONS: --name <name> Your name. @@ -121,7 +121,7 @@ extension HelpGenerationTests { func testHelpWithDefaultValueButNoDiscussion() { AssertHelp(.default, for: Issue27.self, equals: """ - USAGE: issue27 [--two <two>] --three <three> [--four <four>] [--five <five>] + USAGE: \(getFirstArgument()) [--two <two>] --three <three> [--four <four>] [--five <five>] OPTIONS: --two <two> (default: 42) @@ -151,6 +151,8 @@ extension HelpGenerationTests { } struct D: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "d") + @Argument(help: "Your occupation.") var occupation: String = "--" @@ -220,6 +222,8 @@ extension HelpGenerationTests { } struct E: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "e") + enum OutputBehaviour: String, EnumerableFlag { case stats, count, list @@ -233,6 +237,8 @@ extension HelpGenerationTests { } struct F: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "f") + enum OutputBehaviour: String, EnumerableFlag { case stats, count, list @@ -246,6 +252,8 @@ extension HelpGenerationTests { } struct G: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "g") + @Flag(inversion: .prefixedNo, help: "Whether to flag") var flag: Bool = false } @@ -304,7 +312,7 @@ extension HelpGenerationTests { @Argument var argument: String = "" } - static let configuration = CommandConfiguration(subcommands: [CommandWithVeryLongName.self,ShortCommand.self,AnotherCommandWithVeryLongName.self,AnotherCommand.self]) + static let configuration = CommandConfiguration(commandName: "h", subcommands: [CommandWithVeryLongName.self,ShortCommand.self,AnotherCommandWithVeryLongName.self,AnotherCommand.self]) } func testHelpWithSubcommands() { @@ -342,7 +350,7 @@ extension HelpGenerationTests { } struct I: ParsableCommand { - static let configuration = CommandConfiguration(version: "1.0.0") + static let configuration = CommandConfiguration(commandName: "i", version: "1.0.0") } func testHelpWithVersion() { @@ -358,7 +366,7 @@ extension HelpGenerationTests { } struct J: ParsableCommand { - static let configuration = CommandConfiguration(discussion: "test") + static let configuration = CommandConfiguration(commandName: "j", discussion: "test") } func testOverviewButNoAbstractSpacing() { @@ -377,6 +385,8 @@ extension HelpGenerationTests { } struct K: ParsableCommand { + static let configuration = CommandConfiguration(commandName: "k") + @Argument(help: "A list of paths.") var paths: [String] = [] @@ -409,7 +419,7 @@ extension HelpGenerationTests { func testHelpWithMultipleCustomNames() { AssertHelp(.default, for: L.self, equals: """ - USAGE: l [--remote <remote>] + USAGE: \(getFirstArgument()) [--remote <remote>] OPTIONS: -t, -x, -y, --remote, --when, --time, -other, --there <remote> @@ -422,7 +432,7 @@ extension HelpGenerationTests { struct M: ParsableCommand { } struct N: ParsableCommand { - static let configuration = CommandConfiguration(subcommands: [M.self], defaultSubcommand: M.self) + static let configuration = CommandConfiguration(commandName: "n", subcommands: [M.self], defaultSubcommand: M.self) } func testHelpWithDefaultCommand() { @@ -461,7 +471,7 @@ extension HelpGenerationTests { func testHelpWithDefaultValueForArray() { AssertHelp(.default, for: P.self, equals: """ - USAGE: p [-o <o> ...] [<remainder> ...] + USAGE: \(getFirstArgument()) [-o <o> ...] [<remainder> ...] ARGUMENTS: <remainder> Help Message (default: large) @@ -662,7 +672,7 @@ extension HelpGenerationTests { func testHelpWithPrivate() { AssertHelp(.default, for: Q.self, equals: """ - USAGE: q --name <name> [--title <title>] + USAGE: \(getFirstArgument()) --name <name> [--title <title>] OPTIONS: --name <name> Your name @@ -718,6 +728,7 @@ extension HelpGenerationTests { } static let configuration = CommandConfiguration( + commandName: "non-custom-usage", subcommands: [ExampleSubcommand.self]) @Argument var file: String @@ -726,6 +737,7 @@ extension HelpGenerationTests { struct CustomUsageShort: ParsableCommand { static let configuration = CommandConfiguration( + commandName: "custom-usage-short", usage: """ example [--verbose] <file-name> """) @@ -736,6 +748,7 @@ extension HelpGenerationTests { struct CustomUsageLong: ParsableCommand { static let configuration = CommandConfiguration( + commandName: "custom-usage-long", usage: """ example <file-name> example --verbose <file-name> @@ -747,7 +760,7 @@ extension HelpGenerationTests { } struct CustomUsageHidden: ParsableCommand { - static let configuration = CommandConfiguration(usage: "") + static let configuration = CommandConfiguration(commandName: "custom-usage-hidden", usage: "") @Argument var file: String @Flag var verboseMode = false @@ -901,6 +914,8 @@ extension HelpGenerationTests { extension HelpGenerationTests { private struct WideHelp: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "wide-help") + @Argument(help: "54 characters of help, so as to wrap when columns < 80") var argument: String? } From 7f35ffbf3ee7b37e113a12e3ee2e1811bd24061c Mon Sep 17 00:00:00 2001 From: Danny Canter <danny@dcantah.dev> Date: Fri, 31 May 2024 15:57:50 -0700 Subject: [PATCH 2/3] HelpGenerationTests: Add tests for new root command behavior The behavior for displaying the root command name in the help text was recently changed to default to showing argv[0] by default unless you specify an explicit `commandName`. Add some simple tests to verify this. Signed-off-by: Danny Canter <danny@dcantah.dev> --- .../HelpGenerationTests.swift | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index 01953359d..d83cc4470 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -910,6 +910,76 @@ extension HelpGenerationTests { expected: """ """) } + + struct FunSubCommand: ParsableCommand {} + + struct NoCommandName: ParsableCommand { + @Flag(inversion: .prefixedNo, help: "Whether to flag") + var flag: Bool = false + } + + struct WithCommandName: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "with-command-name") + + @Flag(inversion: .prefixedNo, help: "Whether to flag") + var flag: Bool = false + } + + struct NoCommandNameWithSubCommands: ParsableCommand { + static var configuration = CommandConfiguration(subcommands: [FunSubCommand.self]) + + @Flag(inversion: .prefixedNo, help: "Whether to flag") + var flag: Bool = false + } + + struct WithCommandNameWithSubCommand: ParsableCommand { + static var configuration = CommandConfiguration(commandName: "with-command-name", subcommands: [FunSubCommand.self]) + + @Flag(inversion: .prefixedNo, help: "Whether to flag") + var flag: Bool = false + } + + func testParsableCommandWithNoCommandName() { + AssertHelp(.default, for: NoCommandName.self, equals: """ + USAGE: \(getFirstArgument()) [--flag] [--no-flag] + + OPTIONS: + --flag/--no-flag Whether to flag (default: --no-flag) + -h, --help Show help information. + + """) + } + + func testParsableCommandWithCommandName() { + AssertHelp(.default, for: WithCommandName.self, equals: """ + USAGE: with-command-name [--flag] [--no-flag] + + OPTIONS: + --flag/--no-flag Whether to flag (default: --no-flag) + -h, --help Show help information. + + """) + } + + func testParsableCommandWithNoCommandNameAndSubCommand() { + AssertHelp(.default, for: FunSubCommand.self, root: NoCommandNameWithSubCommands.self, equals: """ + USAGE: \(getFirstArgument()) fun-sub-command + + OPTIONS: + -h, --help Show help information. + + """) + } + + func testParsableCommandWithCommandNameAndSubCommand() { + AssertHelp(.default, for: FunSubCommand.self, root: WithCommandNameWithSubCommand.self, equals: """ + USAGE: with-command-name fun-sub-command + + OPTIONS: + -h, --help Show help information. + + """) + } } extension HelpGenerationTests { From 59f35b25a70254b8188cb5994bfe4cf0b951c0a4 Mon Sep 17 00:00:00 2001 From: Danny Canter <danny@dcantah.dev> Date: Fri, 31 May 2024 16:03:27 -0700 Subject: [PATCH 3/3] CommandConfiguration: Clarify help text behavior of commandName Make it clear the behavior of what will be displayed in help text if commandName is supplied or not. Signed-off-by: Danny Canter <danny@dcantah.dev> --- .../ArgumentParser/Parsable Types/CommandConfiguration.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift index b208c1ea1..55506b84f 100644 --- a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift @@ -15,6 +15,10 @@ public struct CommandConfiguration: Sendable { /// /// If `nil`, the command name is derived by converting the name of /// the command type to hyphen-separated lowercase words. + /// + /// NOTE: In help and usage texts if this is the root command and commandName is + /// `nil`, the name shown will be derived from the binary being invoked (argv[0]), + /// otherwise `commandName` will be displayed. public var commandName: String? /// The name of this command's "super-command". (experimental)