Skip to content

Commit 9e77589

Browse files
authored
Don't repeat single-dash arguments with unconditionalRemaining arrays (#95)
This fixes #88.
1 parent 2d8a9bd commit 9e77589

File tree

3 files changed

+64
-32
lines changed

3 files changed

+64
-32
lines changed

Sources/ArgumentParser/Parsing/ArgumentSet.swift

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -377,8 +377,6 @@ extension ArgumentSet {
377377
}
378378

379379
var result = ParsedValues(elements: [], originalInput: all.originalInput)
380-
var positionalValues: [(InputOrigin.Element, String)] = []
381-
var unusedOptions: [(InputOrigin.Element, String)] = []
382380
var usedOrigins = InputOrigin()
383381

384382
try setInitialValues(into: &result)
@@ -390,8 +388,9 @@ extension ArgumentSet {
390388
}
391389

392390
switch next {
393-
case let .value(v):
394-
positionalValues.append((origin, v))
391+
case .value:
392+
// We'll parse positional values later.
393+
break
395394
case let .option(parsed):
396395
// Look for an argument that matches this `--option` or `-o`-style
397396
// input. If we can't find one, just move on to the next input. We
@@ -400,7 +399,6 @@ extension ArgumentSet {
400399
guard
401400
let argument: ArgumentDefinition = try? first(matching: parsed, at: origin)
402401
else {
403-
unusedOptions.append((origin, all.originalInput(at: origin)))
404402
continue
405403
}
406404

@@ -418,14 +416,15 @@ extension ArgumentSet {
418416
case .terminator:
419417
// Mark the terminator as used:
420418
result.set(ParsedValues.Element(key: .terminator, value: 0, inputOrigin: [origin]))
421-
unusedOptions.append((origin, all.originalInput(at: origin)))
422419
}
423420
}
424421

425422
// We have parsed all non-positional values at this point.
426423
// Next: parse / consume the positional values.
427424
do {
428-
try parsePositionalValues(positionalValues, unusedOptions: unusedOptions, into: &result)
425+
var stripped = all
426+
stripped.removeAll(in: usedOrigins)
427+
try parsePositionalValues(from: stripped, into: &result)
429428
} catch {
430429
switch error {
431430
case ParserError.unexpectedExtraValues:
@@ -470,28 +469,35 @@ extension ArgumentSet {
470469
}
471470

472471
func parsePositionalValues(
473-
_ positionalValues: [(InputOrigin.Element, String)],
474-
unusedOptions: [(InputOrigin.Element, String)],
472+
from unusedInput: SplitArguments,
475473
into result: inout ParsedValues
476474
) throws {
477-
guard !positionalValues.isEmpty || !unusedOptions.isEmpty else { return }
478-
var positionalValues = positionalValues[...]
479-
var unusedOptions = unusedOptions[...]
475+
// Filter out the inputs that aren't "whole" arguments, like `-h` and `-i`
476+
// from the input `-hi`.
477+
var argumentStack = unusedInput.elements.filter {
478+
$0.index.subIndex == .complete
479+
}.map {
480+
(InputOrigin.Element.argumentIndex($0.index), $0.element)
481+
}[...]
480482

481-
/// Pops the next origin / string pair to use.
482-
/// If `unconditional` is false, this always pops from `positionalValues`;
483-
/// if true, then it pops from whichever of `positionalValues` or
484-
/// `unusedOptions` has the element that came first in the original input.
485-
func next(unconditional: Bool) -> (InputOrigin.Element, String)? {
486-
guard unconditional, let firstUnusedOption = unusedOptions.first else {
487-
return positionalValues.popFirst()
483+
guard !argumentStack.isEmpty else { return }
484+
485+
/// Pops arguments until reaching one that is a value (i.e., isn't dash-
486+
/// prefixed).
487+
func skipNonValues() {
488+
while argumentStack.first?.1.isValue == false {
489+
_ = argumentStack.popFirst()
488490
}
489-
guard let firstPositional = positionalValues.first else {
490-
return unusedOptions.popFirst()
491+
}
492+
493+
/// Pops the origin of the next argument to use.
494+
///
495+
/// If `unconditional` is false, this skips over any non-"value" input.
496+
func next(unconditional: Bool) -> InputOrigin.Element? {
497+
if !unconditional {
498+
skipNonValues()
491499
}
492-
return firstPositional.0 < firstUnusedOption.0
493-
? positionalValues.popFirst()
494-
: unusedOptions.popFirst()
500+
return argumentStack.popFirst()?.0
495501
}
496502

497503
ArgumentLoop:
@@ -503,16 +509,20 @@ extension ArgumentSet {
503509
let allowOptionsAsInput = argumentDefinition.parsingStrategy == .allRemainingInput
504510

505511
repeat {
506-
guard let (origin, value) = next(unconditional: allowOptionsAsInput) else {
512+
guard let origin = next(unconditional: allowOptionsAsInput) else {
507513
break ArgumentLoop
508514
}
515+
let value = unusedInput.originalInput(at: origin)!
509516
try update([origin], nil, value, &result)
510517
} while argumentDefinition.isRepeatingPositional
511518
}
512519

513520
// Finished with the defined arguments; are there leftover values to parse?
514-
guard positionalValues.isEmpty else {
515-
let extraValues = positionalValues.map { (InputOrigin(element: $0.0), $0.1) }
521+
skipNonValues()
522+
guard argumentStack.isEmpty else {
523+
let extraValues: [(InputOrigin, String)] = argumentStack.map(\.0).map {
524+
return (InputOrigin(element: $0), unusedInput.originalInput(at: $0)!)
525+
}
516526
throw ParserError.unexpectedExtraValues(extraValues)
517527
}
518528
}

Sources/ArgumentParser/Parsing/SplitArguments.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,13 @@ extension SplitArguments {
206206
}?.1
207207
}
208208

209-
/// Returns the original input string at the given origin.
210-
func originalInput(at origin: InputOrigin.Element) -> String {
211-
switch origin {
212-
case .argumentIndex(let index):
213-
return originalInput[index.inputIndex.rawValue]
209+
/// Returns the original input string at the given origin, or `nil` if
210+
/// `origin` is a sub-index.
211+
func originalInput(at origin: InputOrigin.Element) -> String? {
212+
guard case let .argumentIndex(index) = origin else {
213+
return nil
214214
}
215+
return originalInput[index.inputIndex.rawValue]
215216
}
216217

217218
mutating func popNext() -> (InputOrigin.Element, Element)? {

Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ extension RepeatingEndToEndTests {
302302

303303
fileprivate struct Foozle: ParsableArguments {
304304
@Flag() var verbose: Bool
305+
@Flag(name: .customShort("f")) var useFiles: Bool
306+
@Flag(name: .customShort("i")) var useStandardInput: Bool
305307
@Argument(parsing: .unconditionalRemaining) var names: [String]
306308
}
307309

@@ -346,6 +348,25 @@ extension RepeatingEndToEndTests {
346348
XCTAssertFalse(foozle.verbose)
347349
XCTAssertEqual(foozle.names, ["--", "--verbose", "--other", "one", "two", "three"])
348350
}
351+
352+
AssertParse(Foozle.self, ["-one", "-two", "three"]) { foozle in
353+
XCTAssertFalse(foozle.verbose)
354+
XCTAssertFalse(foozle.useFiles)
355+
XCTAssertFalse(foozle.useStandardInput)
356+
XCTAssertEqual(foozle.names, ["-one", "-two", "three"])
357+
}
358+
359+
AssertParse(Foozle.self, ["-one", "-two", "three", "-if"]) { foozle in
360+
XCTAssertFalse(foozle.verbose)
361+
XCTAssertTrue(foozle.useFiles)
362+
XCTAssertTrue(foozle.useStandardInput)
363+
XCTAssertEqual(foozle.names, ["-one", "-two", "three"])
364+
}
365+
}
366+
367+
func testParsing_repeatingUnconditionalArgument_Fails() throws {
368+
// Only partially matches the `-fob` argument
369+
XCTAssertThrowsError(try Foozle.parse(["-fib"]))
349370
}
350371
}
351372

0 commit comments

Comments
 (0)