Skip to content

Commit 72fb0b1

Browse files
authored
Enforce that options must be used by command before matching a subcommand (#96)
We were incorrectly skipping over dash-prefixed inputs when looking for the next subcommand. This means that input like `command sub1 --foo sub2` would match the sub1 and sub2 subcommands, even if `--foo` wasn't defined by sub1. This manifested in issues where a value expected by `--foo` would be eaten by the subcommand matcher. Fixes #92.
1 parent ddc828f commit 72fb0b1

File tree

3 files changed

+96
-30
lines changed

3 files changed

+96
-30
lines changed

Sources/ArgumentParser/Parsing/CommandParser.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ extension CommandParser {
5252
/// - Returns: A node for the matched subcommand if one was found;
5353
/// otherwise, `nil`.
5454
fileprivate func consumeNextCommand(split: inout SplitArguments) -> Tree<ParsableCommand.Type>? {
55-
if let command = split.peekNextValue() {
56-
if let subcommandNode = currentNode.firstChild(withName: command.1) {
57-
_ = split.popNextValue()
58-
return subcommandNode
59-
}
60-
}
61-
return nil
55+
guard let (origin, element) = split.peekNext(),
56+
element.isValue,
57+
let value = split.originalInput(at: origin),
58+
let subcommandNode = currentNode.firstChild(withName: value)
59+
else { return nil }
60+
_ = split.popNextValue()
61+
return subcommandNode
6262
}
6363

6464
/// Throws a `HelpRequested` error if the user has specified either of the
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//===----------------------------------------------------------*- swift -*-===//
2+
//
3+
// This source file is part of the Swift Argument Parser open source project
4+
//
5+
// Copyright (c) 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
import XCTest
13+
import ArgumentParserTestHelpers
14+
import ArgumentParser
15+
16+
final class DefaultSubcommandEndToEndTests: XCTestCase {
17+
}
18+
19+
// MARK: -
20+
21+
private struct Main: ParsableCommand {
22+
static var configuration = CommandConfiguration(
23+
subcommands: [Default.self, Foo.self, Bar.self],
24+
defaultSubcommand: Default.self
25+
)
26+
}
27+
28+
private struct Default: ParsableCommand {
29+
enum Mode: String, CaseIterable, ExpressibleByArgument {
30+
case foo, bar, baz
31+
}
32+
33+
@Option(default: .foo) var mode: Mode
34+
}
35+
36+
private struct Foo: ParsableCommand {}
37+
private struct Bar: ParsableCommand {}
38+
39+
extension DefaultSubcommandEndToEndTests {
40+
func testDefaultSubcommand() {
41+
AssertParseCommand(Main.self, Default.self, []) { def in
42+
XCTAssertEqual(.foo, def.mode)
43+
}
44+
45+
AssertParseCommand(Main.self, Default.self, ["--mode=bar"]) { def in
46+
XCTAssertEqual(.bar, def.mode)
47+
}
48+
49+
AssertParseCommand(Main.self, Default.self, ["--mode", "bar"]) { def in
50+
XCTAssertEqual(.bar, def.mode)
51+
}
52+
53+
AssertParseCommand(Main.self, Default.self, ["--mode", "baz"]) { def in
54+
XCTAssertEqual(.baz, def.mode)
55+
}
56+
}
57+
58+
func testNonDefaultSubcommand() {
59+
AssertParseCommand(Main.self, Foo.self, ["foo"]) { _ in }
60+
AssertParseCommand(Main.self, Bar.self, ["bar"]) { _ in }
61+
62+
AssertParseCommand(Main.self, Default.self, ["default", "--mode", "bar"]) { def in
63+
XCTAssertEqual(.bar, def.mode)
64+
}
65+
}
66+
67+
func testParsingFailure() {
68+
XCTAssertThrowsError(try Main.parseAsRoot(["--mode", "qux"]))
69+
XCTAssertThrowsError(try Main.parseAsRoot(["qux"]))
70+
}
71+
}

Tests/ArgumentParserEndToEndTests/NestedCommandEndToEndTests.swift

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,13 @@ fileprivate func AssertParseFooCommand<A>(_ type: A.Type, _ arguments: [String],
5858

5959
extension NestedCommandEndToEndTests {
6060
func testParsing_package() throws {
61-
AssertParseFooCommand(Foo.Package.Clean.self, ["package", "clean"]) { clean in
62-
XCTAssertEqual(clean.foo.verbose, false)
63-
XCTAssertEqual(clean.package.force, false)
61+
AssertParseFooCommand(Foo.Package.self, ["package"]) { package in
62+
XCTAssertFalse(package.force)
6463
}
6564

66-
AssertParseFooCommand(Foo.Package.Clean.self, ["-f", "package", "clean"]) { clean in
65+
AssertParseFooCommand(Foo.Package.Clean.self, ["package", "clean"]) { clean in
6766
XCTAssertEqual(clean.foo.verbose, false)
68-
XCTAssertEqual(clean.package.force, true)
67+
XCTAssertEqual(clean.package.force, false)
6968
}
7069

7170
AssertParseFooCommand(Foo.Package.Clean.self, ["package", "-f", "clean"]) { clean in
@@ -98,11 +97,6 @@ extension NestedCommandEndToEndTests {
9897
XCTAssertEqual(config.package.force, true)
9998
}
10099

101-
AssertParseFooCommand(Foo.Package.Config.self, ["-f", "package", "config"]) { config in
102-
XCTAssertEqual(config.foo.verbose, false)
103-
XCTAssertEqual(config.package.force, true)
104-
}
105-
106100
AssertParseFooCommand(Foo.Package.Config.self, ["package", "-v", "config", "-f"]) { config in
107101
XCTAssertEqual(config.foo.verbose, true)
108102
XCTAssertEqual(config.package.force, true)
@@ -132,19 +126,20 @@ extension NestedCommandEndToEndTests {
132126
}
133127

134128
func testParsing_fails() throws {
135-
XCTAssertThrowsError(try Foo.parse(["package"]))
136-
XCTAssertThrowsError(try Foo.parse(["clean", "package"]))
137-
XCTAssertThrowsError(try Foo.parse(["config", "package"]))
138-
XCTAssertThrowsError(try Foo.parse(["package", "c"]))
139-
XCTAssertThrowsError(try Foo.parse(["package", "build"]))
140-
XCTAssertThrowsError(try Foo.parse(["package", "build", "clean"]))
141-
XCTAssertThrowsError(try Foo.parse(["package", "clean", "foo"]))
142-
XCTAssertThrowsError(try Foo.parse(["package", "config", "bar"]))
143-
XCTAssertThrowsError(try Foo.parse(["package", "clean", "build"]))
144-
XCTAssertThrowsError(try Foo.parse(["build"]))
145-
XCTAssertThrowsError(try Foo.parse(["build", "-f"]))
146-
XCTAssertThrowsError(try Foo.parse(["build", "--build"]))
147-
XCTAssertThrowsError(try Foo.parse(["build", "--build", "12"]))
129+
XCTAssertThrowsError(try Foo.parseAsRoot(["clean", "package"]))
130+
XCTAssertThrowsError(try Foo.parseAsRoot(["config", "package"]))
131+
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "c"]))
132+
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "build"]))
133+
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "build", "clean"]))
134+
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "clean", "foo"]))
135+
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "config", "bar"]))
136+
XCTAssertThrowsError(try Foo.parseAsRoot(["package", "clean", "build"]))
137+
XCTAssertThrowsError(try Foo.parseAsRoot(["build"]))
138+
XCTAssertThrowsError(try Foo.parseAsRoot(["build", "-f"]))
139+
XCTAssertThrowsError(try Foo.parseAsRoot(["build", "--build"]))
140+
XCTAssertThrowsError(try Foo.parseAsRoot(["build", "--build", "12"]))
141+
XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "package", "clean"]))
142+
XCTAssertThrowsError(try Foo.parseAsRoot(["-f", "package", "config"]))
148143
}
149144
}
150145

0 commit comments

Comments
 (0)