Skip to content

Commit 4793b0f

Browse files
authored
Include help text in error message when validation fails (#283)
1 parent 26744de commit 4793b0f

File tree

6 files changed

+62
-19
lines changed

6 files changed

+62
-19
lines changed

Sources/ArgumentParser/Usage/MessageInfo.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
enum MessageInfo {
1515
case help(text: String)
16-
case validation(message: String, usage: String)
16+
case validation(message: String, usage: String, help: String)
1717
case other(message: String, exitCode: Int32)
1818

1919
init(error: Error, type: ParsableArguments.Type) {
@@ -82,7 +82,7 @@ enum MessageInfo {
8282
if case .userValidationError(let error) = parserError {
8383
switch error {
8484
case let error as ValidationError:
85-
self = .validation(message: error.message, usage: usage)
85+
self = .validation(message: error.message, usage: usage, help: "")
8686
case let error as CleanExit:
8787
switch error {
8888
case .helpRequest(let command):
@@ -109,8 +109,10 @@ enum MessageInfo {
109109
guard case ParserError.noArguments = parserError else { return usage }
110110
return "\n" + HelpGenerator(commandStack: [type.asCommand]).rendered()
111111
}()
112-
let message = ArgumentSet(commandStack.last!).helpMessage(for: parserError)
113-
self = .validation(message: message, usage: usage)
112+
let argumentSet = ArgumentSet(commandStack.last!)
113+
let message = argumentSet.errorDescription(error: parserError) ?? ""
114+
let helpAbstract = argumentSet.helpDescription(error: parserError) ?? ""
115+
self = .validation(message: message, usage: usage, help: helpAbstract)
114116
} else {
115117
self = .other(message: String(describing: error), exitCode: EXIT_FAILURE)
116118
}
@@ -120,7 +122,7 @@ enum MessageInfo {
120122
switch self {
121123
case .help(text: let text):
122124
return text
123-
case .validation(message: let message, usage: _):
125+
case .validation(message: let message, usage: _, help: _):
124126
return message
125127
case .other(let message, _):
126128
return message
@@ -131,9 +133,10 @@ enum MessageInfo {
131133
switch self {
132134
case .help(text: let text):
133135
return text
134-
case .validation(message: let message, usage: let usage):
136+
case .validation(message: let message, usage: let usage, help: let help):
137+
let helpMessage = help.isEmpty ? "" : "Help: \(help)\n"
135138
let errorMessage = message.isEmpty ? "" : "\(args._errorLabel): \(message)\n"
136-
return errorMessage + usage
139+
return errorMessage + helpMessage + usage
137140
case .other(let message, _):
138141
return message.isEmpty ? "" : "\(args._errorLabel): \(message)"
139142
}

Sources/ArgumentParser/Usage/UsageGenerator.swift

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,6 @@ extension ArgumentDefinition {
142142
}
143143

144144
extension ArgumentSet {
145-
func helpMessage(for error: Swift.Error) -> String {
146-
return errorDescription(error: error) ?? ""
147-
}
148-
149145
/// Will generate a descriptive help message if possible.
150146
///
151147
/// If no descriptive help message can be generated, `nil` will be returned.
@@ -163,6 +159,19 @@ extension ArgumentSet {
163159
return nil
164160
}
165161
}
162+
163+
func helpDescription(error: Swift.Error) -> String? {
164+
switch error {
165+
case let parserError as ParserError:
166+
return ErrorMessageGenerator(arguments: self, error: parserError)
167+
.makeHelpMessage()
168+
case let commandError as CommandError:
169+
return ErrorMessageGenerator(arguments: self, error: commandError.parserError)
170+
.makeHelpMessage()
171+
default:
172+
return nil
173+
}
174+
}
166175
}
167176

168177
struct ErrorMessageGenerator {
@@ -223,6 +232,15 @@ extension ErrorMessageGenerator {
223232
}
224233
}
225234
}
235+
236+
func makeHelpMessage() -> String? {
237+
switch error {
238+
case .unableToParseValue(let o, let n, let v, forKey: let k, originalError: let e):
239+
return unableToParseHelpMessage(origin: o, name: n, value: v, key: k, error: e)
240+
default:
241+
return nil
242+
}
243+
}
226244
}
227245

228246
extension ErrorMessageGenerator {
@@ -363,6 +381,21 @@ extension ErrorMessageGenerator {
363381
}
364382
}
365383

384+
func unableToParseHelpMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String {
385+
guard let abstract = help(for: key)?.help?.abstract else { return "" }
386+
387+
let valueName = arguments(for: key).first?.valueName
388+
389+
switch (name, valueName) {
390+
case let (n?, v?):
391+
return "\(n.synopsisString) <\(v)> \(abstract)"
392+
case let (_, v?):
393+
return "<\(v)> \(abstract)"
394+
case (_, _):
395+
return ""
396+
}
397+
}
398+
366399
func unableToParseValueMessage(origin: InputOrigin, name: Name?, value: String, key: InputKey, error: Error?) -> String {
367400
let valueName = arguments(for: key).first?.valueName
368401

Tests/ArgumentParserEndToEndTests/TransformEndToEndTests.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ fileprivate struct FooOption: Convert, ParsableArguments {
4040
Usage: foo_option --string <int_str>
4141
See 'foo_option --help' for more information.
4242
"""
43+
static var help: String = "Help: --string <int_str> Convert string to integer\n"
4344

4445
@Option(help: ArgumentHelp("Convert string to integer", valueName: "int_str"),
4546
transform: { try convert($0) })
@@ -52,6 +53,7 @@ fileprivate struct BarOption: Convert, ParsableCommand {
5253
Usage: bar-option [--strings <int_str> ...]
5354
See 'bar-option --help' for more information.
5455
"""
56+
static var help: String = "Help: --strings <int_str> Convert a list of strings to an array of integers\n"
5557

5658
@Option(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"),
5759
transform: { try convert($0) })
@@ -69,11 +71,11 @@ extension TransformEndToEndTests {
6971
}
7072

7173
func testSingleOptionValidation_Fail_CustomErrorMessage() throws {
72-
AssertFullErrorMessage(FooOption.self, ["--string", "Forty Two"], "Error: The value 'Forty Two' is invalid for '--string <int_str>': Could not transform to an Int.\n" + FooOption.usageString)
74+
AssertFullErrorMessage(FooOption.self, ["--string", "Forty Two"], "Error: The value 'Forty Two' is invalid for '--string <int_str>': Could not transform to an Int.\n" + FooOption.help + FooOption.usageString)
7375
}
7476

7577
func testSingleOptionValidation_Fail_DefaultErrorMessage() throws {
76-
AssertFullErrorMessage(FooOption.self, ["--string", "4827"], "Error: The value '4827' is invalid for '--string <int_str>': outOfBounds\n" + FooOption.usageString)
78+
AssertFullErrorMessage(FooOption.self, ["--string", "4827"], "Error: The value '4827' is invalid for '--string <int_str>': outOfBounds\n" + FooOption.help + FooOption.usageString)
7779
}
7880

7981
// MARK: Arrays
@@ -85,11 +87,11 @@ extension TransformEndToEndTests {
8587
}
8688

8789
func testOptionArrayValidation_Fail_CustomErrorMessage() throws {
88-
AssertFullErrorMessage(BarOption.self, ["--strings", "Forty Two", "--strings", "72", "--strings", "99"], "Error: The value 'Forty Two' is invalid for '--strings <int_str>': Could not transform to an Int.\n" + BarOption.usageString)
90+
AssertFullErrorMessage(BarOption.self, ["--strings", "Forty Two", "--strings", "72", "--strings", "99"], "Error: The value 'Forty Two' is invalid for '--strings <int_str>': Could not transform to an Int.\n" + BarOption.help + BarOption.usageString)
8991
}
9092

9193
func testOptionArrayValidation_Fail_DefaultErrorMessage() throws {
92-
AssertFullErrorMessage(BarOption.self, ["--strings", "4827", "--strings", "72", "--strings", "99"], "Error: The value '4827' is invalid for '--strings <int_str>': outOfBounds\n" + BarOption.usageString)
94+
AssertFullErrorMessage(BarOption.self, ["--strings", "4827", "--strings", "72", "--strings", "99"], "Error: The value '4827' is invalid for '--strings <int_str>': outOfBounds\n" + BarOption.help + BarOption.usageString)
9395
}
9496
}
9597

@@ -101,6 +103,7 @@ fileprivate struct FooArgument: Convert, ParsableArguments {
101103
Usage: foo_argument <int_str>
102104
See 'foo_argument --help' for more information.
103105
"""
106+
static var help: String = "Help: <int_str> Convert string to integer\n"
104107

105108
enum FooError: Error {
106109
case outOfBounds
@@ -117,6 +120,7 @@ fileprivate struct BarArgument: Convert, ParsableCommand {
117120
Usage: bar-argument [<int_str> ...]
118121
See 'bar-argument --help' for more information.
119122
"""
123+
static var help: String = "Help: <int_str> Convert a list of strings to an array of integers\n"
120124

121125
@Argument(help: ArgumentHelp("Convert a list of strings to an array of integers", valueName: "int_str"),
122126
transform: { try convert($0) })
@@ -134,11 +138,11 @@ extension TransformEndToEndTests {
134138
}
135139

136140
func testArgumentValidation_Fail_CustomErrorMessage() throws {
137-
AssertFullErrorMessage(FooArgument.self, ["Forty Two"], "Error: The value 'Forty Two' is invalid for '<int_str>': Could not transform to an Int.\n" + FooArgument.usageString)
141+
AssertFullErrorMessage(FooArgument.self, ["Forty Two"], "Error: The value 'Forty Two' is invalid for '<int_str>': Could not transform to an Int.\n" + FooArgument.help + FooArgument.usageString)
138142
}
139143

140144
func testArgumentValidation_Fail_DefaultErrorMessage() throws {
141-
AssertFullErrorMessage(FooArgument.self, ["4827"], "Error: The value '4827' is invalid for '<int_str>': outOfBounds\n" + FooArgument.usageString)
145+
AssertFullErrorMessage(FooArgument.self, ["4827"], "Error: The value '4827' is invalid for '<int_str>': outOfBounds\n" + FooArgument.help + FooArgument.usageString)
142146
}
143147

144148
// MARK: Arrays
@@ -150,10 +154,10 @@ extension TransformEndToEndTests {
150154
}
151155

152156
func testArgumentArrayValidation_Fail_CustomErrorMessage() throws {
153-
AssertFullErrorMessage(BarArgument.self, ["Forty Two", "72", "99"], "Error: The value 'Forty Two' is invalid for '<int_str>': Could not transform to an Int.\n" + BarArgument.usageString)
157+
AssertFullErrorMessage(BarArgument.self, ["Forty Two", "72", "99"], "Error: The value 'Forty Two' is invalid for '<int_str>': Could not transform to an Int.\n" + BarArgument.help + BarArgument.usageString)
154158
}
155159

156160
func testArgumentArrayValidation_Fail_DefaultErrorMessage() throws {
157-
AssertFullErrorMessage(BarArgument.self, ["4827", "72", "99"], "Error: The value '4827' is invalid for '<int_str>': outOfBounds\n" + BarArgument.usageString)
161+
AssertFullErrorMessage(BarArgument.self, ["4827", "72", "99"], "Error: The value '4827' is invalid for '<int_str>': outOfBounds\n" + BarArgument.help + BarArgument.usageString)
158162
}
159163
}

Tests/ArgumentParserExampleTests/MathExampleTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ final class MathExampleTests: XCTestCase {
167167
command: "math ZZZ",
168168
expected: """
169169
Error: The value 'ZZZ' is invalid for '<values>'
170+
Help: <values> A group of integers to operate on.
170171
Usage: math add [--hex-output] [<values> ...]
171172
See 'math add --help' for more information.
172173
""",

Tests/ArgumentParserExampleTests/RepeatExampleTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ final class RepeatExampleTests: XCTestCase {
7373
command: "repeat hello --count ZZZ",
7474
expected: """
7575
Error: The value 'ZZZ' is invalid for '--count <count>'
76+
Help: --count <count> The number of times to repeat 'phrase'.
7677
Usage: repeat [--count <count>] [--include-counter] <phrase>
7778
See 'repeat --help' for more information.
7879
""",

Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ final class RollDiceExampleTests: XCTestCase {
4949
command: "roll --times ZZZ",
5050
expected: """
5151
Error: The value 'ZZZ' is invalid for '--times <n>'
52+
Help: --times <n> Rolls the dice <n> times.
5253
Usage: roll [--times <n>] [--sides <m>] [--seed <seed>] [--verbose]
5354
See 'roll --help' for more information.
5455
""",

0 commit comments

Comments
 (0)