Skip to content

Commit 56d4248

Browse files
authored
Fix confusing error message w/ single-dash option (#728)
When a required option has a short name, longer single-dash arguments can end up incorrectly colliding and causing confusing error messages. This changes the error handling when a short-name option is missing a value to show both the missing-value error _and_ a message about the unrecognized longer/composite option. Fixes #709.
1 parent 5b1320e commit 56d4248

File tree

5 files changed

+91
-5
lines changed

5 files changed

+91
-5
lines changed

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,25 @@ struct LenientParser {
263263
command.configuration.subcommands
264264
}
265265

266+
func errorForMissingValue(
267+
_ originElement: InputOrigin.Element,
268+
_ parsed: ParsedArgument
269+
) -> ParserError {
270+
if case .argumentIndex(let index) = originElement,
271+
index.subIndex != .complete,
272+
let originalInput =
273+
inputArguments
274+
.originalInput(at: .argumentIndex(index.completeIndex))
275+
{
276+
let completeName = Name(originalInput[...])
277+
return ParserError.missingValueOrUnknownCompositeOption(
278+
InputOrigin(element: originElement), parsed.name, completeName)
279+
} else {
280+
return ParserError.missingValueForOption(
281+
InputOrigin(element: originElement), parsed.name)
282+
}
283+
}
284+
266285
mutating func parseValue(
267286
_ argument: ArgumentDefinition,
268287
_ parsed: ParsedArgument,
@@ -296,7 +315,7 @@ struct LenientParser {
296315
try update(origins, parsed.name, value, &result)
297316
usedOrigins.formUnion(origins)
298317
} else {
299-
throw ParserError.missingValueForOption(origin, parsed.name)
318+
throw errorForMissingValue(originElement, parsed)
300319
}
301320

302321
case .scanningForValue:
@@ -322,7 +341,7 @@ struct LenientParser {
322341
try update(origins, parsed.name, value, &result)
323342
usedOrigins.formUnion(origins)
324343
} else {
325-
throw ParserError.missingValueForOption(origin, parsed.name)
344+
throw errorForMissingValue(originElement, parsed)
326345
}
327346

328347
case .unconditional:
@@ -344,7 +363,7 @@ struct LenientParser {
344363
let (origin2, value) = inputArguments.popNextElementAsValue(
345364
after: originElement)
346365
else {
347-
throw ParserError.missingValueForOption(origin, parsed.name)
366+
throw errorForMissingValue(originElement, parsed)
348367
}
349368
let origins = origin.inserting(origin2)
350369
try update(origins, parsed.name, value, &result)
@@ -415,7 +434,7 @@ struct LenientParser {
415434
if foundAttachedValue {
416435
break
417436
} else {
418-
throw ParserError.missingValueForOption(origin, parsed.name)
437+
throw errorForMissingValue(originElement, parsed)
419438
}
420439
}
421440

Sources/ArgumentParser/Parsing/InputOrigin.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ struct InputOrigin: Equatable, ExpressibleByArrayLiteral {
6363
Array(_elements).sorted()
6464
}
6565

66-
init() {
66+
var firstElement: Element {
67+
guard !elements.isEmpty else {
68+
fatalError("Invalid 'InputOrigin' with no positions")
69+
}
70+
return elements[0]
6771
}
6872

6973
init(elements: [Element]) {

Sources/ArgumentParser/Parsing/ParserError.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ enum ParserError: Error {
2626
case nonAlphanumericShortOption(Character)
2727
/// The option was there, but its value is missing, e.g. `--name` but no value for the `name`.
2828
case missingValueForOption(InputOrigin, Name)
29+
case missingValueOrUnknownCompositeOption(InputOrigin, Name, Name)
2930
case unexpectedValueForOption(InputOrigin.Element, Name, String)
3031
case unexpectedExtraValues([(InputOrigin, String)])
3132
case duplicateExclusiveValues(

Sources/ArgumentParser/Usage/UsageGenerator.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ extension ErrorMessageGenerator {
198198
return unknownOptionMessage(origin: o, name: n)
199199
case .missingValueForOption(let o, let n):
200200
return missingValueForOptionMessage(origin: o, name: n)
201+
case .missingValueOrUnknownCompositeOption(
202+
let o, let shortName, let compositeName):
203+
return missingValueOrUnknownCompositeOptionMessage(
204+
origin: o, shortName: shortName, compositeName: compositeName)
201205
case .unexpectedValueForOption(let o, let n, let v):
202206
return unexpectedValueForOptionMessage(origin: o, name: n, value: v)
203207
case .unexpectedExtraValues(let v):
@@ -340,6 +344,23 @@ extension ErrorMessageGenerator {
340344
}
341345
}
342346

347+
func missingValueOrUnknownCompositeOptionMessage(
348+
origin: InputOrigin,
349+
shortName: Name,
350+
compositeName: Name
351+
) -> String {
352+
let unknownOptionMessage = unknownOptionMessage(
353+
origin: origin.firstElement,
354+
name: compositeName)
355+
let missingValueMessage = missingValueForOptionMessage(
356+
origin: origin,
357+
name: shortName)
358+
return """
359+
\(unknownOptionMessage)
360+
or: \(missingValueMessage) in '\(compositeName.synopsisString)'
361+
"""
362+
}
363+
343364
func unexpectedValueForOptionMessage(
344365
origin: InputOrigin.Element, name: Name, value: String
345366
) -> String? {

Tests/ArgumentParserUnitTests/ParsableArgumentsValidationTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,44 @@ final class ParsableArgumentsValidationTests: XCTestCase {
535535
}
536536
}
537537
}
538+
539+
extension ParsableArgumentsValidationTests {
540+
func testMissingValueForShortNameOptions() throws {
541+
struct SomeArgs: ParsableArguments {
542+
@Option(name: .shortAndLong)
543+
var xArg: Int
544+
@Option(name: .shortAndLong)
545+
var zArg: Int
546+
@Option(name: .customLong("long-with-x-or-y", withSingleDash: true))
547+
var other: Int?
548+
}
549+
550+
AssertErrorMessage(
551+
SomeArgs.self,
552+
["-long_option_with_x_or_z"],
553+
"""
554+
Unknown option '-long_option_with_x_or_z'
555+
or: Missing value for '-x <x-arg>' in '-long_option_with_x_or_z'
556+
"""
557+
)
558+
// Including near-miss checking.
559+
AssertErrorMessage(
560+
SomeArgs.self,
561+
["-long-with-x-or-z"],
562+
"""
563+
Unknown option '-long-with-x-or-z'. Did you mean '-long-with-x-or-y'?
564+
or: Missing value for '-x <x-arg>' in '-long-with-x-or-z'
565+
"""
566+
)
567+
// Missing value for whole option.
568+
AssertErrorMessage(
569+
SomeArgs.self, ["-x", "-z", "2"],
570+
"Missing value for '-x <x-arg>'"
571+
)
572+
// Standalone unexpected option.
573+
AssertErrorMessage(
574+
SomeArgs.self, ["-x", "1", "-z", "2", "-q"],
575+
"Unknown option '-q'"
576+
)
577+
}
578+
}

0 commit comments

Comments
 (0)