diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index c1ebb4e5c..9626776b2 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -395,10 +395,12 @@ struct LenientParser { try argument.initial(origin, &result) // Use an attached value if it exists... + var foundAttachedValue = false if let value = parsed.value { // This was `--foo=bar` style: try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) + foundAttachedValue = true } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement( at: originElement) @@ -408,6 +410,21 @@ struct LenientParser { try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) inputArguments.removeAll(in: usedOrigins) + foundAttachedValue = true + } + + // Fix incorrect error message + // for @Option array without values + guard + inputArguments.peekNextElement(after: originElement) != nil + else { + // No independent values to be found, which is an error if there was + // no `--foo=bar`-style value already found. + if foundAttachedValue { + break + } else { + throw errorForMissingValue(originElement, parsed) + } } // ...and then consume the rest of the arguments diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index bb62b574c..5e5917c0d 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -489,6 +489,22 @@ extension SplitArguments { return (e.index, e.value.valueString!) } + func peekNextElement(after origin: InputOrigin.Element) -> ( + InputOrigin.Element, Element + )? { + guard let start = position(after: origin) else { return nil } + // Elements are sorted by their `InputIndex`. Find the first `InputIndex` + // after `origin`: + guard + let elementIndex = elements[start...].firstIndex(where: { + $0.index.subIndex == .complete + }) + else { return nil } + + let matchedArgument = elements[elementIndex] + return (.argumentIndex(matchedArgument.index), matchedArgument) + } + /// Removes the first element in `elements`. mutating func removeFirst() { firstUnused += 1 diff --git a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift index d0b54e56c..df7ebb488 100644 --- a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift @@ -81,9 +81,8 @@ extension RepeatingEndToEndTests { } @Test func parsing_repeatingStringRemaining_2() { - expectParse(Baz.self, ["--names"]) { baz in - #expect(!baz.verbose) - #expect(baz.names.isEmpty) + #expect(throws: (any Error).self) { + try Baz.parse(["--names"]) } } @@ -132,6 +131,27 @@ extension RepeatingEndToEndTests { #expect(baz.names == ["one", "two", "--verbose", "--other", "three"]) } } + + @Test func parsing_repeatingStringRemaining_9() { + expectParse(Baz.self, ["--verbose", "--names=one"]) { baz in + #expect(baz.verbose) + #expect(baz.names == ["one"]) + } + } + + @Test func parsing_repeatingStringRemaining_10() { + expectParse(Baz.self, ["--verbose", "--names=one", "two"]) { baz in + #expect(baz.verbose) + #expect(baz.names == ["one", "two"]) + } + } + + @Test func parsing_repeatingStringRemaining_11() { + expectParse(Baz.self, ["--names=one", "--verbose", "two"]) { baz in + #expect(!baz.verbose) + #expect(baz.names == ["one", "--verbose", "two"]) + } + } } // MARK: - diff --git a/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift b/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift index d2d9a29a2..fbde07c69 100644 --- a/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift +++ b/Tests/ArgumentParserUnitTests/ErrorMessageTests.swift @@ -366,6 +366,14 @@ private struct EmptyArray: ParsableArguments { var verbose = false } +private struct EmptyArrayAllRemaining: ParsableArguments { + @Option(parsing: .remaining) + var array: [String] = [] + + @Flag(name: [.short, .long]) + var verbose = false +} + extension ErrorMessageTests { static let emptyArrayCases: [ErrorCase] = [ ErrorCase( @@ -390,10 +398,30 @@ extension ErrorMessageTests { expected: "Missing value for '--array '"), ] + static let emptyArrayCasesAllRemaining: [ErrorCase] = [ + ErrorCase( + id: "--array alone", + arguments: ["--array"], + expected: "Missing value for '--array '"), + ErrorCase( + id: "--verbose followed by --array", + arguments: ["--verbose", "--array"], + expected: "Missing value for '--array '"), + ErrorCase( + id: "-v followed by --array", + arguments: ["-v", "--array"], + expected: "Missing value for '--array '"), + ] + @Test(arguments: emptyArrayCases) func emptyArray(_ c: ErrorCase) { expectErrorMessage(EmptyArray.self, c.arguments, c.expected) } + + @Test(arguments: emptyArrayCasesAllRemaining) + func emptyArrayAllRemaining(_ c: ErrorCase) { + expectErrorMessage(EmptyArrayAllRemaining.self, c.arguments, c.expected) + } } // MARK: - diff --git a/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift b/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift index 6bfb25117..1d30f3901 100644 --- a/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift +++ b/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift @@ -734,6 +734,41 @@ extension SplitArgumentTests { ) #expect(valueB.1 == "bar") } + + @Test func peekNextElement() throws { + var sut = try SplitArguments(arguments: ["--foo", "bar", "-baz", "qux"]) + + // To test peekNextElement(after:) we need an index... + let optStartIndex = sut.popNext() + let (startIndex, _) = try #require(optStartIndex) + + let (indexA, valueA) = try #require(sut.peekNextElement(after: startIndex)) + #expect( + indexA + == .argumentIndex( + SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + ) + #expect(valueA.value.valueString == "bar") + + let (indexB, valueB) = try #require(sut.peekNextElement(after: indexA)) + #expect( + indexB + == .argumentIndex( + SplitArguments.Index(inputIndex: 2, subIndex: .complete)) + ) + #expect(valueB.value == .option(.name(.longWithSingleDash("baz")))) + + let (indexC, valueC) = try #require(sut.peekNextElement(after: indexB)) + #expect( + indexC + == .argumentIndex( + SplitArguments.Index(inputIndex: 3, subIndex: .complete)) + ) + #expect(valueC.value.valueString == "qux") + + // There are no elements left. + #expect(sut.peekNextElement(after: indexC) == nil) + } } // MARK: - Long option with empty value (issue #958)