Skip to content

Commit 85196ee

Browse files
authored
Additional help messages (#165)
* Add 'see help' messages to usage messages and the help screen * Update tests for new help messages. * Update guide examples with additional help messages
1 parent 4143a35 commit 85196ee

File tree

14 files changed

+55
-2
lines changed

14 files changed

+55
-2
lines changed

Documentation/02 Arguments, Options, and Flags.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,11 @@ When called without both values, the command exits with an error:
6363
% example 5
6464
Error: Missing '--user-name <user-name>'
6565
Usage: example --user-name <user-name> <value>
66+
See 'example --help' for more information.
6667
% example --user-name kjohnson
6768
Error: Missing '<value>'
6869
Usage: example --user-name <user-name> <value>
70+
See 'example --help' for more information.
6971
```
7072

7173
## Customizing option and flag names
@@ -293,6 +295,7 @@ Verbose: true, name: Tomás, file: none
293295
% example --name --verbose Tomás
294296
Error: Missing value for '--name <name>'
295297
Usage: example [--verbose] --name <name> [<file>]
298+
See 'example --help' for more information.
296299
```
297300

298301
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"]
338341
% example --file --verbose file1.swift --file file2.swift
339342
Error: Missing value for '--file <file>'
340343
Usage: example [--file <file> ...] [--verbose]
344+
See 'example --help' for more information.
341345
```
342346

343347
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"]
388392
% example --verbose file1.swift file2.swift --other
389393
Error: Unexpected argument '--other'
390394
Usage: example [--verbose] [<files> ...]
395+
See 'example --help' for more information.
391396
```
392397

393398
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:

Documentation/03 Commands and Subcommands.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ SUBCOMMANDS:
2727
average Print the average of the values.
2828
stdev Print the standard deviation of the values.
2929
quantiles Print the quantiles of the values (TBD).
30+
31+
See 'math help stats <subcommand>' for detailed help.
3032
```
3133

3234
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.

Documentation/05 Validation and Errors.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,15 @@ When you provide useful error messages, they can guide new users to success with
4444
% select
4545
Error: Please provide at least one element to choose from.
4646
Usage: select [--count <count>] [<elements> ...]
47+
See 'select --help' for more information.
4748
% select --count 2 hello
4849
Error: Please specify a 'count' less than the number of elements.
4950
Usage: select [--count <count>] [<elements> ...]
51+
See 'select --help' for more information.
5052
% select --count 0 hello hey hi howdy
5153
Error: Please specify a 'count' of at least 1.
5254
Usage: select [--count <count>] [<elements> ...]
55+
See 'select --help' for more information.
5356
% select --count 2 hello hey hi howdy
5457
howdy
5558
hey
@@ -145,6 +148,7 @@ Throwing from a transform closure benefits users by providing context and can re
145148
% example '{"Bad JSON"}'
146149
Error: The value '{"Bad JSON"}' is invalid for '<input-json>': 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.})))
147150
Usage: example <input-json> --fail-option <fail-option>
151+
See 'select --help' for more information.
148152
```
149153

150154
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
153157
% example '{"tokenCount":0,"tokens":[],"identifier":"F77D661C-C5B7-448E-9344-267B284F66AD"}' --fail-option="Some Text Here!"
154158
Error: The value 'Some Text Here!' is invalid for '--fail-option <fail-option>': Trying to write to failOption always produces an error. Input: Some Text Here!
155159
Usage: example <input-json> --fail-option <fail-option>
160+
See 'select --help' for more information.
156161
```

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ hello
5454
$ repeat
5555
Error: Missing required value for argument 'phrase'.
5656
Usage: repeat [--count <count>] [--include-counter] <phrase>
57+
See 'repeat --help' for more information.
5758
$ repeat --help
5859
USAGE: repeat [--count <count>] [--include-counter] <phrase>
5960

Sources/ArgumentParser/Usage/HelpGenerator.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ internal struct HelpGenerator {
9696
var content: String
9797
}
9898

99+
var commandStack: [ParsableCommand.Type]
99100
var abstract: String
100101
var usage: Usage
101102
var sections: [Section]
@@ -107,7 +108,8 @@ internal struct HelpGenerator {
107108
}
108109

109110
let currentArgSet = ArgumentSet(currentCommand)
110-
111+
self.commandStack = commandStack
112+
111113
let toolName = commandStack.map { $0._commandName }.joined(separator: " ")
112114
var usageString = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis
113115
if !currentCommand.configuration.subcommands.isEmpty {
@@ -231,6 +233,12 @@ internal struct HelpGenerator {
231233
return "Usage: \(usage.rendered(screenWidth: screenWidth))"
232234
}
233235

236+
var includesSubcommands: Bool {
237+
guard let subcommandSection = sections.first(where: { $0.header == .subcommands })
238+
else { return false }
239+
return !subcommandSection.elements.isEmpty
240+
}
241+
234242
func rendered(screenWidth: Int? = nil) -> String {
235243
let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth
236244
let renderedSections = sections
@@ -241,11 +249,21 @@ internal struct HelpGenerator {
241249
? ""
242250
: "OVERVIEW: \(abstract)".wrapped(to: screenWidth) + "\n\n"
243251

252+
var helpSubcommandMessage: String = ""
253+
if includesSubcommands {
254+
var names = commandStack.map { $0._commandName }
255+
names.insert("help", at: 1)
256+
helpSubcommandMessage = """
257+
258+
See '\(names.joined(separator: " ")) <subcommand>' for detailed help.
259+
"""
260+
}
261+
244262
return """
245263
\(renderedAbstract)\
246264
USAGE: \(usage.rendered(screenWidth: screenWidth))
247265
248-
\(renderedSections)
266+
\(renderedSections)\(helpSubcommandMessage)
249267
"""
250268
}
251269
}

Sources/ArgumentParser/Usage/MessageInfo.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ enum MessageInfo {
5353
parserError = .userValidationError(error)
5454
}
5555

56+
let commandNames = commandStack.map { $0._commandName }.joined(separator: " ")
5657
let usage = HelpGenerator(commandStack: commandStack).usageMessage()
58+
+ "\n See '\(commandNames) --help' for more information."
5759

5860
// Parsing errors and user-thrown validation errors have the usage
5961
// string attached. Other errors just get the error message.

Tests/ArgumentParserEndToEndTests/SubcommandEndToEndTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ extension SubcommandEndToEndTests {
8181
a
8282
b
8383
84+
See 'foo help <subcommand>' for detailed help.
8485
""", helpFoo)
8586
AssertEqualStringsIgnoringTrailingWhitespace("""
8687
USAGE: foo a --name <name> --bar <bar>

Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ fileprivate struct FooOption: Convert, ParsableArguments {
3838

3939
static var usageString: String = """
4040
Usage: foo_option --string <int_str>
41+
See 'foo_option --help' for more information.
4142
"""
4243

4344
@Option(help: ArgumentHelp("Convert string to integer", valueName: "int_str"),
@@ -49,6 +50,7 @@ fileprivate struct BarOption: Convert, ParsableCommand {
4950

5051
static var usageString: String = """
5152
Usage: bar-option [--strings <int_str> ...]
53+
See 'bar-option --help' for more information.
5254
"""
5355

5456
@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 {
9799

98100
static var usageString: String = """
99101
Usage: foo_argument <int_str>
102+
See 'foo_argument --help' for more information.
100103
"""
101104

102105
enum FooError: Error {
@@ -112,6 +115,7 @@ fileprivate struct BarArgument: Convert, ParsableCommand {
112115

113116
static var usageString: String = """
114117
Usage: bar-argument [<int_str> ...]
118+
See 'bar-argument --help' for more information.
115119
"""
116120

117121
@Argument(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"),

Tests/ArgumentParserEndToEndTests/ValidationEndToEndTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ fileprivate enum UserValidationError: LocalizedError {
3030
fileprivate struct Foo: ParsableArguments {
3131
static var usageString: String = """
3232
Usage: foo [--count <count>] [<names> ...] [--version] [--throw]
33+
See 'foo --help' for more information.
3334
"""
3435

3536
static var helpString: String = """

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ final class MathExampleTests: XCTestCase {
3333
add Print the sum of the values.
3434
multiply Print the product of the values.
3535
stats Calculate descriptive statistics.
36+
37+
See 'math help <subcommand>' for detailed help.
3638
"""
3739

3840
AssertExecuteCommand(command: "math -h", expected: helpText)
@@ -109,6 +111,7 @@ final class MathExampleTests: XCTestCase {
109111
expected: """
110112
Error: Please provide at least one value to calculate the mode.
111113
Usage: math stats average [--kind <kind>] [<values> ...]
114+
See 'math stats average --help' for more information.
112115
""",
113116
exitCode: .validationFailure)
114117
}
@@ -150,6 +153,7 @@ final class MathExampleTests: XCTestCase {
150153
expected: """
151154
Error: Unknown option '--foo'
152155
Usage: math add [--hex-output] [<values> ...]
156+
See 'math add --help' for more information.
153157
""",
154158
exitCode: .validationFailure)
155159

@@ -158,6 +162,7 @@ final class MathExampleTests: XCTestCase {
158162
expected: """
159163
Error: The value 'ZZZ' is invalid for '<values>'
160164
Usage: math add [--hex-output] [<values> ...]
165+
See 'math add --help' for more information.
161166
""",
162167
exitCode: .validationFailure)
163168
}

0 commit comments

Comments
 (0)