From e47ac3780ade38b455dd950d4438b72ceac7e0a1 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 31 Aug 2020 13:36:15 -0500 Subject: [PATCH 1/5] Convert some linear operations to constant time --- .../Parsing/ArgumentDecoder.swift | 2 +- .../ArgumentParser/Parsing/ArgumentSet.swift | 14 +- .../Parsing/CommandParser.swift | 14 +- .../ArgumentParser/Parsing/ParsedValues.swift | 14 +- .../Parsing/SplitArguments.swift | 280 +++-- .../ShortNameEndToEndTests.swift | 10 +- .../SplitArgumentTests.swift | 1118 ++++++++--------- 7 files changed, 759 insertions(+), 693 deletions(-) diff --git a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift index a64552e78..09ddde45d 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDecoder.swift @@ -35,7 +35,7 @@ final class ArgumentDecoder: Decoder { self.usedOrigins = InputOrigin() // Mark the terminator position(s) as used: - values.elements.filter { $0.key == .terminator }.forEach { + values.elements.values.filter { $0.key == .terminator }.forEach { usedOrigins.formUnion($0.inputOrigin) } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 334a47d1f..40e84d689 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -316,18 +316,20 @@ extension ArgumentSet { } } - var result = ParsedValues(elements: [], originalInput: all.originalInput) - var usedOrigins = InputOrigin() + var result = ParsedValues(elements: [:], originalInput: all.originalInput) + var allUsedOrigins = InputOrigin() try setInitialValues(into: &result) // Loop over all arguments: while let (origin, next) = inputArguments.popNext() { + var usedOrigins = InputOrigin() defer { inputArguments.removeAll(in: usedOrigins) + allUsedOrigins.formUnion(usedOrigins) } - switch next { + switch next.value { case .value: // We'll parse positional values later. break @@ -359,9 +361,9 @@ extension ArgumentSet { // We have parsed all non-positional values at this point. // Next: parse / consume the positional values. var unusedArguments = all - unusedArguments.removeAll(in: usedOrigins) + unusedArguments.removeAll(in: allUsedOrigins) try parsePositionalValues(from: unusedArguments, into: &result) - + return result } } @@ -407,7 +409,7 @@ extension ArgumentSet { var argumentStack = unusedInput.elements.filter { $0.index.subIndex == .complete }.map { - (InputOrigin.Element.argumentIndex($0.index), $0.element) + (InputOrigin.Element.argumentIndex($0.index), $0) }[...] guard !argumentStack.isEmpty else { return } diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index 8e2d06feb..8875772ba 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -97,9 +97,9 @@ extension CommandParser { // We should have used up all arguments at this point: guard !split.containsNonTerminatorArguments else { // Check if one of the arguments is an unknown option - for (index, element) in split.elements { - if case .option(let argument) = element { - throw ParserError.unknownOption(InputOrigin.Element.argumentIndex(index), argument.name) + for element in split.elements { + if case .option(let argument) = element.value { + throw ParserError.unknownOption(InputOrigin.Element.argumentIndex(element.index), argument.name) } } @@ -296,12 +296,12 @@ extension CommandParser { // Generate the argument set and parse the argument to find in the set let argset = ArgumentSet(current.element) - let (_, parsedArgument) = try! parseIndividualArg(argToMatch, at: 0).first! + let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first! // Look up the specified argument and retrieve its custom completion function let completionFunction: ([String]) -> [String] - switch parsedArgument { + switch parsedArgument.value { case .option(let parsed): guard let matchedArgument = argset.first(matching: parsed), case .custom(let f) = matchedArgument.completion.kind @@ -362,7 +362,7 @@ extension CommandParser { extension SplitArguments { func contains(_ needle: Name) -> Bool { self.elements.contains { - switch $0.element { + switch $0.value { case .option(.name(let name)), .option(.nameWithValue(let name, _)): return name == needle @@ -374,7 +374,7 @@ extension SplitArguments { func contains(anyOf names: [Name]) -> Bool { self.elements.contains { - switch $0.element { + switch $0.value { case .option(.name(let name)), .option(.nameWithValue(let name, _)): return names.contains(name) diff --git a/Sources/ArgumentParser/Parsing/ParsedValues.swift b/Sources/ArgumentParser/Parsing/ParsedValues.swift index 839f15cbc..f490c4d43 100644 --- a/Sources/ArgumentParser/Parsing/ParsedValues.swift +++ b/Sources/ArgumentParser/Parsing/ParsedValues.swift @@ -36,7 +36,7 @@ struct ParsedValues { } /// These are the parsed key-value pairs. - var elements: [Element] = [] + var elements: [InputKey: Element] = [:] /// This is the *original* array of arguments that this was parsed from. /// @@ -50,20 +50,20 @@ extension ParsedValues { } mutating func set(_ element: Element) { - if let index = elements.firstIndex(where: { $0.key == element.key }) { + if let e = elements[element.key] { // Merge the source values. We need to keep track // of any previous source indexes we have used for // this key. - var e = element - e.inputOrigin.formUnion(elements[index].inputOrigin) - elements[index] = e + var element = element + element.inputOrigin.formUnion(e.inputOrigin) + elements[element.key] = element } else { - elements.append(element) + elements[element.key] = element } } func element(forKey key: InputKey) -> Element? { - return elements.first(where: { $0.key == key }) + elements[key] } mutating func update(forKey key: InputKey, inputOrigin: InputOrigin, initial: A, closure: (inout A) -> Void) { diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index ab2218d3b..6c77528b7 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -79,11 +79,37 @@ enum ParsedArgument: Equatable, CustomStringConvertible { /// arguments `["--foo", "bar"]` would be parsed into /// `[.option(.name(.long("foo"))), .value("bar")]`. struct SplitArguments { - enum Element: Equatable { - case option(ParsedArgument) - case value(String) - /// The `--` marker - case terminator + struct Element: Equatable { + enum Value: Equatable { + case option(ParsedArgument) + case value(String) + /// The `--` marker + case terminator + + var valueString: String? { + switch self { + case .value(let str): + return str + case .option, .terminator: + return nil + } + } + } + + var value: Value + var index: Index + + static func option(_ arg: ParsedArgument, index: Index) -> Element { + Element(value: .option(arg), index: index) + } + + static func value(_ str: String, index: Index) -> Element { + Element(value: .value(str), index: index) + } + + static func terminator(index: Index) -> Element { + Element(value: .terminator, index: index) + } } /// The index into the (original) input. @@ -96,10 +122,6 @@ struct SplitArguments { static func <(lhs: InputIndex, rhs: InputIndex) -> Bool { lhs.rawValue < rhs.rawValue } - - var next: InputIndex { - InputIndex(rawValue: rawValue + 1) - } } /// The index into an input index position. @@ -138,13 +160,18 @@ struct SplitArguments { var subIndex: SubIndex = .complete } - var elements: [(index: Index, element: Element)] + var _elements: [Element] = [] var originalInput: [String] + var firstUnused: Int = 0 + + var elements: ArraySlice { + _elements[firstUnused...] + } } extension SplitArguments.Element: CustomDebugStringConvertible { var debugDescription: String { - switch self { + switch value { case .option(.name(let name)): return name.synopsisString case .option(.nameWithValue(let name, let value)): @@ -170,16 +197,16 @@ extension SplitArguments: CustomStringConvertible { var description: String { guard !isEmpty else { return "" } return elements - .map { (index, element) -> String in - switch element { + .map { element -> String in + switch element.value { case .option(.name(let name)): - return "[\(index)] \(name.synopsisString)" + return "[\(element.index)] \(name.synopsisString)" case .option(.nameWithValue(let name, let value)): - return "[\(index)] \(name.synopsisString)='\(value)'" + return "[\(element.index)] \(name.synopsisString)='\(value)'" case .value(let value): - return "[\(index)] '\(value)'" + return "[\(element.index)] '\(value)'" case .terminator: - return "[\(index)] --" + return "[\(element.index)] --" } } .joined(separator: " ") @@ -188,11 +215,18 @@ extension SplitArguments: CustomStringConvertible { extension SplitArguments.Element { var isValue: Bool { - switch self { + switch value { case .value: return true case .option, .terminator: return false } } + + var isTerminator: Bool { + switch value { + case .terminator: return true + case .option, .value: return false + } + } } extension SplitArguments { @@ -201,21 +235,16 @@ extension SplitArguments { elements.isEmpty } - /// `true` if the arguments are empty, or if the only remaining argument is the `--` terminator. + /// `false` if the arguments are empty, or if the only remaining argument is + /// the `--` terminator. var containsNonTerminatorArguments: Bool { if elements.isEmpty { return false } if elements.count > 1 { return true } - if case .terminator = elements[0].element { return false } + if elements.first?.isTerminator == true { return false } else { return true } } - subscript(position: Index) -> Element? { - return elements.first { - $0.0 == position - }?.1 - } - /// Returns the original input string at the given origin, or `nil` if /// `origin` is a sub-index. func originalInput(at origin: InputOrigin.Element) -> String? { @@ -225,15 +254,28 @@ extension SplitArguments { return originalInput[index.inputIndex.rawValue] } + /// Returns the position in `elements` of the given input origin. + mutating func position(of origin: InputOrigin.Element) -> Int? { + guard case let .argumentIndex(index) = origin else { return nil } + return elements.firstIndex(where: { $0.index == index }) + } + + /// Returns the position in `elements` of the first element after the given + /// input origin. + mutating func position(after origin: InputOrigin.Element) -> Int? { + guard case let .argumentIndex(index) = origin else { return nil } + return elements.firstIndex(where: { $0.index > index }) + } + mutating func popNext() -> (InputOrigin.Element, Element)? { - guard let (index, value) = elements.first else { return nil } - elements.remove(at: 0) - return (.argumentIndex(index), value) + guard let element = elements.first else { return nil } + removeFirst() + return (.argumentIndex(element.index), element) } func peekNext() -> (InputOrigin.Element, Element)? { - guard let (index, value) = elements.first else { return nil } - return (.argumentIndex(index), value) + guard let element = elements.first else { return nil } + return (.argumentIndex(element.index), element) } /// Pops the element immediately after the given index, if it is a `.value`. @@ -247,17 +289,16 @@ extension SplitArguments { // `origin` in the input string. We look at the input index so that // packed short options can be followed, in order, by their values. // e.g. "-fn f-value n-value" - guard - case .argumentIndex(let after) = origin, - let elementIndex = elements.firstIndex(where: { $0.0.inputIndex > after.inputIndex }) + guard let start = position(after: origin), + let elementIndex = elements[start...].firstIndex(where: { $0.index.subIndex == .complete }) else { return nil } // Only succeed if the element is a value (not prefixed with a dash) - guard case .value(let value) = elements[elementIndex].1 + guard case .value(let value) = elements[elementIndex].value else { return nil } - - let matchedArgumentIndex = elements[elementIndex].0 - elements.remove(at: elementIndex) + + defer { remove(at: elementIndex) } + let matchedArgumentIndex = elements[elementIndex].index return (.argumentIndex(matchedArgumentIndex), value) } @@ -265,16 +306,11 @@ extension SplitArguments { /// /// This is used to get the next value in `-f -b name` where `name` is the value of `-f`. mutating func popNextValue(after origin: InputOrigin.Element) -> (InputOrigin.Element, String)? { - guard case .argumentIndex(let after) = origin else { return nil } - for (index, element) in elements.enumerated() { - guard - element.0 > after, - case .value(let value) = element.1 - else { continue } - elements.remove(at: index) - return (.argumentIndex(element.0), value) - } - return nil + guard let start = position(after: origin) else { return nil } + guard let resultIndex = elements[start...].firstIndex(where: { $0.isValue }) else { return nil } + + defer { remove(at: resultIndex) } + return (.argumentIndex(elements[resultIndex].index), elements[resultIndex].value.valueString!) } /// Pops the element after the given index as a value. @@ -285,15 +321,14 @@ extension SplitArguments { /// For an input such as `--a --b foo`, if passed the origin of `--a`, /// this will first pop the value `--b`, then the value `foo`. mutating func popNextElementAsValue(after origin: InputOrigin.Element) -> (InputOrigin.Element, String)? { - guard case .argumentIndex(let after) = origin else { return nil } + guard let start = position(after: origin) else { return nil } // Elements are sorted by their `InputIndex`. Find the first `InputIndex` // after `origin`: - guard let unconditionalIndex = elements.first(where: { (index, _) in index.inputIndex > after.inputIndex })?.0.inputIndex else { return nil } - let nextIndex = Index(inputIndex: unconditionalIndex, subIndex: .complete) + guard let nextIndex = elements[start...].first(where: { $0.index.subIndex == .complete })?.index else { return nil } // Remove all elements with this `InputIndex`: remove(at: nextIndex) // Return the original input - return (.argumentIndex(nextIndex), originalInput[unconditionalIndex.rawValue]) + return (.argumentIndex(nextIndex), originalInput[nextIndex.inputIndex.rawValue]) } /// Pops the next element if it is a value. @@ -301,12 +336,9 @@ extension SplitArguments { /// If the current elements are `--b foo`, this will return `nil`. If the /// elements are `foo --b`, this will return the value `foo`. mutating func popNextElementIfValue() -> (InputOrigin.Element, String)? { - guard - let (index, element) = elements.first, - case .value(let value) = element - else { return nil } - elements.remove(at: 0) - return (.argumentIndex(index), value) + guard let element = elements.first, element.isValue else { return nil } + removeFirst() + return (.argumentIndex(element.index), element.value.valueString!) } /// Finds and "pops" the next element that is a value. @@ -314,30 +346,53 @@ extension SplitArguments { /// If the current elements are `--a --b foo`, this will remove and return /// `foo`. mutating func popNextValue() -> (Index, String)? { - guard let idx = elements.firstIndex(where: { - switch $0.element { - case .option: return false - case .value: return true - case .terminator: return false - } - }) else { return nil } + guard let idx = elements.firstIndex(where: { $0.isValue }) + else { return nil } let e = elements[idx] - elements.remove(at: idx) - guard case let .value(v) = e.element else { fatalError() } - return (e.index, v) + remove(at: idx) + return (e.index, e.value.valueString!) } + /// Finds and returns the next element that is a value. func peekNextValue() -> (Index, String)? { - guard let idx = elements.firstIndex(where: { - switch $0.element { - case .option: return false - case .value: return true - case .terminator: return false - } - }) else { return nil } + guard let idx = elements.firstIndex(where: { $0.isValue }) + else { return nil } let e = elements[idx] - guard case let .value(v) = e.element else { fatalError() } - return (e.index, v) + return (e.index, e.value.valueString!) + } + + /// Removes the first element in `elements`. + mutating func removeFirst() { + firstUnused += 1 + } + + /// Removes the element at the given position. + mutating func remove(at position: Int) { + guard position > firstUnused else { + return + } + + // This leaves duplicates of still to-be-used arguments in the unused + // portion of the _elements array. + for i in (firstUnused..) { + var lo = subrange.startIndex + var hi = subrange.endIndex + + // This leaves duplicates of still to-be-used arguments in the unused + // portion of the _elements array. + while lo > firstUnused { + hi -= 1 + lo -= 1 + _elements[hi] = _elements[lo] + } + firstUnused += subrange.count } /// Removes the element(s) at the given `Index`. @@ -350,18 +405,26 @@ extension SplitArguments { /// is removed, that will remove the _long with short dash_ as well. Likewise, if the /// _long with short dash_ is removed, that will remove both of the _short_ elements. mutating func remove(at position: Index) { + guard var start = elements.firstIndex(where: { $0.index.inputIndex == position.inputIndex }) + else { return } + if case .complete = position.subIndex { // When removing a `.complete`, we need to remove _all_ // elements that have the same `InputIndex`. - elements.removeAll { (index, _) -> Bool in - index.inputIndex == position.inputIndex - } + let end = elements[start...].firstIndex(where: { $0.index.inputIndex != position.inputIndex }) + ?? elements.endIndex + + remove(subrange: start.. Bool in - index == position || - ((index.inputIndex == position.inputIndex) && (index.subIndex == .complete)) + if elements[start].index.subIndex == .complete { + remove(at: start) + start += 1 + } + + if let sub = elements[start...].firstIndex(where: { $0.index == position }) { + remove(at: sub) } } } @@ -383,48 +446,48 @@ extension SplitArguments { func coalescedExtraElements() -> [(InputOrigin, String)] { let completeIndexes: [InputIndex] = elements .compactMap { - guard case .complete = $0.0.subIndex else { return nil } - return $0.0.inputIndex + guard case .complete = $0.index.subIndex else { return nil } + return $0.index.inputIndex } // Now return all elements that are either: // 1) `.complete` // 2) `.sub` but not in `completeIndexes` - let extraElements: [(Index, Element)] = elements.filter { - switch $0.0.subIndex { + let extraElements = elements.filter { + switch $0.index.subIndex { case .complete: return true case .sub: - return !completeIndexes.contains($0.0.inputIndex) + return !completeIndexes.contains($0.index.inputIndex) } } - return extraElements.map { index, element -> (InputOrigin, String) in + return extraElements.map { element -> (InputOrigin, String) in let input: String - switch index.subIndex { + switch element.index.subIndex { case .complete: - input = originalInput[index.inputIndex.rawValue] + input = originalInput[element.index.inputIndex.rawValue] case .sub: - if case .option(let option) = element { + if case .option(let option) = element.value { input = String(describing: option) } else { // Odd case. Fall back to entire input at that index: - input = originalInput[index.inputIndex.rawValue] + input = originalInput[element.index.inputIndex.rawValue] } } - return (.init(argumentIndex: index), input) + return (.init(argumentIndex: element.index), input) } } } -func parseIndividualArg(_ arg: String, at position: Int) throws -> [(SplitArguments.Index, SplitArguments.Element)] { +func parseIndividualArg(_ arg: String, at position: Int) throws -> [SplitArguments.Element] { let index = SplitArguments.Index(inputIndex: .init(rawValue: position)) if let nonDashIdx = arg.firstIndex(where: { $0 != "-" }) { let dashCount = arg.distance(from: arg.startIndex, to: nonDashIdx) let remainder = arg[nonDashIdx.. [(SplitArgume switch parts.count { case 0: // This is a '-name=value' style argument - return [(index, .option(parsed))] + return [.option(parsed, index: index)] case 1: // This is a single short '-n' style argument - return [(index, .option(.name(.short(remainder.first!))))] + return [.option(.name(.short(remainder.first!)), index: index)] default: - var result: [(SplitArguments.Index, SplitArguments.Element)] = [(index, .option(parsed))] + var result: [SplitArguments.Element] = [.option(parsed, index: index)] for (sub, a) in parts { var i = index i.subIndex = .sub(sub) - result.append((i, .option(a))) + result.append(.option(a, index: i)) } return result } case 2: - return [(index, .option(ParsedArgument(arg)))] + return [.option(ParsedArgument(arg), index: index)] default: throw ParserError.invalidOption(arg) } @@ -458,10 +521,10 @@ func parseIndividualArg(_ arg: String, at position: Int) throws -> [(SplitArgume switch dashCount { case 0, 1: // Empty string or single dash - return [(index, .value(arg))] + return [.value(arg, index: index)] case 2: // We found the 1st "--". All the remaining are positional. - return [(index, .terminator)] + return [.terminator(index: index)] default: throw ParserError.invalidOption(arg) } @@ -473,24 +536,25 @@ extension SplitArguments { /// /// - Parameter arguments: The input from the command line. init(arguments: [String]) throws { - self.init(elements: [], originalInput: arguments) + self.init(originalInput: arguments) var position = 0 - var args = arguments[arguments.startIndex.. Date: Mon, 31 Aug 2020 13:37:14 -0500 Subject: [PATCH 2/5] Temporary test command for performance testing --- Examples/repeat/main.swift | 195 +++++++++++++++++++++++++++++++------ 1 file changed, 167 insertions(+), 28 deletions(-) diff --git a/Examples/repeat/main.swift b/Examples/repeat/main.swift index bc5e8b1cc..ed2ef2a77 100644 --- a/Examples/repeat/main.swift +++ b/Examples/repeat/main.swift @@ -1,37 +1,176 @@ -//===----------------------------------------------------------*- swift -*-===// -// -// This source file is part of the Swift Argument Parser open source project -// -// Copyright (c) 2020 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 ArgumentParser -struct Repeat: ParsableCommand { - @Option(help: "The number of times to repeat 'phrase'.") - var count: Int? +struct One: ParsableArguments { + @Option var optionOneOne: String = "" + @Option var optionOneTwo: String = "" + @Option var optionOneThree: String = "" + @Option var optionOneFour: String = "" + @Option var optionOneFive: String = "" + @Option var optionOneSix: String = "" + @Option var optionOneSeven: String = "" + @Option var optionOneEight: String = "" + @Option var optionOneNine: String = "" + @Option var optionOneTen: String = "" +} + +struct Two: ParsableArguments { + @Option var optionTwoOne: String = "" + @Option var optionTwoTwo: String = "" + @Option var optionTwoThree: String = "" + @Option var optionTwoFour: String = "" + @Option var optionTwoFive: String = "" + @Option var optionTwoSix: String = "" + @Option var optionTwoSeven: String = "" + @Option var optionTwoEight: String = "" + @Option var optionTwoNine: String = "" + @Option var optionTwoTen: String = "" +} + +struct Three: ParsableArguments { + @Option var optionThreeOne: String = "" + @Option var optionThreeTwo: String = "" + @Option var optionThreeThree: String = "" + @Option var optionThreeFour: String = "" + @Option var optionThreeFive: String = "" + @Option var optionThreeSix: String = "" + @Option var optionThreeSeven: String = "" + @Option var optionThreeEight: String = "" + @Option var optionThreeNine: String = "" + @Option var optionThreeTen: String = "" +} + +struct Four: ParsableArguments { + @Option var optionFourOne: String = "" + @Option var optionFourTwo: String = "" + @Option var optionFourThree: String = "" + @Option var optionFourFour: String = "" + @Option var optionFourFive: String = "" + @Option var optionFourSix: String = "" + @Option var optionFourSeven: String = "" + @Option var optionFourEight: String = "" + @Option var optionFourNine: String = "" + @Option var optionFourTen: String = "" +} - @Flag(help: "Include a counter with each repetition.") - var includeCounter = false +struct Five: ParsableArguments { + @Option var optionFiveOne: String = "" + @Option var optionFiveTwo: String = "" + @Option var optionFiveThree: String = "" + @Option var optionFiveFour: String = "" + @Option var optionFiveFive: String = "" + @Option var optionFiveSix: String = "" + @Option var optionFiveSeven: String = "" + @Option var optionFiveEight: String = "" + @Option var optionFiveNine: String = "" + @Option var optionFiveTen: String = "" +} - @Argument(help: "The phrase to repeat.") - var phrase: String +struct Six: ParsableArguments { + @Option var optionSixOne: String = "" + @Option var optionSixTwo: String = "" + @Option var optionSixThree: String = "" + @Option var optionSixFour: String = "" + @Option var optionSixFive: String = "" + @Option var optionSixSix: String = "" + @Option var optionSixSeven: String = "" + @Option var optionSixEight: String = "" + @Option var optionSixNine: String = "" + @Option var optionSixTen: String = "" +} - mutating func run() throws { - let repeatCount = count ?? .max +struct Seven: ParsableArguments { + @Option var optionSevenOne: String = "" + @Option var optionSevenTwo: String = "" + @Option var optionSevenThree: String = "" + @Option var optionSevenFour: String = "" + @Option var optionSevenFive: String = "" + @Option var optionSevenSix: String = "" + @Option var optionSevenSeven: String = "" + @Option var optionSevenEight: String = "" + @Option var optionSevenNine: String = "" + @Option var optionSevenTen: String = "" +} - for i in 1...repeatCount { - if includeCounter { - print("\(i): \(phrase)") - } else { - print(phrase) - } - } +struct Eight: ParsableArguments { + @Option var optionEightOne: String = "" + @Option var optionEightTwo: String = "" + @Option var optionEightThree: String = "" + @Option var optionEightFour: String = "" + @Option var optionEightFive: String = "" + @Option var optionEightSix: String = "" + @Option var optionEightSeven: String = "" + @Option var optionEightEight: String = "" + @Option var optionEightNine: String = "" + @Option var optionEightTen: String = "" +} + +struct Nine: ParsableArguments { + @Option var optionNineOne: String = "" + @Option var optionNineTwo: String = "" + @Option var optionNineThree: String = "" + @Option var optionNineFour: String = "" + @Option var optionNineFive: String = "" + @Option var optionNineSix: String = "" + @Option var optionNineSeven: String = "" + @Option var optionNineEight: String = "" + @Option var optionNineNine: String = "" + @Option var optionNineTen: String = "" +} + +struct Ten: ParsableArguments { + @Option var optionTenOne: String = "" + @Option var optionTenTwo: String = "" + @Option var optionTenThree: String = "" + @Option var optionTenFour: String = "" + @Option var optionTenFive: String = "" + @Option var optionTenSix: String = "" + @Option var optionTenSeven: String = "" + @Option var optionTenEight: String = "" + @Option var optionTenNine: String = "" + @Option var optionTenTen: String = "" +} + +struct Main: ParsableCommand { + @OptionGroup var groupOne: One + @OptionGroup var groupTwo: Two + @OptionGroup var groupThree: Three + @OptionGroup var groupFour: Four + @OptionGroup var groupFive: Five + @OptionGroup var groupSix: Six + @OptionGroup var groupSeven: Seven + @OptionGroup var groupEight: Eight + @OptionGroup var groupNine: Nine + @OptionGroup var groupTen: Ten + + func run() { + dump(self) } } -Repeat.main() +let argCount = Int(CommandLine.arguments.last ?? "") ?? 1 +print("Parsing \(argCount) options") + +let numberWords = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"] +var args: [String] = [] +for _ in 0.. Date: Tue, 1 Sep 2020 03:21:12 -0500 Subject: [PATCH 3/5] Improve SplitArguments docs --- .../Parsing/SplitArguments.swift | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/Sources/ArgumentParser/Parsing/SplitArguments.swift b/Sources/ArgumentParser/Parsing/SplitArguments.swift index 6c77528b7..4316b11c1 100644 --- a/Sources/ArgumentParser/Parsing/SplitArguments.swift +++ b/Sources/ArgumentParser/Parsing/SplitArguments.swift @@ -73,7 +73,7 @@ enum ParsedArgument: Equatable, CustomStringConvertible { } } -/// A parsed version of command-line arguments. +/// A collection of parsed command-line arguments. /// /// This is a flat list of *values* and *options*. E.g. the /// arguments `["--foo", "bar"]` would be parsed into @@ -112,10 +112,10 @@ struct SplitArguments { } } - /// The index into the (original) input. + /// The position of the original input string for an element. /// - /// E.g. for `["--foo", "-vh"]` there are index positions 0 (`--foo`) and - /// 1 (`-vh`). + /// For example, if `originalInput` is `["--foo", "-vh"]`, there are index + /// positions 0 (`--foo`) and 1 (`-vh`). struct InputIndex: RawRepresentable, Hashable, Comparable { var rawValue: Int @@ -124,10 +124,15 @@ struct SplitArguments { } } - /// The index into an input index position. + /// The position within an option for an element. /// - /// E.g. the input `"-vh"` will be split into the elements `-v`, and `-h` - /// each with its own subindex. + /// Single-dash prefixed options can be treated as a whole option or as a + /// group of individual short options. For example, the input `-vh` is split + /// into three elements, with distinct sub-indexes: + /// + /// - `-vh`: `.complete` + /// - `-v`: `.sub(0)` + /// - `-h`: `.sub(1)` enum SubIndex: Hashable, Comparable { case complete case sub(Int) @@ -144,7 +149,7 @@ struct SplitArguments { } } - /// Tracks both the index into the original input and the index into the split arguments (array of elements). + /// An index into the original input and the sub-index of an element. struct Index: Hashable, Comparable { static func < (lhs: SplitArguments.Index, rhs: SplitArguments.Index) -> Bool { if lhs.inputIndex < rhs.inputIndex { @@ -160,10 +165,14 @@ struct SplitArguments { var subIndex: SubIndex = .complete } + /// The parsed arguments. Onl var _elements: [Element] = [] - var originalInput: [String] var firstUnused: Int = 0 + /// The original array of arguments that was used to generate this instance. + var originalInput: [String] + + /// The unused arguments represented by this instance. var elements: ArraySlice { _elements[firstUnused...] } @@ -368,7 +377,7 @@ extension SplitArguments { /// Removes the element at the given position. mutating func remove(at position: Int) { - guard position > firstUnused else { + guard position >= firstUnused else { return } @@ -405,19 +414,32 @@ extension SplitArguments { /// is removed, that will remove the _long with short dash_ as well. Likewise, if the /// _long with short dash_ is removed, that will remove both of the _short_ elements. mutating func remove(at position: Index) { - guard var start = elements.firstIndex(where: { $0.index.inputIndex == position.inputIndex }) - else { return } + guard !isEmpty else { return } + + // Find the first element at the given input index. Since `elements` is + // always sorted by input index, we can leave this method if we see a + // higher value than `position`. + var start = elements.startIndex + while start < elements.endIndex { + if elements[start].index.inputIndex == position.inputIndex { break } + if elements[start].index.inputIndex > position.inputIndex { return } + start += 1 + } if case .complete = position.subIndex { - // When removing a `.complete`, we need to remove _all_ - // elements that have the same `InputIndex`. + // When removing a `.complete` position, we need to remove both the + // complete element and any sub-elements with the same input index. + + // Remove up to the first element where the input index doesn't match. let end = elements[start...].firstIndex(where: { $0.index.inputIndex != position.inputIndex }) ?? elements.endIndex remove(subrange: start.. Date: Tue, 1 Sep 2020 03:52:44 -0500 Subject: [PATCH 4/5] Re-enable split arguments unit test --- .../SplitArgumentTests.swift | 1118 ++++++++--------- 1 file changed, 559 insertions(+), 559 deletions(-) diff --git a/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift b/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift index f460c6ef1..5b1c1c2c9 100644 --- a/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift +++ b/Tests/ArgumentParserUnitTests/SplitArgumentTests.swift @@ -13,562 +13,562 @@ import XCTest @testable import ArgumentParser import ArgumentParserTestHelpers -//extension SplitArguments.InputIndex: ExpressibleByIntegerLiteral { -// public init(integerLiteral value: Int) { -// self.init(rawValue: value) -// } -//} -// -//private func AssertIndexEqual(_ sut: SplitArguments, at index: Int, inputIndex: Int, subIndex: SplitArguments.SubIndex, file: StaticString = #file, line: UInt = #line) { -// guard index < sut.elements.count else { -// XCTFail("Element index \(index) is out of range. sur only has \(sut.elements.count) elements.", file: (file), line: line) -// return -// } -// let splitIndex = sut.elements[index].0 -// let expected = SplitArguments.Index(inputIndex: SplitArguments.InputIndex(rawValue: inputIndex), subIndex: subIndex) -// if splitIndex.inputIndex != expected.inputIndex { -// XCTFail("inputIndex does not match: \(splitIndex.inputIndex.rawValue) != \(expected.inputIndex.rawValue)", file: (file), line: line) -// } -// if splitIndex.subIndex != expected.subIndex { -// XCTFail("inputIndex does not match: \(splitIndex.subIndex) != \(expected.subIndex)", file: (file), line: line) -// } -//} -// -//private func AssertElementEqual(_ sut: SplitArguments, at index: Int, _ element: SplitArguments.Element, file: StaticString = #file, line: UInt = #line) { -// guard index < sut.elements.count else { -// XCTFail("Element index \(index) is out of range. sur only has \(sut.elements.count) elements.", file: (file), line: line) -// return -// } -// XCTAssertEqual(sut.elements[index].1, element, file: (file), line: line) -//} -// -//final class SplitArgumentTests: XCTestCase { -// func testEmpty() throws { -// let sut = try SplitArguments(arguments: []) -// XCTAssertEqual(sut.elements.count, 0) -// XCTAssertEqual(sut.originalInput.count, 0) -// } -// -// func testSingleValue() throws { -// let sut = try SplitArguments(arguments: ["abc"]) -// -// XCTAssertEqual(sut.elements.count, 1) -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .value("abc")) -// -// XCTAssertEqual(sut.originalInput.count, 1) -// XCTAssertEqual(sut.originalInput, ["abc"]) -// } -// -// func testSingleLongOption() throws { -// let sut = try SplitArguments(arguments: ["--abc"]) -// -// XCTAssertEqual(sut.elements.count, 1) -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.long("abc")))) -// -// XCTAssertEqual(sut.originalInput.count, 1) -// XCTAssertEqual(sut.originalInput, ["--abc"]) -// } -// -// func testSingleShortOption() throws { -// let sut = try SplitArguments(arguments: ["-a"]) -// -// XCTAssertEqual(sut.elements.count, 1) -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.short("a")))) -// -// XCTAssertEqual(sut.originalInput.count, 1) -// XCTAssertEqual(sut.originalInput, ["-a"]) -// } -// -// func testSingleLongOptionWithValue() throws { -// let sut = try SplitArguments(arguments: ["--abc=def"]) -// -// XCTAssertEqual(sut.elements.count, 1) -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.nameWithValue(.long("abc"), "def"))) -// -// XCTAssertEqual(sut.originalInput.count, 1) -// XCTAssertEqual(sut.originalInput, ["--abc=def"]) -// } -// -// func testMultipleShortOptionsCombined() throws { -// let sut = try SplitArguments(arguments: ["-abc"]) -// -// XCTAssertEqual(sut.elements.count, 4) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("abc")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) -// AssertElementEqual(sut, at: 1, .option(.name(.short("a")))) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) -// AssertElementEqual(sut, at: 2, .option(.name(.short("b")))) -// -// AssertIndexEqual(sut, at: 3, inputIndex: 0, subIndex: .sub(2)) -// AssertElementEqual(sut, at: 3, .option(.name(.short("c")))) -// -// XCTAssertEqual(sut.originalInput.count, 1) -// XCTAssertEqual(sut.originalInput, ["-abc"]) -// } -// -// func testSingleLongOptionWithValueAndSingleDash() throws { -// let sut = try SplitArguments(arguments: ["-abc=def"]) -// -// XCTAssertEqual(sut.elements.count, 1) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.nameWithValue(.longWithSingleDash("abc"), "def"))) -// -// XCTAssertEqual(sut.originalInput.count, 1) -// XCTAssertEqual(sut.originalInput, ["-abc=def"]) -// } -//} -// -//extension SplitArgumentTests { -// func testMultipleValues() throws { -// let sut = try SplitArguments(arguments: ["abc", "x", "1234"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .value("abc")) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .value("x")) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .value("1234")) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["abc", "x", "1234"]) -// } -// -// func testMultipleLongOptions() throws { -// let sut = try SplitArguments(arguments: ["--d", "--1", "--abc-def"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.long("d")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .option(.name(.long("1")))) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .option(.name(.long("abc-def")))) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["--d", "--1", "--abc-def"]) -// } -// -// func testMultipleShortOptions() throws { -// let sut = try SplitArguments(arguments: ["-x", "-y", "-z"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.short("x")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .option(.name(.short("y")))) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .option(.name(.short("z")))) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["-x", "-y", "-z"]) -// } -// -// func testMultipleShortOptionsCombined_2() throws { -// let sut = try SplitArguments(arguments: ["-bc", "-fv", "-a"]) -// -// XCTAssertEqual(sut.elements.count, 7) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("bc")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) -// AssertElementEqual(sut, at: 1, .option(.name(.short("b")))) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) -// AssertElementEqual(sut, at: 2, .option(.name(.short("c")))) -// -// AssertIndexEqual(sut, at: 3, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 3, .option(.name(.longWithSingleDash("fv")))) -// -// AssertIndexEqual(sut, at: 4, inputIndex: 1, subIndex: .sub(0)) -// AssertElementEqual(sut, at: 4, .option(.name(.short("f")))) -// -// AssertIndexEqual(sut, at: 5, inputIndex: 1, subIndex: .sub(1)) -// AssertElementEqual(sut, at: 5, .option(.name(.short("v")))) -// -// AssertIndexEqual(sut, at: 6, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 6, .option(.name(.short("a")))) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["-bc", "-fv", "-a"]) -// } -//} -// -//extension SplitArgumentTests { -// func testMixed_1() throws { -// let sut = try SplitArguments(arguments: ["-x", "abc", "--foo", "1234", "-zz"]) -// -// XCTAssertEqual(sut.elements.count, 7) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.short("x")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .value("abc")) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .option(.name(.long("foo")))) -// -// AssertIndexEqual(sut, at: 3, inputIndex: 3, subIndex: .complete) -// AssertElementEqual(sut, at: 3, .value("1234")) -// -// AssertIndexEqual(sut, at: 4, inputIndex: 4, subIndex: .complete) -// AssertElementEqual(sut, at: 4, .option(.name(.longWithSingleDash("zz")))) -// -// AssertIndexEqual(sut, at: 5, inputIndex: 4, subIndex: .sub(0)) -// AssertElementEqual(sut, at: 5, .option(.name(.short("z")))) -// -// AssertIndexEqual(sut, at: 6, inputIndex: 4, subIndex: .sub(1)) -// AssertElementEqual(sut, at: 6, .option(.name(.short("z")))) -// -// XCTAssertEqual(sut.originalInput.count, 5) -// XCTAssertEqual(sut.originalInput, ["-x", "abc", "--foo", "1234", "-zz"]) -// } -// -// func testMixed_2() throws { -// let sut = try SplitArguments(arguments: ["1234", "-zz", "abc", "-x", "--foo"]) -// -// XCTAssertEqual(sut.elements.count, 7) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .value("1234")) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .option(.name(.longWithSingleDash("zz")))) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 1, subIndex: .sub(0)) -// AssertElementEqual(sut, at: 2, .option(.name(.short("z")))) -// -// AssertIndexEqual(sut, at: 3, inputIndex: 1, subIndex: .sub(1)) -// AssertElementEqual(sut, at: 3, .option(.name(.short("z")))) -// -// AssertIndexEqual(sut, at: 4, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 4, .value("abc")) -// -// AssertIndexEqual(sut, at: 5, inputIndex: 3, subIndex: .complete) -// AssertElementEqual(sut, at: 5, .option(.name(.short("x")))) -// -// AssertIndexEqual(sut, at: 6, inputIndex: 4, subIndex: .complete) -// AssertElementEqual(sut, at: 6, .option(.name(.long("foo")))) -// -// XCTAssertEqual(sut.originalInput.count, 5) -// XCTAssertEqual(sut.originalInput, ["1234", "-zz", "abc", "-x", "--foo"]) -// } -// -// func testTerminator_1() throws { -// let sut = try SplitArguments(arguments: ["--foo", "--", "--bar"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .terminator) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .value("--bar")) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["--foo", "--", "--bar"]) -// } -// -// func testTerminator_2() throws { -// let sut = try SplitArguments(arguments: ["--foo", "--", "bar"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .terminator) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .value("bar")) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["--foo", "--", "bar"]) -// } -// -// func testTerminator_3() throws { -// let sut = try SplitArguments(arguments: ["--foo", "--", "--bar=baz"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .terminator) -// -// AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) -// AssertElementEqual(sut, at: 2, .value("--bar=baz")) -// -// XCTAssertEqual(sut.originalInput.count, 3) -// XCTAssertEqual(sut.originalInput, ["--foo", "--", "--bar=baz"]) -// } -// -// func testTerminatorAtTheEnd() throws { -// let sut = try SplitArguments(arguments: ["--foo", "--"]) -// -// XCTAssertEqual(sut.elements.count, 2) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .terminator) -// -// XCTAssertEqual(sut.originalInput.count, 2) -// XCTAssertEqual(sut.originalInput, ["--foo", "--"]) -// } -// -// func testTerminatorAtTheBeginning() throws { -// let sut = try SplitArguments(arguments: ["--", "--foo"]) -// -// XCTAssertEqual(sut.elements.count, 2) -// -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .terminator) -// -// AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) -// AssertElementEqual(sut, at: 1, .value("--foo")) -// -// XCTAssertEqual(sut.originalInput.count, 2) -// XCTAssertEqual(sut.originalInput, ["--", "--foo"]) -// } -//} -// -//// MARK: - Removing Entries -// -//extension SplitArgumentTests { -// func testRemovingValuesForLongNames() throws { -// var sut = try SplitArguments(arguments: ["--foo", "--bar"]) -// XCTAssertEqual(sut.elements.count, 2) -// sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) -// XCTAssertEqual(sut.elements.count, 1) -// sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) -// XCTAssertEqual(sut.elements.count, 0) -// } -// -// func testRemovingValuesForLongNamesWithValue() throws { -// var sut = try SplitArguments(arguments: ["--foo=A", "--bar=B"]) -// XCTAssertEqual(sut.elements.count, 2) -// sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) -// XCTAssertEqual(sut.elements.count, 1) -// sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) -// XCTAssertEqual(sut.elements.count, 0) -// } -// -// func testRemovingValuesForShortNames() throws { -// var sut = try SplitArguments(arguments: ["-f", "-b"]) -// XCTAssertEqual(sut.elements.count, 2) -// sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) -// XCTAssertEqual(sut.elements.count, 1) -// sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) -// XCTAssertEqual(sut.elements.count, 0) -// } -// -// func testRemovingValuesForCombinedShortNames() throws { -// let sut = try SplitArguments(arguments: ["-fb"]) -// -// XCTAssertEqual(sut.elements.count, 3) -// AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) -// AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("fb")))) -// AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) -// AssertElementEqual(sut, at: 1, .option(.name(.short("f")))) -// AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) -// AssertElementEqual(sut, at: 2, .option(.name(.short("b")))) -// -// do { -// var sutB = sut -// sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) -// -// XCTAssertEqual(sutB.elements.count, 0) -// } -// do { -// var sutB = sut -// sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .sub(0))) -// -// XCTAssertEqual(sutB.elements.count, 1) -// AssertIndexEqual(sutB, at: 0, inputIndex: 0, subIndex: .sub(1)) -// AssertElementEqual(sutB, at: 0, .option(.name(.short("b")))) -// } -// do { -// var sutB = sut -// sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .sub(1))) -// -// XCTAssertEqual(sutB.elements.count, 1) -// AssertIndexEqual(sutB, at: 0, inputIndex: 0, subIndex: .sub(0)) -// AssertElementEqual(sutB, at: 0, .option(.name(.short("f")))) -// } -// } -//} -// -//// MARK: - Pop & Peek -// -//extension SplitArgumentTests { -// func testPopNext() throws { -// var sut = try SplitArguments(arguments: ["--foo", "bar"]) -// -// let a = try XCTUnwrap(sut.popNext()) -// XCTAssertEqual(a.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) -// XCTAssertEqual(a.1, .option(.name(.long("foo")))) -// -// let b = try XCTUnwrap(sut.popNext()) -// XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) -// XCTAssertEqual(b.1, .value("bar")) -// -// XCTAssertNil(sut.popNext()) -// } -// -// func testPeekNext() throws { -// let sut = try SplitArguments(arguments: ["--foo", "bar"]) -// -// let a = try XCTUnwrap(sut.peekNext()) -// XCTAssertEqual(a.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) -// XCTAssertEqual(a.1, .option(.name(.long("foo")))) -// -// let b = try XCTUnwrap(sut.peekNext()) -// XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) -// XCTAssertEqual(b.1, .option(.name(.long("foo")))) -// } -// -// func testPeekNextWhenEmpty() throws { -// let sut = try SplitArguments(arguments: []) -// XCTAssertNil(sut.peekNext()) -// } -// -// func testPopNextElementIfValueAfter_1() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let value = try XCTUnwrap(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) -// XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) -// XCTAssertEqual(value.1, "bar") -// } -// -// func testPopNextElementIfValueAfter_2() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let value = try XCTUnwrap(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) -// XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) -// XCTAssertEqual(value.1, "foo") -// } -// -// func testPopNextElementIfValueAfter_3() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// XCTAssertNil(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete)))) -// } -// -// func testPopNextValueAfter_1() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let valueA = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) -// XCTAssertEqual(valueA.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) -// XCTAssertEqual(valueA.1, "bar") -// -// let valueB = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) -// XCTAssertEqual(valueB.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) -// XCTAssertEqual(valueB.1, "foo") -// } -// -// func testPopNextValueAfter_2() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let value = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) -// XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) -// XCTAssertEqual(value.1, "foo") -// -// XCTAssertNil(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) -// } -// -// func testPopNextValueAfter_3() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// XCTAssertNil(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete)))) -// } -// -// func testPopNextElementAsValueAfter_1() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let valueA = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) -// XCTAssertEqual(valueA.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) -// XCTAssertEqual(valueA.1, "bar") -// -// let valueB = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) -// XCTAssertEqual(valueB.0, .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete))) -// XCTAssertEqual(valueB.1, "--foo") -// } -// -// func testPopNextElementAsValueAfter_2() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// XCTAssertNil(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete)))) -// } -// -// func testPopNextElementAsValueAfter_3() throws { -// var sut = try SplitArguments(arguments: ["--bar", "-bar"]) -// -// let value = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) -// XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) -// XCTAssertEqual(value.1, "-bar") -// } -// -// func testPopNextElementIfValue() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// _ = try XCTUnwrap(sut.popNext()) -// -// let value = try XCTUnwrap(sut.popNextElementIfValue()) -// XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) -// XCTAssertEqual(value.1, "bar") -// -// XCTAssertNil(sut.popNextElementIfValue()) -// } -// -// func testPopNextValue() throws { -// var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let valueA = try XCTUnwrap(sut.popNextValue()) -// XCTAssertEqual(valueA.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) -// XCTAssertEqual(valueA.1, "bar") -// -// let valueB = try XCTUnwrap(sut.popNextValue()) -// XCTAssertEqual(valueB.0, SplitArguments.Index(inputIndex: 3, subIndex: .complete)) -// XCTAssertEqual(valueB.1, "foo") -// -// XCTAssertNil(sut.popNextElementIfValue()) -// } -// -// func testPeekNextValue() throws { -// let sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) -// -// let valueA = try XCTUnwrap(sut.peekNextValue()) -// XCTAssertEqual(valueA.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) -// XCTAssertEqual(valueA.1, "bar") -// -// let valueB = try XCTUnwrap(sut.peekNextValue()) -// XCTAssertEqual(valueB.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) -// XCTAssertEqual(valueB.1, "bar") -// } -//} +extension SplitArguments.InputIndex: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self.init(rawValue: value) + } +} + +private func AssertIndexEqual(_ sut: SplitArguments, at index: Int, inputIndex: Int, subIndex: SplitArguments.SubIndex, file: StaticString = #file, line: UInt = #line) { + guard index < sut.elements.endIndex else { + XCTFail("Element index \(index) is out of range. sur only has \(sut.elements.count) elements.", file: (file), line: line) + return + } + let splitIndex = sut.elements[index].index + let expected = SplitArguments.Index(inputIndex: SplitArguments.InputIndex(rawValue: inputIndex), subIndex: subIndex) + if splitIndex.inputIndex != expected.inputIndex { + XCTFail("inputIndex does not match: \(splitIndex.inputIndex.rawValue) != \(expected.inputIndex.rawValue)", file: (file), line: line) + } + if splitIndex.subIndex != expected.subIndex { + XCTFail("inputIndex does not match: \(splitIndex.subIndex) != \(expected.subIndex)", file: (file), line: line) + } +} + +private func AssertElementEqual(_ sut: SplitArguments, at index: Int, _ element: SplitArguments.Element.Value, file: StaticString = #file, line: UInt = #line) { + guard index < sut.elements.endIndex else { + XCTFail("Element index \(index) is out of range. sur only has \(sut.elements.count) elements.", file: (file), line: line) + return + } + XCTAssertEqual(sut.elements[index].value, element, file: (file), line: line) +} + +final class SplitArgumentTests: XCTestCase { + func testEmpty() throws { + let sut = try SplitArguments(arguments: []) + XCTAssertEqual(sut.elements.count, 0) + XCTAssertEqual(sut.originalInput.count, 0) + } + + func testSingleValue() throws { + let sut = try SplitArguments(arguments: ["abc"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .value("abc")) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["abc"]) + } + + func testSingleLongOption() throws { + let sut = try SplitArguments(arguments: ["--abc"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("abc")))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["--abc"]) + } + + func testSingleShortOption() throws { + let sut = try SplitArguments(arguments: ["-a"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.short("a")))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["-a"]) + } + + func testSingleLongOptionWithValue() throws { + let sut = try SplitArguments(arguments: ["--abc=def"]) + + XCTAssertEqual(sut.elements.count, 1) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.nameWithValue(.long("abc"), "def"))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["--abc=def"]) + } + + func testMultipleShortOptionsCombined() throws { + let sut = try SplitArguments(arguments: ["-abc"]) + + XCTAssertEqual(sut.elements.count, 4) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("abc")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sut, at: 1, .option(.name(.short("a")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sut, at: 2, .option(.name(.short("b")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 0, subIndex: .sub(2)) + AssertElementEqual(sut, at: 3, .option(.name(.short("c")))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["-abc"]) + } + + func testSingleLongOptionWithValueAndSingleDash() throws { + let sut = try SplitArguments(arguments: ["-abc=def"]) + + XCTAssertEqual(sut.elements.count, 1) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.nameWithValue(.longWithSingleDash("abc"), "def"))) + + XCTAssertEqual(sut.originalInput.count, 1) + XCTAssertEqual(sut.originalInput, ["-abc=def"]) + } +} + +extension SplitArgumentTests { + func testMultipleValues() throws { + let sut = try SplitArguments(arguments: ["abc", "x", "1234"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .value("abc")) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .value("x")) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("1234")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["abc", "x", "1234"]) + } + + func testMultipleLongOptions() throws { + let sut = try SplitArguments(arguments: ["--d", "--1", "--abc-def"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("d")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .option(.name(.long("1")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .option(.name(.long("abc-def")))) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--d", "--1", "--abc-def"]) + } + + func testMultipleShortOptions() throws { + let sut = try SplitArguments(arguments: ["-x", "-y", "-z"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.short("x")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .option(.name(.short("y")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .option(.name(.short("z")))) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["-x", "-y", "-z"]) + } + + func testMultipleShortOptionsCombined_2() throws { + let sut = try SplitArguments(arguments: ["-bc", "-fv", "-a"]) + + XCTAssertEqual(sut.elements.count, 7) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("bc")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sut, at: 1, .option(.name(.short("b")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sut, at: 2, .option(.name(.short("c")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 3, .option(.name(.longWithSingleDash("fv")))) + + AssertIndexEqual(sut, at: 4, inputIndex: 1, subIndex: .sub(0)) + AssertElementEqual(sut, at: 4, .option(.name(.short("f")))) + + AssertIndexEqual(sut, at: 5, inputIndex: 1, subIndex: .sub(1)) + AssertElementEqual(sut, at: 5, .option(.name(.short("v")))) + + AssertIndexEqual(sut, at: 6, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 6, .option(.name(.short("a")))) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["-bc", "-fv", "-a"]) + } +} + +extension SplitArgumentTests { + func testMixed_1() throws { + let sut = try SplitArguments(arguments: ["-x", "abc", "--foo", "1234", "-zz"]) + + XCTAssertEqual(sut.elements.count, 7) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.short("x")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .value("abc")) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 3, subIndex: .complete) + AssertElementEqual(sut, at: 3, .value("1234")) + + AssertIndexEqual(sut, at: 4, inputIndex: 4, subIndex: .complete) + AssertElementEqual(sut, at: 4, .option(.name(.longWithSingleDash("zz")))) + + AssertIndexEqual(sut, at: 5, inputIndex: 4, subIndex: .sub(0)) + AssertElementEqual(sut, at: 5, .option(.name(.short("z")))) + + AssertIndexEqual(sut, at: 6, inputIndex: 4, subIndex: .sub(1)) + AssertElementEqual(sut, at: 6, .option(.name(.short("z")))) + + XCTAssertEqual(sut.originalInput.count, 5) + XCTAssertEqual(sut.originalInput, ["-x", "abc", "--foo", "1234", "-zz"]) + } + + func testMixed_2() throws { + let sut = try SplitArguments(arguments: ["1234", "-zz", "abc", "-x", "--foo"]) + + XCTAssertEqual(sut.elements.count, 7) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .value("1234")) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .option(.name(.longWithSingleDash("zz")))) + + AssertIndexEqual(sut, at: 2, inputIndex: 1, subIndex: .sub(0)) + AssertElementEqual(sut, at: 2, .option(.name(.short("z")))) + + AssertIndexEqual(sut, at: 3, inputIndex: 1, subIndex: .sub(1)) + AssertElementEqual(sut, at: 3, .option(.name(.short("z")))) + + AssertIndexEqual(sut, at: 4, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 4, .value("abc")) + + AssertIndexEqual(sut, at: 5, inputIndex: 3, subIndex: .complete) + AssertElementEqual(sut, at: 5, .option(.name(.short("x")))) + + AssertIndexEqual(sut, at: 6, inputIndex: 4, subIndex: .complete) + AssertElementEqual(sut, at: 6, .option(.name(.long("foo")))) + + XCTAssertEqual(sut.originalInput.count, 5) + XCTAssertEqual(sut.originalInput, ["1234", "-zz", "abc", "-x", "--foo"]) + } + + func testTerminator_1() throws { + let sut = try SplitArguments(arguments: ["--foo", "--", "--bar"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("--bar")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--foo", "--", "--bar"]) + } + + func testTerminator_2() throws { + let sut = try SplitArguments(arguments: ["--foo", "--", "bar"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("bar")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--foo", "--", "bar"]) + } + + func testTerminator_3() throws { + let sut = try SplitArguments(arguments: ["--foo", "--", "--bar=baz"]) + + XCTAssertEqual(sut.elements.count, 3) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + AssertIndexEqual(sut, at: 2, inputIndex: 2, subIndex: .complete) + AssertElementEqual(sut, at: 2, .value("--bar=baz")) + + XCTAssertEqual(sut.originalInput.count, 3) + XCTAssertEqual(sut.originalInput, ["--foo", "--", "--bar=baz"]) + } + + func testTerminatorAtTheEnd() throws { + let sut = try SplitArguments(arguments: ["--foo", "--"]) + + XCTAssertEqual(sut.elements.count, 2) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.long("foo")))) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .terminator) + + XCTAssertEqual(sut.originalInput.count, 2) + XCTAssertEqual(sut.originalInput, ["--foo", "--"]) + } + + func testTerminatorAtTheBeginning() throws { + let sut = try SplitArguments(arguments: ["--", "--foo"]) + + XCTAssertEqual(sut.elements.count, 2) + + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .terminator) + + AssertIndexEqual(sut, at: 1, inputIndex: 1, subIndex: .complete) + AssertElementEqual(sut, at: 1, .value("--foo")) + + XCTAssertEqual(sut.originalInput.count, 2) + XCTAssertEqual(sut.originalInput, ["--", "--foo"]) + } +} + +// MARK: - Removing Entries + +extension SplitArgumentTests { + func testRemovingValuesForLongNames() throws { + var sut = try SplitArguments(arguments: ["--foo", "--bar"]) + XCTAssertEqual(sut.elements.count, 2) + sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 1) + sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 0) + } + + func testRemovingValuesForLongNamesWithValue() throws { + var sut = try SplitArguments(arguments: ["--foo=A", "--bar=B"]) + XCTAssertEqual(sut.elements.count, 2) + sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 1) + sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 0) + } + + func testRemovingValuesForShortNames() throws { + var sut = try SplitArguments(arguments: ["-f", "-b"]) + XCTAssertEqual(sut.elements.count, 2) + sut.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 1) + sut.remove(at: SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(sut.elements.count, 0) + } + + func testRemovingValuesForCombinedShortNames() throws { + let sut = try SplitArguments(arguments: ["-fb"]) + + XCTAssertEqual(sut.elements.count, 3) + AssertIndexEqual(sut, at: 0, inputIndex: 0, subIndex: .complete) + AssertElementEqual(sut, at: 0, .option(.name(.longWithSingleDash("fb")))) + AssertIndexEqual(sut, at: 1, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sut, at: 1, .option(.name(.short("f")))) + AssertIndexEqual(sut, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sut, at: 2, .option(.name(.short("b")))) + + do { + var sutB = sut + sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .complete)) + + XCTAssertEqual(sutB.elements.count, 0) + } + do { + var sutB = sut + sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .sub(0))) + + XCTAssertEqual(sutB.elements.count, 1) + AssertIndexEqual(sutB, at: 2, inputIndex: 0, subIndex: .sub(1)) + AssertElementEqual(sutB, at: 2, .option(.name(.short("b")))) + } + do { + var sutB = sut + sutB.remove(at: SplitArguments.Index(inputIndex: 0, subIndex: .sub(1))) + + XCTAssertEqual(sutB.elements.count, 1) + AssertIndexEqual(sutB, at: 2, inputIndex: 0, subIndex: .sub(0)) + AssertElementEqual(sutB, at: 2, .option(.name(.short("f")))) + } + } +} + +// MARK: - Pop & Peek + +extension SplitArgumentTests { + func testPopNext() throws { + var sut = try SplitArguments(arguments: ["--foo", "bar"]) + + let a = try XCTUnwrap(sut.popNext()) + XCTAssertEqual(a.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(a.1.value, .option(.name(.long("foo")))) + + let b = try XCTUnwrap(sut.popNext()) + XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(b.1.value, .value("bar")) + + XCTAssertNil(sut.popNext()) + } + + func testPeekNext() throws { + let sut = try SplitArguments(arguments: ["--foo", "bar"]) + + let a = try XCTUnwrap(sut.peekNext()) + XCTAssertEqual(a.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(a.1.value, .option(.name(.long("foo")))) + + let b = try XCTUnwrap(sut.peekNext()) + XCTAssertEqual(b.0, .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete))) + XCTAssertEqual(b.1.value, .option(.name(.long("foo")))) + } + + func testPeekNextWhenEmpty() throws { + let sut = try SplitArguments(arguments: []) + XCTAssertNil(sut.peekNext()) + } + + func testPopNextElementIfValueAfter_1() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let value = try XCTUnwrap(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(value.1, "bar") + } + + func testPopNextElementIfValueAfter_2() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let value = try XCTUnwrap(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) + XCTAssertEqual(value.1, "foo") + } + + func testPopNextElementIfValueAfter_3() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + XCTAssertNil(sut.popNextElementIfValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete)))) + } + + func testPopNextValueAfter_1() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueA.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueB.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) + XCTAssertEqual(valueB.1, "foo") + } + + func testPopNextValueAfter_2() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let value = try XCTUnwrap(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete))) + XCTAssertEqual(value.1, "foo") + + XCTAssertNil(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete)))) + } + + func testPopNextValueAfter_3() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + XCTAssertNil(sut.popNextValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete)))) + } + + func testPopNextElementAsValueAfter_1() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueA.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(valueB.0, .argumentIndex(SplitArguments.Index(inputIndex: 2, subIndex: .complete))) + XCTAssertEqual(valueB.1, "--foo") + } + + func testPopNextElementAsValueAfter_2() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + XCTAssertNil(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 3, subIndex: .complete)))) + } + + func testPopNextElementAsValueAfter_3() throws { + var sut = try SplitArguments(arguments: ["--bar", "-bar"]) + + let value = try XCTUnwrap(sut.popNextElementAsValue(after: .argumentIndex(SplitArguments.Index(inputIndex: 0, subIndex: .complete)))) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(value.1, "-bar") + } + + func testPopNextElementIfValue() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + _ = try XCTUnwrap(sut.popNext()) + + let value = try XCTUnwrap(sut.popNextElementIfValue()) + XCTAssertEqual(value.0, .argumentIndex(SplitArguments.Index(inputIndex: 1, subIndex: .complete))) + XCTAssertEqual(value.1, "bar") + + XCTAssertNil(sut.popNextElementIfValue()) + } + + func testPopNextValue() throws { + var sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.popNextValue()) + XCTAssertEqual(valueA.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.popNextValue()) + XCTAssertEqual(valueB.0, SplitArguments.Index(inputIndex: 3, subIndex: .complete)) + XCTAssertEqual(valueB.1, "foo") + + XCTAssertNil(sut.popNextElementIfValue()) + } + + func testPeekNextValue() throws { + let sut = try SplitArguments(arguments: ["--bar", "bar", "--foo", "foo"]) + + let valueA = try XCTUnwrap(sut.peekNextValue()) + XCTAssertEqual(valueA.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(valueA.1, "bar") + + let valueB = try XCTUnwrap(sut.peekNextValue()) + XCTAssertEqual(valueB.0, SplitArguments.Index(inputIndex: 1, subIndex: .complete)) + XCTAssertEqual(valueB.1, "bar") + } +} From 220e97d85519e73142057f44042e8f0e5a66a123 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Tue, 1 Sep 2020 09:22:30 -0500 Subject: [PATCH 5/5] Restore repeat example --- Examples/repeat/main.swift | 195 ++++++------------------------------- 1 file changed, 28 insertions(+), 167 deletions(-) diff --git a/Examples/repeat/main.swift b/Examples/repeat/main.swift index ed2ef2a77..bc5e8b1cc 100644 --- a/Examples/repeat/main.swift +++ b/Examples/repeat/main.swift @@ -1,176 +1,37 @@ -import ArgumentParser - -struct One: ParsableArguments { - @Option var optionOneOne: String = "" - @Option var optionOneTwo: String = "" - @Option var optionOneThree: String = "" - @Option var optionOneFour: String = "" - @Option var optionOneFive: String = "" - @Option var optionOneSix: String = "" - @Option var optionOneSeven: String = "" - @Option var optionOneEight: String = "" - @Option var optionOneNine: String = "" - @Option var optionOneTen: String = "" -} - -struct Two: ParsableArguments { - @Option var optionTwoOne: String = "" - @Option var optionTwoTwo: String = "" - @Option var optionTwoThree: String = "" - @Option var optionTwoFour: String = "" - @Option var optionTwoFive: String = "" - @Option var optionTwoSix: String = "" - @Option var optionTwoSeven: String = "" - @Option var optionTwoEight: String = "" - @Option var optionTwoNine: String = "" - @Option var optionTwoTen: String = "" -} - -struct Three: ParsableArguments { - @Option var optionThreeOne: String = "" - @Option var optionThreeTwo: String = "" - @Option var optionThreeThree: String = "" - @Option var optionThreeFour: String = "" - @Option var optionThreeFive: String = "" - @Option var optionThreeSix: String = "" - @Option var optionThreeSeven: String = "" - @Option var optionThreeEight: String = "" - @Option var optionThreeNine: String = "" - @Option var optionThreeTen: String = "" -} - -struct Four: ParsableArguments { - @Option var optionFourOne: String = "" - @Option var optionFourTwo: String = "" - @Option var optionFourThree: String = "" - @Option var optionFourFour: String = "" - @Option var optionFourFive: String = "" - @Option var optionFourSix: String = "" - @Option var optionFourSeven: String = "" - @Option var optionFourEight: String = "" - @Option var optionFourNine: String = "" - @Option var optionFourTen: String = "" -} +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 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 +// +//===----------------------------------------------------------------------===// -struct Five: ParsableArguments { - @Option var optionFiveOne: String = "" - @Option var optionFiveTwo: String = "" - @Option var optionFiveThree: String = "" - @Option var optionFiveFour: String = "" - @Option var optionFiveFive: String = "" - @Option var optionFiveSix: String = "" - @Option var optionFiveSeven: String = "" - @Option var optionFiveEight: String = "" - @Option var optionFiveNine: String = "" - @Option var optionFiveTen: String = "" -} - -struct Six: ParsableArguments { - @Option var optionSixOne: String = "" - @Option var optionSixTwo: String = "" - @Option var optionSixThree: String = "" - @Option var optionSixFour: String = "" - @Option var optionSixFive: String = "" - @Option var optionSixSix: String = "" - @Option var optionSixSeven: String = "" - @Option var optionSixEight: String = "" - @Option var optionSixNine: String = "" - @Option var optionSixTen: String = "" -} - -struct Seven: ParsableArguments { - @Option var optionSevenOne: String = "" - @Option var optionSevenTwo: String = "" - @Option var optionSevenThree: String = "" - @Option var optionSevenFour: String = "" - @Option var optionSevenFive: String = "" - @Option var optionSevenSix: String = "" - @Option var optionSevenSeven: String = "" - @Option var optionSevenEight: String = "" - @Option var optionSevenNine: String = "" - @Option var optionSevenTen: String = "" -} +import ArgumentParser -struct Eight: ParsableArguments { - @Option var optionEightOne: String = "" - @Option var optionEightTwo: String = "" - @Option var optionEightThree: String = "" - @Option var optionEightFour: String = "" - @Option var optionEightFive: String = "" - @Option var optionEightSix: String = "" - @Option var optionEightSeven: String = "" - @Option var optionEightEight: String = "" - @Option var optionEightNine: String = "" - @Option var optionEightTen: String = "" -} +struct Repeat: ParsableCommand { + @Option(help: "The number of times to repeat 'phrase'.") + var count: Int? -struct Nine: ParsableArguments { - @Option var optionNineOne: String = "" - @Option var optionNineTwo: String = "" - @Option var optionNineThree: String = "" - @Option var optionNineFour: String = "" - @Option var optionNineFive: String = "" - @Option var optionNineSix: String = "" - @Option var optionNineSeven: String = "" - @Option var optionNineEight: String = "" - @Option var optionNineNine: String = "" - @Option var optionNineTen: String = "" -} + @Flag(help: "Include a counter with each repetition.") + var includeCounter = false -struct Ten: ParsableArguments { - @Option var optionTenOne: String = "" - @Option var optionTenTwo: String = "" - @Option var optionTenThree: String = "" - @Option var optionTenFour: String = "" - @Option var optionTenFive: String = "" - @Option var optionTenSix: String = "" - @Option var optionTenSeven: String = "" - @Option var optionTenEight: String = "" - @Option var optionTenNine: String = "" - @Option var optionTenTen: String = "" -} + @Argument(help: "The phrase to repeat.") + var phrase: String -struct Main: ParsableCommand { - @OptionGroup var groupOne: One - @OptionGroup var groupTwo: Two - @OptionGroup var groupThree: Three - @OptionGroup var groupFour: Four - @OptionGroup var groupFive: Five - @OptionGroup var groupSix: Six - @OptionGroup var groupSeven: Seven - @OptionGroup var groupEight: Eight - @OptionGroup var groupNine: Nine - @OptionGroup var groupTen: Ten + mutating func run() throws { + let repeatCount = count ?? .max - func run() { - dump(self) + for i in 1...repeatCount { + if includeCounter { + print("\(i): \(phrase)") + } else { + print(phrase) + } + } } } -let argCount = Int(CommandLine.arguments.last ?? "") ?? 1 -print("Parsing \(argCount) options") - -let numberWords = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"] -var args: [String] = [] -for _ in 0..