Skip to content

Add customization point for command usage text #400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 11, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,30 @@ OPTIONS:

## Customizing Help for Commands

In addition to configuring the command name and subcommands, as described in <doc:CommandsAndSubcommands>, you can also configure a command's help text by providing an abstract and discussion.
In addition to configuring the command name and subcommands, as described in <doc:CommandsAndSubcommands>, you can also configure a command's help text by providing an abstract, discussion, or custom usage string.

```swift
struct Repeat: ParsableCommand {
static var configuration = CommandConfiguration(
abstract: "Repeats your input phrase.",
usage: """
repeat <phrase>
repeat --count <count> <phrase>
""",
discussion: """
Prints to stdout forever, or until you halt the program.
""")

@Argument(help: "The phrase to repeat.")
var phrase: String

@Option(help: "How many times to repeat.")
var count: Int?

mutating func run() throws {
while true { print(phrase) }
for _ in 0..<(count ?? Int.max) {
print(phrase)
}
}
}
```
Expand All @@ -104,6 +113,7 @@ OVERVIEW: Repeats your input phrase.
Prints to stdout forever, or until you halt the program.

USAGE: repeat <phrase>
repeat --count <count> <phrase>

ARGUMENTS:
<phrase> The phrase to repeat.
Expand Down
43 changes: 42 additions & 1 deletion Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ public struct CommandConfiguration {
/// A one-line description of this command.
public var abstract: String

/// A customized usage string to be shown in the help display and error
/// messages.
///
/// If `usage` is `nil`, the help display and errors show the autogenerated
/// usage string. To hide the usage string entirely, set `usage` to the empty
/// string.
public var usage: String?

/// A longer description of this command, to be shown in the extended help
/// display.
public var discussion: String
Expand Down Expand Up @@ -54,6 +62,10 @@ public struct CommandConfiguration {
/// `commandName` is `nil`, the command name is derived by converting
/// the name of the command type to hyphen-separated lowercase words.
/// - abstract: A one-line description of the command.
/// - usage: A custom usage description for the command. When you provide
/// a non-`nil` string, the argument parser uses `usage` instead of
/// automatically generating a usage description. Passing an empty string
/// hides the usage string altogether.
/// - discussion: A longer description of the command.
/// - version: The version number for this command. When you provide a
/// non-empty string, the argument parser prints it if the user provides
Expand All @@ -67,10 +79,11 @@ public struct CommandConfiguration {
/// - helpNames: The flag names to use for requesting help, when combined
/// with a simulated Boolean property named `help`. If `helpNames` is
/// `nil`, the names are inherited from the parent command, if any, or
/// `-h` and `--help`.
/// are `-h` and `--help`.
public init(
commandName: String? = nil,
abstract: String = "",
usage: String? = nil,
discussion: String = "",
version: String = "",
shouldDisplay: Bool = true,
Expand All @@ -80,6 +93,7 @@ public struct CommandConfiguration {
) {
self.commandName = commandName
self.abstract = abstract
self.usage = usage
self.discussion = discussion
self.version = version
self.shouldDisplay = shouldDisplay
Expand All @@ -94,6 +108,7 @@ public struct CommandConfiguration {
commandName: String? = nil,
_superCommandName: String,
abstract: String = "",
usage: String? = nil,
discussion: String = "",
version: String = "",
shouldDisplay: Bool = true,
Expand All @@ -104,6 +119,7 @@ public struct CommandConfiguration {
self.commandName = commandName
self._superCommandName = _superCommandName
self.abstract = abstract
self.usage = usage
self.discussion = discussion
self.version = version
self.shouldDisplay = shouldDisplay
Expand All @@ -112,3 +128,28 @@ public struct CommandConfiguration {
self.helpNames = helpNames
}
}

extension CommandConfiguration {
@available(*, deprecated, message: "Use the memberwise initializer with the usage parameter.")
public init(
commandName: String?,
abstract: String,
discussion: String,
version: String,
shouldDisplay: Bool,
subcommands: [ParsableCommand.Type],
defaultSubcommand: ParsableCommand.Type?,
helpNames: NameSpecification?
) {
self.init(
commandName: commandName,
abstract: abstract,
usage: "",
discussion: discussion,
version: version,
shouldDisplay: shouldDisplay,
subcommands: subcommands,
defaultSubcommand: defaultSubcommand,
helpNames: helpNames)
}
}
24 changes: 16 additions & 8 deletions Sources/ArgumentParser/Usage/HelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,15 @@ internal struct HelpGenerator {
toolName = "\(superName) \(toolName)"
}

var usage = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis
if !currentCommand.configuration.subcommands.isEmpty {
if usage.last != " " { usage += " " }
usage += "<subcommand>"
if let usage = currentCommand.configuration.usage {
self.usage = usage
} else {
var usage = UsageGenerator(toolName: toolName, definition: [currentArgSet]).synopsis
if !currentCommand.configuration.subcommands.isEmpty {
if usage.last != " " { usage += " " }
usage += "<subcommand>"
}
self.usage = usage
}

self.abstract = currentCommand.configuration.abstract
Expand All @@ -117,7 +122,6 @@ internal struct HelpGenerator {
self.abstract += "\n\(currentCommand.configuration.discussion)"
}

self.usage = usage
self.sections = HelpGenerator.generateSections(commandStack: commandStack)
self.discussionSections = []
}
Expand Down Expand Up @@ -210,7 +214,8 @@ internal struct HelpGenerator {
}

func usageMessage() -> String {
return "Usage: \(usage)"
guard !usage.isEmpty else { return "" }
return "Usage: \(usage.hangingIndentingEachLine(by: 7))"
}

var includesSubcommands: Bool {
Expand Down Expand Up @@ -243,10 +248,13 @@ internal struct HelpGenerator {
"""
}

let renderedUsage = usage.isEmpty
? ""
: "USAGE: \(usage.hangingIndentingEachLine(by: 7))\n\n"

return """
\(renderedAbstract)\
USAGE: \(usage)

\(renderedUsage)\
\(renderedSections)\(helpSubcommandMessage)
"""
}
Expand Down
5 changes: 4 additions & 1 deletion Sources/ArgumentParser/Usage/MessageInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ enum MessageInfo {

let commandNames = commandStack.map { $0._commandName }.joined(separator: " ")
if let helpName = commandStack.getPrimaryHelpName() {
usage += "\n See '\(commandNames) \(helpName.synopsisString)' for more information."
if !usage.isEmpty {
usage += "\n"
}
usage += " See '\(commandNames) \(helpName.synopsisString)' for more information."
}

// Parsing errors and user-thrown validation errors have the usage
Expand Down
27 changes: 16 additions & 11 deletions Sources/ArgumentParser/Utilities/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//
//===----------------------------------------------------------------------===//

extension String {
extension StringProtocol where SubSequence == Substring {
func wrapped(to columns: Int, wrappingIndent: Int = 0) -> String {
let columns = columns - wrappingIndent
guard columns > 0 else {
Expand Down Expand Up @@ -96,7 +96,7 @@ extension String {
/// "myURLProperty".convertedToSnakeCase(separator: "-")
/// // my-url-property
func convertedToSnakeCase(separator: Character = "_") -> String {
guard !isEmpty else { return self }
guard !isEmpty else { return "" }
var result = ""
// Whether we should append a separator when we see a uppercase character.
var separateOnUppercase = true
Expand Down Expand Up @@ -138,7 +138,7 @@ extension String {
let columns = target.count

if rows <= 0 || columns <= 0 {
return max(rows, columns)
return Swift.max(rows, columns)
}

var matrix = Array(repeating: Array(repeating: 0, count: columns + 1), count: rows + 1)
Expand Down Expand Up @@ -168,14 +168,19 @@ extension String {
}

func indentingEachLine(by n: Int) -> String {
let hasTrailingNewline = self.last == "\n"
let lines = self.split(separator: "\n", omittingEmptySubsequences: false)
if hasTrailingNewline && lines.last == "" {
return lines.dropLast().map { String(repeating: " ", count: n) + $0 }
.joined(separator: "\n") + "\n"
} else {
return lines.map { String(repeating: " ", count: n) + $0 }
.joined(separator: "\n")
}
let spacer = String(repeating: " ", count: n)
return lines.map {
$0.isEmpty ? $0 : spacer + $0
}.joined(separator: "\n")
}

func hangingIndentingEachLine(by n: Int) -> String {
let lines = self.split(
separator: "\n",
maxSplits: 1,
omittingEmptySubsequences: false)
guard lines.count == 2 else { return lines.joined(separator: "") }
return "\(lines[0])\n\(lines[1].indentingEachLine(by: n))"
}
}
2 changes: 1 addition & 1 deletion Tests/ArgumentParserUnitTests/CompletionScriptTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ _base() {
fi
case $prev in
--name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


return
;;
--kind)
Expand Down
91 changes: 90 additions & 1 deletion Tests/ArgumentParserUnitTests/HelpGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,6 @@ extension HelpGenerationTests {
}

func testIssue278() {
print(ParserBug.helpMessage(for: ParserBug.Sub.self))
AssertHelp(for: ParserBug.Sub.self, root: ParserBug.self, equals: """
USAGE: parserBug sub [--example] [<argument>]

Expand All @@ -608,4 +607,94 @@ extension HelpGenerationTests {

""")
}

struct CustomUsageShort: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(usage: """
example [--verbose] <file-name>
""")
}

@Argument var file: String
@Flag var verboseMode = false
}

struct CustomUsageLong: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(usage: """
example <file-name>
example --verbose <file-name>
example --help
""")
}

@Argument var file: String
@Flag var verboseMode = false
}

struct CustomUsageHidden: ParsableCommand {
static var configuration: CommandConfiguration {
CommandConfiguration(usage: "")
}

@Argument var file: String
@Flag var verboseMode = false
}

func testCustomUsageHelp() {
XCTAssertEqual(CustomUsageShort.helpMessage(columns: 80), """
USAGE: example [--verbose] <file-name>

ARGUMENTS:
<file>

OPTIONS:
--verbose-mode
-h, --help Show help information.

""")

XCTAssertEqual(CustomUsageLong.helpMessage(columns: 80), """
USAGE: example <file-name>
example --verbose <file-name>
example --help

ARGUMENTS:
<file>

OPTIONS:
--verbose-mode
-h, --help Show help information.

""")

XCTAssertEqual(CustomUsageHidden.helpMessage(columns: 80), """
ARGUMENTS:
<file>

OPTIONS:
--verbose-mode
-h, --help Show help information.

""")
}

func testCustomUsageError() {
XCTAssertEqual(CustomUsageShort.fullMessage(for: ValidationError("Test")), """
Error: Test
Usage: example [--verbose] <file-name>
See 'custom-usage-short --help' for more information.
""")
XCTAssertEqual(CustomUsageLong.fullMessage(for: ValidationError("Test")), """
Error: Test
Usage: example <file-name>
example --verbose <file-name>
example --help
See 'custom-usage-long --help' for more information.
""")
XCTAssertEqual(CustomUsageHidden.fullMessage(for: ValidationError("Test")), """
Error: Test
See 'custom-usage-hidden --help' for more information.
""")
}
}
Loading