Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add two new ArgumentArrayParsingStrategy options #496

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
9 changes: 7 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,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
natecook1000 marked this conversation as resolved.
Show resolved Hide resolved
case allUnrecognized
}

var kind: Kind
Expand Down
84 changes: 61 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,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[..<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