From 643d38c88c86e94fd2c3e6b7d968a51d05d9336b Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Thu, 22 Sep 2022 16:53:35 -0500 Subject: [PATCH 1/4] Add two new `ArgumentArrayParsingStrategy` options This adds two new parsing options for argument arrays, and renames `.unconditionalRemaining` to `.captureForPassthrough`. - `.allUnrecognized` collects all the inputs that weren't used during parsing. This essentially suppresses all "unrecognized flag/option" and "unexpected argument" errors, and makes those extra inputs available to the client. - `.postTerminator` collects all inputs that follow the `--` terminator, before trying to parse any other positional arguments. This is a non-standard, but sometimes useful parsing strategy. --- .../Parsable Properties/Argument.swift | 38 ++++++--- .../Parsing/ArgumentDefinition.swift | 9 +- .../ArgumentParser/Parsing/ArgumentSet.swift | 84 ++++++++++++++----- .../Parsing/SplitArguments.swift | 4 +- 4 files changed, 98 insertions(+), 37 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index 0d44f9c5e..ef2d7afb0 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -120,19 +120,29 @@ public struct ArgumentArrayParsingStrategy: Hashable { self.init(base: .default) } + /// After parsing, capture all unrecognized inputs in this argument array. + public static var allUnrecognized: ArgumentArrayParsingStrategy { + self.init(base: .allUnrecognized) + } + + /// Before parsing, capture all inputs that follow the `--` terminator in this + /// argument array. + public static var postTerminator: ArgumentArrayParsingStrategy { + self.init(base: .postTerminator) + } + /// Parse all remaining inputs after parsing any known options or flags, /// including dash-prefixed inputs and the `--` terminator. /// - /// When you use the `unconditionalRemaining` parsing strategy, the parser + /// When you use the `captureForPassthrough` parsing strategy, the parser /// stops parsing flags and options as soon as it encounters a positional - /// argument or an unrecognized flag. For example, for a parsable type - /// defined as following: + /// argument or an unrecognized flag, and captures all remaining inputs in + /// the array argument. For example, for a parsable type defined as following: /// /// struct Options: ParsableArguments { - /// @Flag - /// var verbose: Bool = false + /// @Flag var verbose: Bool = false /// - /// @Argument(parsing: .unconditionalRemaining) + /// @Argument(parsing: .captureForPassthrough) /// var words: [String] = [] /// } /// @@ -141,14 +151,20 @@ public struct ArgumentArrayParsingStrategy: Hashable { /// `Options(verbose: true, words: ["one", "two", "--verbose"])`. /// /// - Note: This parsing strategy can be surprising for users, particularly - /// when combined with options and flags. Prefer `remaining` whenever + /// when combined with options and flags. Prefer `upToNextOption` whenever /// possible, since users can always terminate options and flags with - /// the `--` terminator. With the `remaining` parsing strategy, the input - /// `--verbose -- one two --verbose` would have the same result as the above - /// example: `Options(verbose: true, words: ["one", "two", "--verbose"])`. - public static var unconditionalRemaining: ArgumentArrayParsingStrategy { + /// the `--` terminator. With the `upToNextOption` parsing strategy, the + /// input `--verbose -- one two --verbose` would have the same result as + /// the above example: + /// `Options(verbose: true, words: ["one", "two", "--verbose"])`. + public static var captureForPassthrough: ArgumentArrayParsingStrategy { self.init(base: .allRemainingInput) } + + @available(*, deprecated, renamed: "captureForPassthrough") + public static var unconditionalRemaining: ArgumentArrayParsingStrategy { + .captureForPassthrough + } } // MARK: - @Argument T: ExpressibleByArgument Initializers diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index e8aebb7c2..08cf9cd52 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -78,8 +78,8 @@ struct ArgumentDefinition { /// This folds the public `ArrayParsingStrategy` and `SingleValueParsingStrategy` /// into a single enum. enum ParsingStrategy { - /// Expect the next `SplitArguments.Element` to be a value and parse it. Will fail if the next - /// input is an option. + /// Expect the next `SplitArguments.Element` to be a value and parse it. + /// Will fail if the next input is an option. case `default` /// Parse the next `SplitArguments.Element.value` case scanningForValue @@ -89,6 +89,11 @@ struct ArgumentDefinition { case upToNextOption /// Parse all remaining `SplitArguments.Element` as values, regardless of its type. case allRemainingInput + /// Collect all the elements after the terminator, preventing them from + /// appearing in any other position. + case postTerminator + /// Collect all unused inputs after the + case allUnrecognized } var kind: Kind diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 1abaca160..4375719f9 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -316,6 +316,10 @@ extension ArgumentSet { try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } + + case .postTerminator, .allUnrecognized: + // These parsing kinds are for arguments only. + throw ParserError.invalidState } } @@ -442,49 +446,83 @@ extension ArgumentSet { from unusedInput: SplitArguments, into result: inout ParsedValues ) throws { - // Filter out the inputs that aren't "whole" arguments, like `-h` and `-i` - // from the input `-hi`. - var argumentStack = unusedInput.elements.filter { - $0.index.subIndex == .complete - }.map { - (InputOrigin.Element.argumentIndex($0.index), $0) - }[...] - - guard !argumentStack.isEmpty else { return } + var endOfInput = unusedInput.elements.endIndex - /// Pops arguments until reaching one that is a value (i.e., isn't dash- - /// prefixed). - func skipNonValues() { - while argumentStack.first?.1.isValue == false { - _ = argumentStack.popFirst() + // Check for a post-terminator argument, and if so, collect all post- + // terminator args. + if let postTerminatorArg = self.first(where: { def in + def.isRepeatingPositional && def.parsingStrategy == .postTerminator + }), + case let .unary(update) = postTerminatorArg.update, + let terminatorIndex = unusedInput.elements.firstIndex(where: \.isTerminator) + { + for input in unusedInput.elements[(terminatorIndex + 1)...] { + // Everything post-terminator is a value, force-unwrapping here is safe: + let value = input.value.valueString! + try update([.argumentIndex(input.index)], nil, value, &result) } + + endOfInput = terminatorIndex } - - /// Pops the origin of the next argument to use. - /// - /// If `unconditional` is false, this skips over any non-"value" input. - func next(unconditional: Bool) -> InputOrigin.Element? { - if !unconditional { - skipNonValues() + + // Create a stack out of the remaining unused inputs that aren't "partial" + // arguments (i.e. the individual components of a `-vix` grouped short + // option input). + var argumentStack = unusedInput.elements[.. SplitArguments.Element? { + while let arg = argumentStack.popFirst() { + if arg.isValue || unconditional { + return arg + } } - return argumentStack.popFirst()?.0 + + return nil } + // For all positional arguments, consume one or more inputs. + var usedOrigins = InputOrigin() ArgumentLoop: for argumentDefinition in self { guard case .positional = argumentDefinition.kind else { continue } + switch argumentDefinition.parsingStrategy { + case .default, .allRemainingInput: + break + default: + continue ArgumentLoop + } guard case let .unary(update) = argumentDefinition.update else { preconditionFailure("Shouldn't see a nullary positional argument.") } let allowOptionsAsInput = argumentDefinition.parsingStrategy == .allRemainingInput repeat { - guard let origin = next(unconditional: allowOptionsAsInput) else { + guard let arg = next(unconditional: allowOptionsAsInput) else { break ArgumentLoop } + let origin: InputOrigin.Element = .argumentIndex(arg.index) let value = unusedInput.originalInput(at: origin)! try update([origin], nil, value, &result) + usedOrigins.insert(origin) } while argumentDefinition.isRepeatingPositional } + + // If there's an `.allUnrecognized` argument array, collect leftover args. + if let allUnrecognizedArg = self.first(where: { def in + def.isRepeatingPositional && def.parsingStrategy == .allUnrecognized + }), + case let .unary(update) = allUnrecognizedArg.update + { + while let arg = argumentStack.popFirst() { + let origin: InputOrigin.Element = .argumentIndex(arg.index) + let value = unusedInput.originalInput(at: origin)! + try update([origin], nil, value, &result) + } + } } } diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index 2d08ee6c9..fdf1ab7d2 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -500,11 +500,13 @@ extension SplitArguments { return $0.index.inputIndex } - // Now return all elements that are either: + // Now return all non-terminator elements that are either: // 1) `.complete` // 2) `.sub` but not in `completeIndexes` let extraElements = elements.filter { + if $0.isTerminator { return false } + switch $0.index.subIndex { case .complete: return true From 275f5822812bb2e7f7f0d4556ceb2a9383693a0a Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 26 Sep 2022 12:08:00 -0500 Subject: [PATCH 2/4] Revise / add documentation for parsing strategies --- .../Parsable Properties/Argument.swift | 168 +++++++++++++++--- 1 file changed, 145 insertions(+), 23 deletions(-) diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index ef2d7afb0..1899e76ae 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -104,29 +104,121 @@ public struct ArgumentArrayParsingStrategy: Hashable { /// Parse only unprefixed values from the command-line input, ignoring /// any inputs that have a dash prefix. This is the default strategy. /// - /// For example, for a parsable type defined as following: + /// `remaining` is the default parsing strategy for argument arrays. /// - /// struct Options: ParsableArguments { - /// @Flag var verbose: Bool - /// @Argument(parsing: .remaining) var words: [String] + /// For example, the `Example` command defined below has a `words` array that + /// uses the `remaining` parsing strategy: + /// + /// @main + /// struct Example: ParsableCommand { + /// @Flag var verbose = false + /// + /// @Argument(parsing: .remaining) + /// var words: [String] + /// + /// func run() { + /// print(words.joined(separator: "\n")) + /// } /// } /// - /// Parsing the input `--verbose one two` or `one two --verbose` would result - /// in `Options(verbose: true, words: ["one", "two"])`. Parsing the input - /// `one two --other` would result in an unknown option error for `--other`. + /// Any non-dash-prefixed inputs will be captured in the `words` array. + /// + /// ``` + /// $ example --verbose one two + /// one + /// two + /// $ example one two --verbose + /// one + /// two + /// $ example one two --other + /// Error: Unknown option '--other' + /// ``` + /// + /// If a user uses the `--` terminator in their input, all following inputs + /// will be captured in `words`. /// - /// This is the default strategy for parsing argument arrays. + /// ``` + /// $ example one two -- --verbose --other + /// one + /// two + /// --verbose + /// --other + /// ``` public static var remaining: ArgumentArrayParsingStrategy { self.init(base: .default) } /// After parsing, capture all unrecognized inputs in this argument array. + /// + /// You can use the `allUnrecognized` parsing strategy to suppress + /// "unexpected argument" errors or to capture unrecognized inputs for further + /// processing. + /// + /// For example, the `Example` command defined below has an `other` array that + /// uses the `allUnrecognized` parsing strategy: + /// + /// @main + /// struct Example: ParsableCommand { + /// @Flag var verbose = false + /// @Argument var name: String + /// + /// @Argument(parsing: .allUnrecognized) + /// var other: [String] + /// + /// func run() { + /// print(other.joined(separator: "\n")) + /// } + /// } + /// + /// After parsing the `--verbose` flag and `` argument, any remaining + /// input is captured in the `other` array. + /// + /// ``` + /// $ example --verbose Negin one two + /// one + /// two + /// $ example Asa --verbose --other -zzz + /// --other + /// -zzz + /// ``` public static var allUnrecognized: ArgumentArrayParsingStrategy { self.init(base: .allUnrecognized) } /// Before parsing, capture all inputs that follow the `--` terminator in this /// argument array. + /// + /// For example, the `Example` command defined below has a `words` array that + /// uses the `postTerminator` parsing strategy: + /// + /// @main + /// struct Example: ParsableCommand { + /// @Flag var verbose = false + /// @Argument var name = "" + /// + /// @Argument(parsing: .postTerminator) + /// var words: [String] + /// + /// func run() { + /// print(words.joined(separator: "\n")) + /// } + /// } + /// + /// Before looking for the `--verbose` flag and `` argument, any inputs + /// after the `--` terminator are captured into the `words` array. + /// + /// ``` + /// $ example --verbose Asa -- one two --other + /// one + /// two + /// --other + /// $ example Asa Extra -- one two --other + /// Error: Unexpected argument 'Extra' + /// ``` + /// + /// - Note: This parsing strategy can be surprising for users, since it + /// changes the behavior of the `--` terminator. Prefer ``remaining`` + /// whenever possible. public static var postTerminator: ArgumentArrayParsingStrategy { self.init(base: .postTerminator) } @@ -134,29 +226,59 @@ public struct ArgumentArrayParsingStrategy: Hashable { /// Parse all remaining inputs after parsing any known options or flags, /// including dash-prefixed inputs and the `--` terminator. /// - /// When you use the `captureForPassthrough` parsing strategy, the parser - /// stops parsing flags and options as soon as it encounters a positional - /// argument or an unrecognized flag, and captures all remaining inputs in - /// the array argument. For example, for a parsable type defined as following: + /// You can use the `captureForPassthrough` parsing strategy if you need to + /// capture a user's input to manually pass it unchanged to another command. + /// + /// When you use this parsing strategy, the parser stops parsing flags and + /// options as soon as it encounters a positional argument or an unrecognized + /// flag, and captures all remaining inputs in the array argument. /// - /// struct Options: ParsableArguments { - /// @Flag var verbose: Bool = false + /// For example, the `Example` command defined below has an `words` array that + /// uses the `captureForPassthrough` parsing strategy: + /// + /// @main + /// struct Example: ParsableCommand { + /// @Flag var verbose = false /// /// @Argument(parsing: .captureForPassthrough) /// var words: [String] = [] + /// + /// func run() { + /// print(words.joined(separator: "\n")) + /// } /// } /// - /// Parsing the input `--verbose one two --verbose` includes the second - /// `--verbose` flag in `words`, resulting in - /// `Options(verbose: true, words: ["one", "two", "--verbose"])`. + /// Any values after the first unrecognized input are captured in the `words` + /// array. + /// + /// ``` + /// $ example --verbose one two --other + /// one + /// two + /// --other + /// $ example one two --verbose + /// one + /// two + /// --verbose + /// ``` + /// + /// With the `captureForPassthrough` parsing strategy, the `--` terminator + /// is included in the captured values. + /// + /// ``` + /// $ example --verbose one two -- --other + /// one + /// two + /// -- + /// --other + /// ``` /// /// - Note: This parsing strategy can be surprising for users, particularly - /// when combined with options and flags. Prefer `upToNextOption` whenever - /// possible, since users can always terminate options and flags with - /// the `--` terminator. With the `upToNextOption` parsing strategy, the - /// input `--verbose -- one two --verbose` would have the same result as - /// the above example: - /// `Options(verbose: true, words: ["one", "two", "--verbose"])`. + /// when combined with options and flags. Prefer ``remaining`` or + /// ``allUnrecognized`` whenever possible, since users can always terminate + /// options and flags with the `--` terminator. With the `remaining` + /// parsing strategy, the input `--verbose -- one two --other` would have + /// the same result as the first example above. public static var captureForPassthrough: ArgumentArrayParsingStrategy { self.init(base: .allRemainingInput) } From 5cf901fd30fd402aee69b3795b3800cc091b4613 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 30 Sep 2022 16:20:28 -0500 Subject: [PATCH 3/4] Add tests for allUnrecognized and postTerminator --- .../CMakeLists.txt | 1 + .../DefaultSubcommandEndToEndTests.swift | 4 +- ...peatingEndToEndTests+ParsingStrategy.swift | 230 ++++++++++++++++++ .../RepeatingEndToEndTests.swift | 92 +------ 4 files changed, 234 insertions(+), 93 deletions(-) create mode 100644 Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests+ParsingStrategy.swift diff --git a/Tests/ArgumentParserEndToEndTests/CMakeLists.txt b/Tests/ArgumentParserEndToEndTests/CMakeLists.txt index d89093b13..2e094848c 100644 --- a/Tests/ArgumentParserEndToEndTests/CMakeLists.txt +++ b/Tests/ArgumentParserEndToEndTests/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(EndToEndTests PositionalEndToEndTests.swift RawRepresentableEndToEndTests.swift RepeatingEndToEndTests.swift + RepeatingEndToEndTests+ParsingStrategy.swift ShortNameEndToEndTests.swift SimpleEndToEndTests.swift SingleValueParsingStrategyTests.swift diff --git a/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift index 2073afa8b..db21448c7 100644 --- a/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/DefaultSubcommandEndToEndTests.swift @@ -91,7 +91,7 @@ extension DefaultSubcommandEndToEndTests { @OptionGroup var options: CommonOptions @Argument var pluginName: String - @Argument(parsing: .unconditionalRemaining) + @Argument(parsing: .captureForPassthrough) var pluginArguments: [String] = [] } @@ -99,7 +99,7 @@ extension DefaultSubcommandEndToEndTests { @OptionGroup var options: CommonOptions @Argument var pluginName: String - @Argument(parsing: .unconditionalRemaining) + @Argument(parsing: .captureForPassthrough) var pluginArguments: [String] = [] } diff --git a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests+ParsingStrategy.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests+ParsingStrategy.swift new file mode 100644 index 000000000..457f3bbaf --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests+ParsingStrategy.swift @@ -0,0 +1,230 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +import XCTest +import ArgumentParserTestHelpers +import ArgumentParser + +// MARK: - allUnrecognized + +fileprivate struct AllUnrecognizedArgs: ParsableArguments { + @Flag var verbose: Bool = false + @Flag(name: .customShort("f")) var useFiles: Bool = false + @Flag(name: .customShort("i")) var useStandardInput: Bool = false + @Option var config = "debug" + @Argument(parsing: .allUnrecognized) var names: [String] = [] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingAllUnrecognized() throws { + AssertParse(AllUnrecognizedArgs.self, []) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, []) + } + AssertParse(AllUnrecognizedArgs.self, ["foo", "--verbose", "-fi", "bar", "-z", "--other"]) { cmd in + XCTAssertTrue(cmd.verbose) + XCTAssertTrue(cmd.useFiles) + XCTAssertTrue(cmd.useStandardInput) + XCTAssertEqual(cmd.names, ["foo", "bar", "-z", "--other"]) + } + AssertParse(AllUnrecognizedArgs.self, []) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, []) + } + } + + func testParsing_repeatingAllUnrecognized_Fails() throws { + // Only partially matches the `-fib` argument + XCTAssertThrowsError(try PassthroughArgs.parse(["-fib"])) + } +} + +fileprivate struct AllUnrecognizedRoot: ParsableCommand { + static var configuration: CommandConfiguration { + .init(subcommands: [Child.self]) + } + + @Flag var verbose: Bool = false + + struct Child: ParsableCommand { + @Flag var includeExtras: Bool = false + @Option var config = "debug" + @Argument(parsing: .allUnrecognized) var extras: [String] = [] + @OptionGroup var root: AllUnrecognizedRoot + } +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingAllUnrecognized_Nested() throws { + AssertParseCommand( + AllUnrecognizedRoot.self, AllUnrecognizedRoot.Child.self, + ["child"]) + { cmd in + XCTAssertFalse(cmd.root.verbose) + XCTAssertFalse(cmd.includeExtras) + XCTAssertEqual(cmd.config, "debug") + XCTAssertEqual(cmd.extras, []) + } + AssertParseCommand( + AllUnrecognizedRoot.self, AllUnrecognizedRoot.Child.self, + ["child", "--verbose", "--other", "one", "two", "--config", "prod"]) + { cmd in + XCTAssertTrue(cmd.root.verbose) + XCTAssertFalse(cmd.includeExtras) + XCTAssertEqual(cmd.config, "prod") + XCTAssertEqual(cmd.extras, ["--other", "one", "two"]) + } + } + + func testParsing_repeatingAllUnrecognized_Nested_Fails() throws { + // Extra arguments need to make it to the child + XCTAssertThrowsError(try AllUnrecognizedRoot.parse(["--verbose", "--other"])) + } +} + +// MARK: - postTerminator + +fileprivate struct PostTerminatorArgs: ParsableArguments { + @Flag(name: .customShort("f")) var useFiles: Bool = false + @Flag(name: .customShort("i")) var useStandardInput: Bool = false + @Option var config = "debug" + @Argument var title: String? + @Argument(parsing: .postTerminator) + var names: [String] = [] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingPostTerminator() throws { + AssertParse(PostTerminatorArgs.self, []) { cmd in + XCTAssertNil(cmd.title) + XCTAssertEqual(cmd.names, []) + } + AssertParse(PostTerminatorArgs.self, ["--", "-fi"]) { cmd in + XCTAssertNil(cmd.title) + XCTAssertEqual(cmd.names, ["-fi"]) + } + AssertParse(PostTerminatorArgs.self, ["-fi", "--", "-fi", "--"]) { cmd in + XCTAssertTrue(cmd.useFiles) + XCTAssertTrue(cmd.useStandardInput) + XCTAssertNil(cmd.title) + XCTAssertEqual(cmd.names, ["-fi", "--"]) + } + AssertParse(PostTerminatorArgs.self, ["-fi", "title", "--", "title"]) { cmd in + XCTAssertTrue(cmd.useFiles) + XCTAssertTrue(cmd.useStandardInput) + XCTAssertEqual(cmd.title, "title") + XCTAssertEqual(cmd.names, ["title"]) + } + AssertParse(PostTerminatorArgs.self, ["--config", "config", "--", "--config", "post"]) { cmd in + XCTAssertEqual(cmd.config, "config") + XCTAssertNil(cmd.title) + XCTAssertEqual(cmd.names, ["--config", "post"]) + } + } + + func testParsing_repeatingPostTerminator_Fails() throws { + // Only partially matches the `-fib` argument + XCTAssertThrowsError(try PostTerminatorArgs.parse(["-fib"])) + // The post-terminator input can't provide the option's value + XCTAssertThrowsError(try PostTerminatorArgs.parse(["--config", "--", "config"])) + } +} + +// MARK: - captureForPassthrough + +fileprivate struct PassthroughArgs: ParsableArguments { + @Flag var verbose: Bool = false + @Flag(name: .customShort("f")) var useFiles: Bool = false + @Flag(name: .customShort("i")) var useStandardInput: Bool = false + @Option var config = "debug" + @Argument(parsing: .captureForPassthrough) var names: [String] = [] +} + +extension RepeatingEndToEndTests { + func testParsing_repeatingCaptureForPassthrough() throws { + AssertParse(PassthroughArgs.self, []) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, []) + } + + AssertParse(PassthroughArgs.self, ["--other"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, ["--other"]) + } + + AssertParse(PassthroughArgs.self, ["--verbose", "one", "two", "three"]) { cmd in + XCTAssertTrue(cmd.verbose) + XCTAssertEqual(cmd.names, ["one", "two", "three"]) + } + + AssertParse(PassthroughArgs.self, ["one", "two", "three", "--other", "--verbose"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, ["one", "two", "three", "--other", "--verbose"]) + } + + AssertParse(PassthroughArgs.self, ["--verbose", "--other", "one", "two", "three"]) { cmd in + XCTAssertTrue(cmd.verbose) + XCTAssertEqual(cmd.names, ["--other", "one", "two", "three"]) + } + + AssertParse(PassthroughArgs.self, ["--verbose", "--other", "one", "--", "two", "three"]) { cmd in + XCTAssertTrue(cmd.verbose) + XCTAssertEqual(cmd.names, ["--other", "one", "--", "two", "three"]) + } + + AssertParse(PassthroughArgs.self, ["--other", "one", "--", "two", "three", "--verbose"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, ["--other", "one", "--", "two", "three", "--verbose"]) + } + + AssertParse(PassthroughArgs.self, ["--", "--verbose", "--other", "one", "two", "three"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertEqual(cmd.names, ["--", "--verbose", "--other", "one", "two", "three"]) + } + + AssertParse(PassthroughArgs.self, ["-one", "-two", "three"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertFalse(cmd.useFiles) + XCTAssertFalse(cmd.useStandardInput) + XCTAssertEqual(cmd.names, ["-one", "-two", "three"]) + } + + AssertParse(PassthroughArgs.self, ["--config", "release", "one", "two", "--config", "debug"]) { cmd in + XCTAssertEqual(cmd.config, "release") + XCTAssertEqual(cmd.names, ["one", "two", "--config", "debug"]) + } + + AssertParse(PassthroughArgs.self, ["--config", "release", "--config", "debug", "one", "two"]) { cmd in + XCTAssertEqual(cmd.config, "debug") + XCTAssertEqual(cmd.names, ["one", "two"]) + } + + AssertParse(PassthroughArgs.self, ["-if", "-one", "-two", "three"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertTrue(cmd.useFiles) + XCTAssertTrue(cmd.useStandardInput) + XCTAssertEqual(cmd.names, ["-one", "-two", "three"]) + } + + AssertParse(PassthroughArgs.self, ["-one", "-two", "-three", "-if"]) { cmd in + XCTAssertFalse(cmd.verbose) + XCTAssertFalse(cmd.useFiles) + XCTAssertFalse(cmd.useStandardInput) + XCTAssertEqual(cmd.names, ["-one", "-two", "-three", "-if"]) + } + } + + func testParsing_repeatingCaptureForPassthrough_Fails() throws { + // Only partially matches the `-fib` argument + XCTAssertThrowsError(try PassthroughArgs.parse(["-fib"])) + } +} + diff --git a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift index a25847a8b..63bb5fa8f 100644 --- a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift @@ -137,7 +137,7 @@ fileprivate struct Inner: ParsableCommand { @Flag var verbose: Bool = false - @Argument(parsing: .unconditionalRemaining) + @Argument(parsing: .captureForPassthrough) var files: [String] = [] } @@ -325,96 +325,6 @@ extension RepeatingEndToEndTests { // MARK: - -fileprivate struct Foozle: ParsableArguments { - @Flag var verbose: Bool = false - @Flag(name: .customShort("f")) var useFiles: Bool = false - @Flag(name: .customShort("i")) var useStandardInput: Bool = false - @Option var config = "debug" - @Argument(parsing: .unconditionalRemaining) var names: [String] = [] -} - -extension RepeatingEndToEndTests { - func testParsing_repeatingUnconditionalArgument() throws { - AssertParse(Foozle.self, []) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertEqual(foozle.names, []) - } - - AssertParse(Foozle.self, ["--other"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertEqual(foozle.names, ["--other"]) - } - - AssertParse(Foozle.self, ["--verbose", "one", "two", "three"]) { foozle in - XCTAssertTrue(foozle.verbose) - XCTAssertEqual(foozle.names, ["one", "two", "three"]) - } - - AssertParse(Foozle.self, ["one", "two", "three", "--other", "--verbose"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertEqual(foozle.names, ["one", "two", "three", "--other", "--verbose"]) - } - - AssertParse(Foozle.self, ["--verbose", "--other", "one", "two", "three"]) { foozle in - XCTAssertTrue(foozle.verbose) - XCTAssertEqual(foozle.names, ["--other", "one", "two", "three"]) - } - - AssertParse(Foozle.self, ["--verbose", "--other", "one", "--", "two", "three"]) { foozle in - XCTAssertTrue(foozle.verbose) - XCTAssertEqual(foozle.names, ["--other", "one", "--", "two", "three"]) - } - - AssertParse(Foozle.self, ["--other", "one", "--", "two", "three", "--verbose"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertEqual(foozle.names, ["--other", "one", "--", "two", "three", "--verbose"]) - } - - AssertParse(Foozle.self, ["--", "--verbose", "--other", "one", "two", "three"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertEqual(foozle.names, ["--", "--verbose", "--other", "one", "two", "three"]) - } - - AssertParse(Foozle.self, ["-one", "-two", "three"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertFalse(foozle.useFiles) - XCTAssertFalse(foozle.useStandardInput) - XCTAssertEqual(foozle.names, ["-one", "-two", "three"]) - } - - AssertParse(Foozle.self, ["--config", "release", "one", "two", "--config", "debug"]) { foozle in - XCTAssertEqual(foozle.config, "release") - XCTAssertEqual(foozle.names, ["one", "two", "--config", "debug"]) - } - - AssertParse(Foozle.self, ["--config", "release", "--config", "debug", "one", "two"]) { foozle in - XCTAssertEqual(foozle.config, "debug") - XCTAssertEqual(foozle.names, ["one", "two"]) - } - - AssertParse(Foozle.self, ["-if", "-one", "-two", "three"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertTrue(foozle.useFiles) - XCTAssertTrue(foozle.useStandardInput) - XCTAssertEqual(foozle.names, ["-one", "-two", "three"]) - } - - AssertParse(Foozle.self, ["-one", "-two", "-three", "-if"]) { foozle in - XCTAssertFalse(foozle.verbose) - XCTAssertFalse(foozle.useFiles) - XCTAssertFalse(foozle.useStandardInput) - XCTAssertEqual(foozle.names, ["-one", "-two", "-three", "-if"]) - } - } - - func testParsing_repeatingUnconditionalArgument_Fails() throws { - // Only partially matches the `-fob` argument - XCTAssertThrowsError(try Foozle.parse(["-fib"])) - } -} - -// MARK: - - struct PerformanceTest: ParsableCommand { @Option(name: .short) var bundleIdentifiers: [String] = [] From 543522cf4b9f9b48e00f9cd886014edeaa631fbc Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Fri, 7 Oct 2022 13:34:14 -0500 Subject: [PATCH 4/4] Improve comments --- Sources/ArgumentParser/Parsing/ArgumentDefinition.swift | 3 ++- Sources/ArgumentParser/Parsing/ArgumentSet.swift | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index 175af54c2..bdbda9a26 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -94,7 +94,8 @@ struct ArgumentDefinition { /// Collect all the elements after the terminator, preventing them from /// appearing in any other position. case postTerminator - /// Collect all unused inputs after the + /// Collect all unused inputs once recognized arguments/options/flags have + /// been parsed. case allUnrecognized } diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 4375719f9..6adfebf99 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -448,8 +448,9 @@ extension ArgumentSet { ) throws { var endOfInput = unusedInput.elements.endIndex - // Check for a post-terminator argument, and if so, collect all post- - // terminator args. + // If this argument set includes a definition that should collect all the + // post-terminator inputs, capture them before trying to fill other + // `@Argument` definitions. if let postTerminatorArg = self.first(where: { def in def.isRepeatingPositional && def.parsingStrategy == .postTerminator }),