Skip to content

Commit

Permalink
Add two new ArgumentArrayParsingStrategy options (#496)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
natecook1000 committed Oct 7, 2022
1 parent b80fb05 commit a8b48bc
Show file tree
Hide file tree
Showing 8 changed files with 470 additions and 144 deletions.
188 changes: 163 additions & 25 deletions Sources/ArgumentParser/Parsable Properties/Argument.swift
Expand Up @@ -104,51 +104,189 @@ 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 `<name>` 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 `<name>` 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)
}

/// 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
/// 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:
/// You can use the `captureForPassthrough` parsing strategy if you need to
/// capture a user's input to manually pass it unchanged to another command.
///
/// struct Options: ParsableArguments {
/// @Flag
/// var verbose: Bool = false
/// 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.
///
/// @Argument(parsing: .unconditionalRemaining)
/// 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 `remaining` 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 {
/// 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)
}

@available(*, deprecated, renamed: "captureForPassthrough")
public static var unconditionalRemaining: ArgumentArrayParsingStrategy {
.captureForPassthrough
}
}

// MARK: - @Argument T: ExpressibleByArgument Initializers
Expand Down
10 changes: 8 additions & 2 deletions Sources/ArgumentParser/Parsing/ArgumentDefinition.swift
Expand Up @@ -80,8 +80,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
Expand All @@ -91,6 +91,12 @@ 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 once recognized arguments/options/flags have
/// been parsed.
case allUnrecognized
}

var kind: Kind
Expand Down
85 changes: 62 additions & 23 deletions Sources/ArgumentParser/Parsing/ArgumentSet.swift
Expand Up @@ -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
}
}

Expand Down Expand Up @@ -442,49 +446,84 @@ 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()
// 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
}),
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[..<endOfInput].filter {
$0.index.subIndex == .complete
}[...]
guard !argumentStack.isEmpty else { return }

/// Pops arguments off the stack until the next valid value. Skips over
/// dash-prefixed inputs unless `unconditional` is `true`.
func next(unconditional: Bool) -> 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)
}
}
}
}
4 changes: 3 additions & 1 deletion Sources/ArgumentParser/Parsing/SplitArguments.swift
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Tests/ArgumentParserEndToEndTests/CMakeLists.txt
Expand Up @@ -11,6 +11,7 @@ add_library(EndToEndTests
PositionalEndToEndTests.swift
RawRepresentableEndToEndTests.swift
RepeatingEndToEndTests.swift
RepeatingEndToEndTests+ParsingStrategy.swift
ShortNameEndToEndTests.swift
SimpleEndToEndTests.swift
SingleValueParsingStrategyTests.swift
Expand Down
Expand Up @@ -91,15 +91,15 @@ extension DefaultSubcommandEndToEndTests {
@OptionGroup var options: CommonOptions
@Argument var pluginName: String

@Argument(parsing: .unconditionalRemaining)
@Argument(parsing: .captureForPassthrough)
var pluginArguments: [String] = []
}

fileprivate struct NonDefault: ParsableCommand {
@OptionGroup var options: CommonOptions
@Argument var pluginName: String

@Argument(parsing: .unconditionalRemaining)
@Argument(parsing: .captureForPassthrough)
var pluginArguments: [String] = []
}

Expand Down

0 comments on commit a8b48bc

Please sign in to comment.