diff --git a/Documentation/02 Arguments, Options, and Flags.md b/Documentation/02 Arguments, Options, and Flags.md index 92e38cd0c..e63a8c029 100644 --- a/Documentation/02 Arguments, Options, and Flags.md +++ b/Documentation/02 Arguments, Options, and Flags.md @@ -63,9 +63,11 @@ When called without both values, the command exits with an error: % example 5 Error: Missing '--user-name ' Usage: example --user-name + See 'example --help' for more information. % example --user-name kjohnson Error: Missing '' Usage: example --user-name + See 'example --help' for more information. ``` ## Customizing option and flag names @@ -293,6 +295,7 @@ Verbose: true, name: Tomás, file: none % example --name --verbose Tomás Error: Missing value for '--name ' Usage: example [--verbose] --name [] + See 'example --help' for more information. ``` Parsing options as arrays is similar — only adjacent key-value pairs are recognized by default. @@ -338,6 +341,7 @@ Verbose: true, files: ["file1.swift", "file2.swift"] % example --file --verbose file1.swift --file file2.swift Error: Missing value for '--file ' Usage: example [--file ...] [--verbose] + See 'example --help' for more information. ``` The `.unconditionalSingleValue` parsing strategy uses whatever input follows the key as its value, even if that input is dash-prefixed. If `file` were defined as `@Option(parsing: .unconditionalSingleValue) var file: [String]`, then the resulting array could include strings that look like options: @@ -388,6 +392,7 @@ Verbose: true, files: ["file1.swift", "file2.swift"] % example --verbose file1.swift file2.swift --other Error: Unexpected argument '--other' Usage: example [--verbose] [ ...] + See 'example --help' for more information. ``` Any input after the `--` terminator is automatically treated as positional input, so users can provide dash-prefixed values that way even with the default configuration: diff --git a/Documentation/03 Commands and Subcommands.md b/Documentation/03 Commands and Subcommands.md index 38dce8b3a..ada023726 100644 --- a/Documentation/03 Commands and Subcommands.md +++ b/Documentation/03 Commands and Subcommands.md @@ -27,6 +27,8 @@ SUBCOMMANDS: average 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 ' for detailed help. ``` Start by defining the root `Math` command. You can provide a static `configuration` property for a command that specifies its subcommands and a default subcommand, if any. diff --git a/Documentation/05 Validation and Errors.md b/Documentation/05 Validation and Errors.md index 561e1dfc9..cc5723f12 100644 --- a/Documentation/05 Validation and Errors.md +++ b/Documentation/05 Validation and Errors.md @@ -44,12 +44,15 @@ When you provide useful error messages, they can guide new users to success with % select Error: Please provide at least one element to choose from. Usage: select [--count ] [ ...] + See 'select --help' for more information. % select --count 2 hello Error: Please specify a 'count' less than the number of elements. Usage: select [--count ] [ ...] + See 'select --help' for more information. % select --count 0 hello hey hi howdy Error: Please specify a 'count' of at least 1. Usage: select [--count ] [ ...] + See 'select --help' for more information. % select --count 2 hello hey hi howdy howdy hey @@ -145,6 +148,7 @@ Throwing from a transform closure benefits users by providing context and can re % example '{"Bad JSON"}' Error: The value '{"Bad JSON"}' is invalid for '': dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "No value for key in object around character 11." UserInfo={NSDebugDescription=No value for key in object around character 11.}))) Usage: example --fail-option + See 'select --help' for more information. ``` While throwing standard library or Foundation errors adds context, custom errors provide the best experience for users and developers. @@ -153,4 +157,5 @@ While throwing standard library or Foundation errors adds context, custom errors % example '{"tokenCount":0,"tokens":[],"identifier":"F77D661C-C5B7-448E-9344-267B284F66AD"}' --fail-option="Some Text Here!" Error: The value 'Some Text Here!' is invalid for '--fail-option ': Trying to write to failOption always produces an error. Input: Some Text Here! Usage: example --fail-option + See 'select --help' for more information. ``` diff --git a/README.md b/README.md index c866b6df4..dbe49e008 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ hello $ repeat Error: Missing required value for argument 'phrase'. Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. $ repeat --help USAGE: repeat [--count ] [--include-counter] diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 182a52a7f..26ac4301c 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -96,6 +96,7 @@ internal struct HelpGenerator { var content: String } + var commandStack: [ParsableCommand.Type] var abstract: String var usage: Usage var sections: [Section] @@ -107,7 +108,8 @@ internal struct HelpGenerator { } let currentArgSet = ArgumentSet(currentCommand) - + self.commandStack = commandStack + let toolName = commandStack.map { $0._commandName }.joined(separator: " ") var usageString = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis if !currentCommand.configuration.subcommands.isEmpty { @@ -231,6 +233,12 @@ internal struct HelpGenerator { return "Usage: \(usage.rendered(screenWidth: screenWidth))" } + var includesSubcommands: Bool { + guard let subcommandSection = sections.first(where: { $0.header == .subcommands }) + else { return false } + return !subcommandSection.elements.isEmpty + } + func rendered(screenWidth: Int? = nil) -> String { let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth let renderedSections = sections @@ -241,11 +249,21 @@ internal struct HelpGenerator { ? "" : "OVERVIEW: \(abstract)".wrapped(to: screenWidth) + "\n\n" + var helpSubcommandMessage: String = "" + if includesSubcommands { + var names = commandStack.map { $0._commandName } + names.insert("help", at: 1) + helpSubcommandMessage = """ + + See '\(names.joined(separator: " ")) ' for detailed help. + """ + } + return """ \(renderedAbstract)\ USAGE: \(usage.rendered(screenWidth: screenWidth)) - \(renderedSections) + \(renderedSections)\(helpSubcommandMessage) """ } } diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 882c67bb9..4c4948b51 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -53,7 +53,9 @@ enum MessageInfo { parserError = .userValidationError(error) } + let commandNames = commandStack.map { $0._commandName }.joined(separator: " ") let usage = HelpGenerator(commandStack: commandStack).usageMessage() + + "\n See '\(commandNames) --help' for more information." // Parsing errors and user-thrown validation errors have the usage // string attached. Other errors just get the error message. diff --git a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift index e40fcbfe4..9f44089e0 100644 --- a/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift @@ -81,6 +81,7 @@ extension SubcommandEndToEndTests { a b + See 'foo help ' for detailed help. """, helpFoo) AssertEqualStringsIgnoringTrailingWhitespace(""" USAGE: foo a --name --bar diff --git a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift index c6f4b898d..82c47fbfa 100644 --- a/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift @@ -38,6 +38,7 @@ fileprivate struct FooOption: Convert, ParsableArguments { static var usageString: String = """ Usage: foo_option --string + See 'foo_option --help' for more information. """ @Option(help: ArgumentHelp("Convert string to integer", valueName: "int_str"), @@ -49,6 +50,7 @@ fileprivate struct BarOption: Convert, ParsableCommand { static var usageString: String = """ Usage: bar-option [--strings ...] + See 'bar-option --help' for more information. """ @Option(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"), @@ -97,6 +99,7 @@ fileprivate struct FooArgument: Convert, ParsableArguments { static var usageString: String = """ Usage: foo_argument + See 'foo_argument --help' for more information. """ enum FooError: Error { @@ -112,6 +115,7 @@ fileprivate struct BarArgument: Convert, ParsableCommand { static var usageString: String = """ Usage: bar-argument [ ...] + See 'bar-argument --help' for more information. """ @Argument(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"), diff --git a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift index bc77e8823..0bff871bd 100644 --- a/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift @@ -30,6 +30,7 @@ fileprivate enum UserValidationError: LocalizedError { fileprivate struct Foo: ParsableArguments { static var usageString: String = """ Usage: foo [--count ] [ ...] [--version] [--throw] + See 'foo --help' for more information. """ static var helpString: String = """ diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index e171ff61a..cd0761462 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -33,6 +33,8 @@ final class MathExampleTests: XCTestCase { add Print the sum of the values. multiply Print the product of the values. stats Calculate descriptive statistics. + + See 'math help ' for detailed help. """ AssertExecuteCommand(command: "math -h", expected: helpText) @@ -109,6 +111,7 @@ final class MathExampleTests: XCTestCase { expected: """ Error: Please provide at least one value to calculate the mode. Usage: math stats average [--kind ] [ ...] + See 'math stats average --help' for more information. """, exitCode: .validationFailure) } @@ -150,6 +153,7 @@ final class MathExampleTests: XCTestCase { expected: """ Error: Unknown option '--foo' Usage: math add [--hex-output] [ ...] + See 'math add --help' for more information. """, exitCode: .validationFailure) @@ -158,6 +162,7 @@ final class MathExampleTests: XCTestCase { expected: """ Error: The value 'ZZZ' is invalid for '' Usage: math add [--hex-output] [ ...] + See 'math add --help' for more information. """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift index 9dc77f14a..4e6b3d46a 100644 --- a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift @@ -65,6 +65,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ Error: Missing value for '--count ' Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. """, exitCode: .validationFailure) @@ -73,6 +74,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ Error: The value 'ZZZ' is invalid for '--count ' Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. """, exitCode: .validationFailure) @@ -81,6 +83,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ Error: Unknown option '--version' Usage: repeat [--count ] [--include-counter] + See 'repeat --help' for more information. """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift index 732788a45..fb3fc138d 100644 --- a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift @@ -41,6 +41,7 @@ final class RollDiceExampleTests: XCTestCase { expected: """ Error: Missing value for '--times ' Usage: roll [--times ] [--sides ] [--seed ] [--verbose] + See 'roll --help' for more information. """, exitCode: .validationFailure) @@ -49,6 +50,7 @@ final class RollDiceExampleTests: XCTestCase { expected: """ Error: The value 'ZZZ' is invalid for '--times ' Usage: roll [--times ] [--sides ] [--seed ] [--verbose] + See 'roll --help' for more information. """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index 54a990b2c..6af060005 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -56,6 +56,7 @@ extension HelpTests { describe generate-xcodeproj + See 'package help ' for detailed help. """.trimmingLines()) } @@ -74,6 +75,7 @@ extension HelpTests { describe generate-xcodeproj + See 'package help ' for detailed help. """.trimmingLines() ) } @@ -100,6 +102,7 @@ extension HelpTests { set-mirror unset-mirror + See 'package help config ' for detailed help. """.trimmingLines()) } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index 122ddea04..0ff371d7f 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -264,6 +264,7 @@ extension HelpGenerationTests { Test long command name. another-command + See 'h help ' for detailed help. """) AssertHelp(for: H.AnotherCommand.self, root: H.self, equals: """