Skip to content
Open
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
17 changes: 17 additions & 0 deletions Sources/ArgumentParser/Parsing/ArgumentSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions Sources/ArgumentParser/Parsing/SplitArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 23 additions & 3 deletions Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}
}

Expand Down Expand Up @@ -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: -
Expand Down
28 changes: 28 additions & 0 deletions Tests/ArgumentParserUnitTests/ErrorMessageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -390,10 +398,30 @@ extension ErrorMessageTests {
expected: "Missing value for '--array <array>'"),
]

static let emptyArrayCasesAllRemaining: [ErrorCase] = [
ErrorCase(
id: "--array alone",
arguments: ["--array"],
expected: "Missing value for '--array <array>'"),
ErrorCase(
id: "--verbose followed by --array",
arguments: ["--verbose", "--array"],
expected: "Missing value for '--array <array>'"),
ErrorCase(
id: "-v followed by --array",
arguments: ["-v", "--array"],
expected: "Missing value for '--array <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: -
Expand Down
35 changes: 35 additions & 0 deletions Tests/ArgumentParserUnitTests/SplitArgumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down