Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Documentation/02 Arguments, Options, and Flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,11 @@ When called without both values, the command exits with an error:
% example 5
Error: Missing '--user-name <user-name>'
Usage: example --user-name <user-name> <value>
See 'example --help' for more information.
% example --user-name kjohnson
Error: Missing '<value>'
Usage: example --user-name <user-name> <value>
See 'example --help' for more information.
```

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

Parsing options as arrays is similar — only adjacent key-value pairs are recognized by default.
Expand Down Expand Up @@ -338,6 +341,7 @@ Verbose: true, files: ["file1.swift", "file2.swift"]
% example --file --verbose file1.swift --file file2.swift
Error: Missing value for '--file <file>'
Usage: example [--file <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:
Expand Down Expand Up @@ -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] [<files> ...]
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:
Expand Down
2 changes: 2 additions & 0 deletions Documentation/03 Commands and Subcommands.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <subcommand>' 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.
Expand Down
5 changes: 5 additions & 0 deletions Documentation/05 Validation and Errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <count>] [<elements> ...]
See 'select --help' for more information.
% select --count 2 hello
Error: Please specify a 'count' less than the number of elements.
Usage: select [--count <count>] [<elements> ...]
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 <count>] [<elements> ...]
See 'select --help' for more information.
% select --count 2 hello hey hi howdy
howdy
hey
Expand Down Expand Up @@ -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 '<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.})))
Usage: example <input-json> --fail-option <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.
Expand All @@ -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 <fail-option>': Trying to write to failOption always produces an error. Input: Some Text Here!
Usage: example <input-json> --fail-option <fail-option>
See 'select --help' for more information.
```
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ hello
$ repeat
Error: Missing required value for argument 'phrase'.
Usage: repeat [--count <count>] [--include-counter] <phrase>
See 'repeat --help' for more information.
$ repeat --help
USAGE: repeat [--count <count>] [--include-counter] <phrase>

Expand Down
22 changes: 20 additions & 2 deletions Sources/ArgumentParser/Usage/HelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ internal struct HelpGenerator {
var content: String
}

var commandStack: [ParsableCommand.Type]
var abstract: String
var usage: Usage
var sections: [Section]
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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: " ")) <subcommand>' for detailed help.
"""
}

return """
\(renderedAbstract)\
USAGE: \(usage.rendered(screenWidth: screenWidth))

\(renderedSections)
\(renderedSections)\(helpSubcommandMessage)
"""
}
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/ArgumentParser/Usage/MessageInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ extension SubcommandEndToEndTests {
a
b

See 'foo help <subcommand>' for detailed help.
""", helpFoo)
AssertEqualStringsIgnoringTrailingWhitespace("""
USAGE: foo a --name <name> --bar <bar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ fileprivate struct FooOption: Convert, ParsableArguments {

static var usageString: String = """
Usage: foo_option --string <int_str>
See 'foo_option --help' for more information.
"""

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

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

@Option(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"),
Expand Down Expand Up @@ -97,6 +99,7 @@ fileprivate struct FooArgument: Convert, ParsableArguments {

static var usageString: String = """
Usage: foo_argument <int_str>
See 'foo_argument --help' for more information.
"""

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

static var usageString: String = """
Usage: bar-argument [<int_str> ...]
See 'bar-argument --help' for more information.
"""

@Argument(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ 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.
"""

static var helpString: String = """
Expand Down
5 changes: 5 additions & 0 deletions Tests/ArgumentParserExampleTests/MathExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 <subcommand>' for detailed help.
"""

AssertExecuteCommand(command: "math -h", expected: helpText)
Expand Down Expand Up @@ -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 <kind>] [<values> ...]
See 'math stats average --help' for more information.
""",
exitCode: .validationFailure)
}
Expand Down Expand Up @@ -150,6 +153,7 @@ final class MathExampleTests: XCTestCase {
expected: """
Error: Unknown option '--foo'
Usage: math add [--hex-output] [<values> ...]
See 'math add --help' for more information.
""",
exitCode: .validationFailure)

Expand All @@ -158,6 +162,7 @@ final class MathExampleTests: XCTestCase {
expected: """
Error: The value 'ZZZ' is invalid for '<values>'
Usage: math add [--hex-output] [<values> ...]
See 'math add --help' for more information.
""",
exitCode: .validationFailure)
}
Expand Down
3 changes: 3 additions & 0 deletions Tests/ArgumentParserExampleTests/RepeatExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ final class RepeatExampleTests: XCTestCase {
expected: """
Error: Missing value for '--count <count>'
Usage: repeat [--count <count>] [--include-counter] <phrase>
See 'repeat --help' for more information.
""",
exitCode: .validationFailure)

Expand All @@ -73,6 +74,7 @@ final class RepeatExampleTests: XCTestCase {
expected: """
Error: The value 'ZZZ' is invalid for '--count <count>'
Usage: repeat [--count <count>] [--include-counter] <phrase>
See 'repeat --help' for more information.
""",
exitCode: .validationFailure)

Expand All @@ -81,6 +83,7 @@ final class RepeatExampleTests: XCTestCase {
expected: """
Error: Unknown option '--version'
Usage: repeat [--count <count>] [--include-counter] <phrase>
See 'repeat --help' for more information.
""",
exitCode: .validationFailure)
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ final class RollDiceExampleTests: XCTestCase {
expected: """
Error: Missing value for '--times <n>'
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
See 'roll --help' for more information.
""",
exitCode: .validationFailure)

Expand All @@ -49,6 +50,7 @@ final class RollDiceExampleTests: XCTestCase {
expected: """
Error: The value 'ZZZ' is invalid for '--times <n>'
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
See 'roll --help' for more information.
""",
exitCode: .validationFailure)
}
Expand Down
3 changes: 3 additions & 0 deletions Tests/ArgumentParserPackageManagerTests/HelpTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ extension HelpTests {
describe
generate-xcodeproj

See 'package help <subcommand>' for detailed help.
""".trimmingLines())
}

Expand All @@ -74,6 +75,7 @@ extension HelpTests {
describe
generate-xcodeproj

See 'package help <subcommand>' for detailed help.
""".trimmingLines()
)
}
Expand All @@ -100,6 +102,7 @@ extension HelpTests {
set-mirror
unset-mirror

See 'package help config <subcommand>' for detailed help.
""".trimmingLines())
}

Expand Down
1 change: 1 addition & 0 deletions Tests/ArgumentParserUnitTests/HelpGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ extension HelpGenerationTests {
Test long command name.
another-command

See 'h help <subcommand>' for detailed help.
""")

AssertHelp(for: H.AnotherCommand.self, root: H.self, equals: """
Expand Down