Skip to content

Commit 44fb59e

Browse files
authored
Add APIs for generating the help screen (#142)
* Add public API for generating help text * Add tests for `helpMessage()` * Update documentation * Use new rendered method instead of property
1 parent afeb200 commit 44fb59e

File tree

8 files changed

+84
-23
lines changed

8 files changed

+84
-23
lines changed

Documentation/04 Customizing Help.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,17 @@ struct Example: ParsableCommand {
165165
var experimentalEnableWidgets: Bool
166166
}
167167
```
168+
169+
## Generating Help Text Programmatically
170+
171+
The help screen is automatically shown to users when they call your command with the help flag. You can generate the same text from within your program by calling the `helpMessage()` method.
172+
173+
```swift
174+
let help = Repeat.helpMessage()
175+
// `help` matches the output above
176+
177+
let fortyColumnHelp = Repeat.helpMessage(columns: 40)
178+
// `fortyColumnHelp` is the same help screen, but wrapped to 40 columns
179+
```
180+
181+
When generating help text for a subcommand, call `helpMessage(for:)` on the `ParsableCommand` type that represents the root of the command tree and pass the subcommand type as a parameter to ensure the correct display.

Sources/ArgumentParser/Parsable Types/ParsableArguments.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ extension ParsableArguments {
125125
MessageInfo(error: error, type: self).fullText
126126
}
127127

128+
/// Returns the text of the help screen for this type.
129+
///
130+
/// - Parameter columns: The column width to use when wrapping long lines in
131+
/// the help screen. If `columns` is `nil`, uses the current terminal width,
132+
/// or a default value of `80` if the terminal width is not available.
133+
/// - Returns: The full help screen for this type.
134+
public static func helpMessage(columns: Int? = nil) -> String {
135+
HelpGenerator(self).rendered(screenWidth: columns)
136+
}
137+
128138
/// Returns the exit code for the given error.
129139
///
130140
/// The returned code is the same exit code that is used if `error` is passed

Sources/ArgumentParser/Parsable Types/ParsableCommand.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ extension ParsableCommand {
6464
return try parser.parse(arguments: arguments).get()
6565
}
6666

67+
/// Returns the text of the help screen for the given subcommand of this
68+
/// command.
69+
///
70+
/// - Parameters:
71+
/// - subcommand: The subcommand to generate the help screen for.
72+
/// `subcommand` must be declared in the subcommand tree of this
73+
/// command.
74+
/// - columns: The column width to use when wrapping long line in the
75+
/// help screen. If `columns` is `nil`, uses the current terminal
76+
/// width, or a default value of `80` if the terminal width is not
77+
/// available.
78+
public static func helpMessage(
79+
for subcommand: ParsableCommand.Type,
80+
columns: Int? = nil
81+
) -> String {
82+
let stack = CommandParser(self).commandStack(for: subcommand)
83+
return HelpGenerator(commandStack: stack).rendered(screenWidth: columns)
84+
}
85+
6786
/// Parses an instance of this type, or one of its subcommands, from
6887
/// command-line arguments and calls its `run()` method, exiting cleanly
6988
/// or with a relevant error message.

Sources/ArgumentParser/Usage/HelpCommand.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct HelpCommand: ParsableCommand {
2727
}
2828

2929
func generateHelp() -> String {
30-
return HelpGenerator(commandStack: commandStack).rendered
30+
return HelpGenerator(commandStack: commandStack).rendered()
3131
}
3232

3333
enum CodingKeys: CodingKey {

Sources/ArgumentParser/Usage/HelpGenerator.swift

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
internal struct HelpGenerator {
1313
static var helpIndent = 2
1414
static var labelColumnWidth = 26
15-
static var screenWidth: Int {
15+
static var systemScreenWidth: Int {
1616
_screenWidthOverride ?? _terminalSize().width
1717
}
1818

@@ -21,7 +21,7 @@ internal struct HelpGenerator {
2121
struct Usage {
2222
var components: [String]
2323

24-
var rendered: String {
24+
func rendered(screenWidth: Int) -> String {
2525
components
2626
.joined(separator: "\n")
2727
}
@@ -37,13 +37,13 @@ internal struct HelpGenerator {
3737
String(repeating: " ", count: HelpGenerator.helpIndent) + label
3838
}
3939

40-
var rendered: String {
40+
func rendered(screenWidth: Int) -> String {
4141
let paddedLabel = self.paddedLabel
4242
let wrappedAbstract = self.abstract
43-
.wrapped(to: HelpGenerator.screenWidth, wrappingIndent: HelpGenerator.labelColumnWidth)
43+
.wrapped(to: screenWidth, wrappingIndent: HelpGenerator.labelColumnWidth)
4444
let wrappedDiscussion = self.discussion.isEmpty
4545
? ""
46-
: self.discussion.wrapped(to: HelpGenerator.screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) + "\n"
46+
: self.discussion.wrapped(to: screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) + "\n"
4747
let renderedAbstract: String = {
4848
guard !abstract.isEmpty else { return "" }
4949
if paddedLabel.count < HelpGenerator.labelColumnWidth {
@@ -82,10 +82,10 @@ internal struct HelpGenerator {
8282
var discussion: String = ""
8383
var isSubcommands: Bool = false
8484

85-
var rendered: String {
85+
func rendered(screenWidth: Int) -> String {
8686
guard !elements.isEmpty else { return "" }
8787

88-
let renderedElements = elements.map { $0.rendered }.joined()
88+
let renderedElements = elements.map { $0.rendered(screenWidth: screenWidth) }.joined()
8989
return "\(String(describing: header).uppercased()):\n"
9090
+ renderedElements
9191
}
@@ -125,6 +125,10 @@ internal struct HelpGenerator {
125125
self.discussionSections = []
126126
}
127127

128+
init(_ type: ParsableArguments.Type) {
129+
self.init(commandStack: [type.asCommand])
130+
}
131+
128132
static func generateSections(commandStack: [ParsableCommand.Type]) -> [Section] {
129133
var positionalElements: [Section.Element] = []
130134
var optionElements: [Section.Element] = []
@@ -219,22 +223,24 @@ internal struct HelpGenerator {
219223
]
220224
}
221225

222-
var usageMessage: String {
223-
"Usage: \(usage.rendered)"
226+
func usageMessage(screenWidth: Int? = nil) -> String {
227+
let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth
228+
return "Usage: \(usage.rendered(screenWidth: screenWidth))"
224229
}
225230

226-
var rendered: String {
231+
func rendered(screenWidth: Int? = nil) -> String {
232+
let screenWidth = screenWidth ?? HelpGenerator.systemScreenWidth
227233
let renderedSections = sections
228-
.map { $0.rendered }
234+
.map { $0.rendered(screenWidth: screenWidth) }
229235
.filter { !$0.isEmpty }
230236
.joined(separator: "\n")
231237
let renderedAbstract = abstract.isEmpty
232238
? ""
233-
: "OVERVIEW: \(abstract)".wrapped(to: HelpGenerator.screenWidth) + "\n\n"
239+
: "OVERVIEW: \(abstract)".wrapped(to: screenWidth) + "\n\n"
234240

235241
return """
236242
\(renderedAbstract)\
237-
USAGE: \(usage.rendered)
243+
USAGE: \(usage.rendered(screenWidth: screenWidth))
238244
239245
\(renderedSections)
240246
"""

Sources/ArgumentParser/Usage/MessageInfo.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ enum MessageInfo {
2727

2828
switch e.parserError {
2929
case .helpRequested:
30-
self = .help(text: HelpGenerator(commandStack: e.commandStack).rendered)
30+
self = .help(text: HelpGenerator(commandStack: e.commandStack).rendered())
3131
return
3232
case .versionRequested:
3333
let versionString = commandStack
@@ -43,7 +43,7 @@ enum MessageInfo {
4343
commandStack = [type.asCommand]
4444
parserError = e
4545
if case .helpRequested = e {
46-
self = .help(text: HelpGenerator(commandStack: [type.asCommand]).rendered)
46+
self = .help(text: HelpGenerator(commandStack: [type.asCommand]).rendered())
4747
return
4848
}
4949
default:
@@ -53,7 +53,7 @@ enum MessageInfo {
5353
parserError = .userValidationError(error)
5454
}
5555

56-
let usage = HelpGenerator(commandStack: commandStack).usageMessage
56+
let usage = HelpGenerator(commandStack: commandStack).usageMessage()
5757

5858
// Parsing errors and user-thrown validation errors have the usage
5959
// string attached. Other errors just get the error message.
@@ -68,7 +68,7 @@ enum MessageInfo {
6868
if let command = command {
6969
commandStack = CommandParser(type.asCommand).commandStack(for: command)
7070
}
71-
self = .help(text: HelpGenerator(commandStack: commandStack).rendered)
71+
self = .help(text: HelpGenerator(commandStack: commandStack).rendered())
7272
case .message(let message):
7373
self = .help(text: message)
7474
}
@@ -82,7 +82,7 @@ enum MessageInfo {
8282
} else if let parserError = parserError {
8383
let usage: String = {
8484
guard case ParserError.noArguments = parserError else { return usage }
85-
return "\n" + HelpGenerator(commandStack: [type.asCommand]).rendered
85+
return "\n" + HelpGenerator(commandStack: [type.asCommand]).rendered()
8686
}()
8787
let message = ArgumentSet(commandStack.last!).helpMessage(for: parserError)
8888
self = .validation(message: message, usage: usage)

Sources/ArgumentParserTestHelpers/TestHelpers.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,25 @@ public func AssertHelp<T: ParsableArguments>(
117117
) {
118118
do {
119119
_ = try T.parse(["-h"])
120-
XCTFail()
120+
XCTFail(file: file, line: line)
121121
} catch {
122122
let helpString = T.fullMessage(for: error)
123123
AssertEqualStringsIgnoringTrailingWhitespace(
124124
helpString, expected, file: file, line: line)
125125
}
126+
127+
let helpString = T.helpMessage()
128+
AssertEqualStringsIgnoringTrailingWhitespace(
129+
helpString, expected, file: file, line: line)
130+
}
131+
132+
public func AssertHelp<T: ParsableCommand, U: ParsableCommand>(
133+
for _: T.Type, root _: U.Type, equals expected: String,
134+
file: StaticString = #file, line: UInt = #line
135+
) {
136+
let helpString = U.helpMessage(for: T.self)
137+
AssertEqualStringsIgnoringTrailingWhitespace(
138+
helpString, expected, file: file, line: line)
126139
}
127140

128141
extension XCTest {

Tests/ArgumentParserUnitTests/HelpGenerationTests.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ extension HelpGenerationTests {
132132
}
133133
}
134134

135-
136135
struct D: ParsableCommand {
137136
@Argument(default: "--", help: "Your occupation.")
138137
var occupation: String
@@ -267,8 +266,8 @@ extension HelpGenerationTests {
267266
268267
""")
269268

270-
AssertHelp(for: H.AnotherCommand.self, equals: """
271-
USAGE: another-command [--some-option-with-very-long-name <some-option-with-very-long-name>] [--option <option>] [<argument-with-very-long-name-and-help>] [<argument-with-very-long-name>] [<argument>]
269+
AssertHelp(for: H.AnotherCommand.self, root: H.self, equals: """
270+
USAGE: h another-command [--some-option-with-very-long-name <some-option-with-very-long-name>] [--option <option>] [<argument-with-very-long-name-and-help>] [<argument-with-very-long-name>] [<argument>]
272271
273272
ARGUMENTS:
274273
<argument-with-very-long-name-and-help>

0 commit comments

Comments
 (0)