diff --git a/Sources/ArgumentParser/Parsable Properties/Argument.swift b/Sources/ArgumentParser/Parsable Properties/Argument.swift index f409a1003..7731e4d9b 100644 --- a/Sources/ArgumentParser/Parsable Properties/Argument.swift +++ b/Sources/ArgumentParser/Parsable Properties/Argument.swift @@ -225,12 +225,14 @@ extension Argument { let help = ArgumentDefinition.Help(options: [], help: help, key: key) let arg = ArgumentDefinition(kind: .positional, help: help, completion: completion ?? .default, update: .unary({ (origin, name, valueString, parsedValues) in + guard let valueString = valueString else { return false } do { let transformedValue = try transform(valueString) parsedValues.set(transformedValue, forKey: key, inputOrigin: origin) } catch { throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) } + return true }), initial: { origin, values in if let v = initial { values.set(v, forKey: key, inputOrigin: origin) @@ -412,6 +414,7 @@ extension Argument { parsingStrategy: parsingStrategy.base, update: .unary({ (origin, name, valueString, parsedValues) in + guard let valueString = valueString else { return false } do { let transformedElement = try transform(valueString) parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { @@ -420,6 +423,7 @@ extension Argument { } catch { throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) } + return true }), initial: setInitialValue) arg.help.defaultValue = helpDefaultValue diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 004bffae6..9d77dfb67 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -338,12 +338,14 @@ extension Option { let help = ArgumentDefinition.Help(options: initial != nil ? .isOptional : [], help: help, key: key) var arg = ArgumentDefinition(kind: kind, help: help, completion: completion ?? .default, parsingStrategy: parsingStrategy.base, update: .unary({ (origin, name, valueString, parsedValues) in + guard let valueString = valueString else { return false } do { let transformedValue = try transform(valueString) parsedValues.set(transformedValue, forKey: key, inputOrigin: origin) } catch { throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) } + return true }), initial: { origin, values in if let v = initial { values.set(v, forKey: key, inputOrigin: origin) @@ -456,6 +458,45 @@ extension Option { }) } + /// Creates an array property with an optional value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: [Element]?, + name: NameSpecification, + parsingStrategy: ArrayParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind? + ) where Element: ExpressibleByArgument, Value == Array? { + self.init(_parsedValue: .init { key in + // Assign the initial-value setter and help text for default value based on if an initial value was provided. + let setInitialValue: ArgumentDefinition.Initial + let helpDefaultValue: String? + if let initial = initial { + setInitialValue = { origin, values in + values.set(initial, forKey: key, inputOrigin: origin) + } + helpDefaultValue = !initial.isEmpty ? initial.defaultValueDescription : nil + } else { + setInitialValue = { _, _ in } + helpDefaultValue = nil + } + + let kind = ArgumentDefinition.Kind.name(key: key, specification: name) + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + var arg = ArgumentDefinition( + kind: kind, + help: help, + completion: completion ?? Element.defaultCompletionKind, + parsingStrategy: parsingStrategy.base, + update: .appendToArray(forType: Element.self, key: key), + initial: setInitialValue + ) + arg.help.defaultValue = helpDefaultValue + return ArgumentSet(arg) + }) + } + /// Creates an array property that reads its values from zero or more /// labeled options. /// @@ -508,6 +549,29 @@ extension Option { ) } + /// Creates an optional array property that reads its values from zero or more + /// labeled options. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - help: Information about how to use this option. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil + ) where Element: ExpressibleByArgument, Value == Array? { + self.init( + initial: nil, + name: name, + parsingStrategy: parsingStrategy, + help: help, + completion: completion + ) + } /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. /// @@ -542,6 +606,64 @@ extension Option { completion: completion ?? .default, parsingStrategy: parsingStrategy.base, update: .unary({ (origin, name, valueString, parsedValues) in + // First of all we need to create an empty array. + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element]()) { _ in } + // Returns true anyway, because we always create an empty array above. + guard let valueString = valueString else { return true } + do { + let transformedElement = try transform(valueString) + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { + $0.append(transformedElement) + }) + } catch { + throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) + } + return true + }), + initial: setInitialValue + ) + arg.help.defaultValue = helpDefaultValue + return ArgumentSet(arg) + }) + } + + /// Creates an array property with an optional default value, intended to be called by other constructors to centralize logic. + /// + /// This private `init` allows us to expose multiple other similar constructors to allow for standard default property initialization while reducing code duplication. + private init( + initial: [Element]?, + name: NameSpecification, + parsingStrategy: ArrayParsingStrategy, + help: ArgumentHelp?, + completion: CompletionKind?, + transform: @escaping (String) throws -> Element + ) where Value == Array? { + self.init(_parsedValue: .init { key in + // Assign the initial-value setter and help text for default value based on if an initial value was provided. + let setInitialValue: ArgumentDefinition.Initial + let helpDefaultValue: String? + if let initial = initial { + setInitialValue = { origin, values in + values.set(initial, forKey: key, inputOrigin: origin) + } + helpDefaultValue = !initial.isEmpty ? "\(initial)" : nil + } else { + setInitialValue = { _, _ in } + helpDefaultValue = nil + } + + let kind = ArgumentDefinition.Kind.name(key: key, specification: name) + let help = ArgumentDefinition.Help(options: [.isOptional, .isRepeating], help: help, key: key) + var arg = ArgumentDefinition( + kind: kind, + help: help, + completion: completion ?? .default, + parsingStrategy: parsingStrategy.base, + update: .unary({ (origin, name, valueString, parsedValues) in + // First of all we need to create an empty array. + parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element]()) { _ in } + // Returns true anyway, because we always create an empty array above. + guard let valueString = valueString else { return true } do { let transformedElement = try transform(valueString) parsedValues.update(forKey: key, inputOrigin: origin, initial: [Element](), closure: { @@ -550,6 +672,7 @@ extension Option { } catch { throw ParserError.unableToParseValue(origin, name, valueString, forKey: key, originalError: error) } + return true }), initial: setInitialValue ) @@ -591,6 +714,38 @@ extension Option { ) } + /// Creates an optional array property that reads its values from zero or more + /// labeled options, parsing with the given closure. + /// + /// This property defaults to an empty array if the `initial` parameter + /// is not specified. + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this flag. + /// - initial: A default value to use for this property. If `initial` is + /// `nil`, this option defaults to an empty array. + /// - parsingStrategy: The behavior to use when parsing multiple values + /// from the command-line arguments. + /// - help: Information about how to use this option. + /// - transform: A closure that converts a string into this property's + /// element type or throws an error. + public init( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @escaping (String) throws -> Element + ) where Value == Array? { + self.init( + initial: nil, + name: name, + parsingStrategy: parsingStrategy, + help: help, + completion: completion, + transform: transform + ) + } + /// Creates an array property with no default value that reads its values from zero or more labeled options, parsing each element with the given closure. /// /// This method is called to initialize an array `Option` with no default value such as: diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index ec45561ca..9f1286e86 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -14,7 +14,8 @@ struct ArgumentDefinition { /// argument's value. enum Update { typealias Nullary = (InputOrigin, Name?, inout ParsedValues) throws -> Void - typealias Unary = (InputOrigin, Name?, String, inout ParsedValues) throws -> Void + /// - Returns: true if value was updated, otherwise false + typealias Unary = (InputOrigin, Name?, String?, inout ParsedValues) throws -> Bool /// An argument that gets its value solely from its presence. case nullary(Nullary) @@ -199,12 +200,18 @@ extension ArgumentDefinition.Update { static func appendToArray(forType type: A.Type, key: InputKey) -> ArgumentDefinition.Update { return ArgumentDefinition.Update.unary { (origin, name, value, values) in + // First of all we need to create an empty array. + values.update(forKey: key, inputOrigin: origin, initial: [A]()) { _ in } + // Returns true anyway, because we always create an empty array above. + guard let value = value else { return true } + guard let v = A(argument: value) else { throw ParserError.unableToParseValue(origin, name, value, forKey: key) } - values.update(forKey: key, inputOrigin: origin, initial: [A](), closure: { + values.update(forKey: key, inputOrigin: origin, initial: [A]()) { $0.append(v) - }) + } + return true } } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentSet.swift b/Sources/ArgumentParser/Parsing/ArgumentSet.swift index 860ba5fd8..51d8694f4 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentSet.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentSet.swift @@ -169,10 +169,12 @@ extension ArgumentDefinition { } self.init(kind: kind, help: ArgumentDefinition.Help(key: key), completion: completion, parsingStrategy: parsingStrategy, update: .unary({ (origin, name, value, values) in + guard let value = value else { return false } guard let v = parser(value) else { throw ParserError.unableToParseValue(origin, name, value, forKey: key) } values.set(v, forKey: key, inputOrigin: origin) + return true }), initial: { origin, values in switch kind { case .default: @@ -215,26 +217,30 @@ extension ArgumentSet { _ usedOrigins: inout InputOrigin ) throws { let origin = InputOrigin(elements: [originElement]) + + // Try to update with default value if argument presented. For example, init an empty array. + let updated = try update(origin, parsed.name, nil, &result) + switch argument.parsingStrategy { case .default: // We need a value for this option. if let value = parsed.value { // This was `--foo=bar` style: - try update(origin, parsed.name, value, &result) + _ = try update(origin, parsed.name, value, &result) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) - try update(origins, parsed.name, String(value), &result) + _ = try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) } else if let (origin2, value) = inputArguments.popNextElementIfValue(after: originElement) { // Use `popNextElementIfValue(after:)` to handle cases where short option // labels are combined let origins = origin.inserting(origin2) - try update(origins, parsed.name, value, &result) + _ = try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) - } else { + } else if !updated { throw ParserError.missingValueForOption(origin, parsed.name) } @@ -242,20 +248,20 @@ extension ArgumentSet { // We need a value for this option. if let value = parsed.value { // This was `--foo=bar` style: - try update(origin, parsed.name, value, &result) + _ = try update(origin, parsed.name, value, &result) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) - try update(origins, parsed.name, String(value), &result) + _ = try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) } else if let (origin2, value) = inputArguments.popNextValue(after: originElement) { // Use `popNext(after:)` to handle cases where short option // labels are combined let origins = origin.inserting(origin2) - try update(origins, parsed.name, value, &result) + _ = try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) - } else { + } else if !updated { throw ParserError.missingValueForOption(origin, parsed.name) } @@ -263,37 +269,36 @@ extension ArgumentSet { // Use an attached value if it exists... if let value = parsed.value { // This was `--foo=bar` style: - try update(origin, parsed.name, value, &result) + _ = try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, - let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { + let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) - try update(origins, parsed.name, String(value), &result) + _ = try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) } else { - guard let (origin2, value) = inputArguments.popNextElementAsValue(after: originElement) else { - throw ParserError.missingValueForOption(origin, parsed.name) + if let (origin2, value) = inputArguments.popNextElementAsValue(after: originElement) { + let origins = origin.inserting(origin2) + _ = try update(origins, parsed.name, value, &result) + usedOrigins.formUnion(origins) } - let origins = origin.inserting(origin2) - try update(origins, parsed.name, value, &result) - usedOrigins.formUnion(origins) } case .allRemainingInput: // Reset initial value with the found input origins: - try argument.initial(origin, &result) + if !updated { try argument.initial(origin, &result) } // Use an attached value if it exists... if let value = parsed.value { // This was `--foo=bar` style: - try update(origin, parsed.name, value, &result) + _ = try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) - try update(origins, parsed.name, String(value), &result) + _ = try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) inputArguments.removeAll(in: usedOrigins) } @@ -301,7 +306,7 @@ extension ArgumentSet { // ...and then consume the rest of the arguments while let (origin2, value) = inputArguments.popNextElementAsValue(after: originElement) { let origins = origin.inserting(origin2) - try update(origins, parsed.name, value, &result) + _ = try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } @@ -309,13 +314,13 @@ extension ArgumentSet { // Use an attached value if it exists... if let value = parsed.value { // This was `--foo=bar` style: - try update(origin, parsed.name, value, &result) + _ = try update(origin, parsed.name, value, &result) usedOrigins.formUnion(origin) } else if argument.allowsJoinedValue, let (origin2, value) = inputArguments.extractJoinedElement(at: originElement) { // Found a joined argument let origins = origin.inserting(origin2) - try update(origins, parsed.name, String(value), &result) + _ = try update(origins, parsed.name, String(value), &result) usedOrigins.formUnion(origins) inputArguments.removeAll(in: usedOrigins) } @@ -323,7 +328,7 @@ extension ArgumentSet { // ...and then consume the arguments until hitting an option while let (origin2, value) = inputArguments.popNextElementIfValue() { let origins = origin.inserting(origin2) - try update(origins, parsed.name, value, &result) + _ = try update(origins, parsed.name, value, &result) usedOrigins.formUnion(origins) } } @@ -458,7 +463,7 @@ extension ArgumentSet { break ArgumentLoop } let value = unusedInput.originalInput(at: origin)! - try update([origin], nil, value, &result) + _ = try update([origin], nil, value, &result) } while argumentDefinition.isRepeatingPositional } } diff --git a/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift index 482d3fc47..e2fb260a0 100644 --- a/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/JoinedEndToEndTests.swift @@ -122,7 +122,6 @@ extension JoinedEndToEndTests { } func testArrayValueParsing_Fails() throws { - XCTAssertThrowsError(try Bar.parse(["-D"])) XCTAssertThrowsError(try Bar.parse(["-Ddebug1", "debug2"])) } } diff --git a/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift index 9148b3c50..35796ab64 100644 --- a/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/OptionalEndToEndTests.swift @@ -206,3 +206,158 @@ extension OptionalEndToEndTests { XCTAssertThrowsError(try Bar.parse(["-f", "--name", "A"])) } } + +// MARK: - + +fileprivate struct ExpressibleByArgumentArrays: ParsableArguments { + @Option(parsing: .singleValue) var singleValue: [String]? + @Option(parsing: .unconditionalSingleValue) var unconditionalSingleValue: [String]? + @Option(parsing: .upToNextOption) var upToNextOption: [String]? + @Option(parsing: .remaining) var remaining: [String]? +} + +extension OptionalEndToEndTests { + func testParsing_Optional_ExpressibleByArgument_Arrays() throws { + AssertParse(ExpressibleByArgumentArrays.self, []) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--single-value"]) { foo in + XCTAssertEqual(foo.singleValue, []) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--single-value", "a"]) { foo in + XCTAssertEqual(foo.singleValue, ["a"]) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--unconditional-single-value"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertEqual(foo.unconditionalSingleValue, []) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--unconditional-single-value", "b"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertEqual(foo.unconditionalSingleValue, ["b"]) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--up-to-next-option"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertEqual(foo.upToNextOption, []) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--up-to-next-option", "c"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertEqual(foo.upToNextOption, ["c"]) + XCTAssertNil(foo.remaining) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--remaining"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertEqual(foo.remaining, []) + } + + AssertParse(ExpressibleByArgumentArrays.self, ["--remaining", "d"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertEqual(foo.remaining, ["d"]) + } + } +} + +// MARK: - + +fileprivate struct NonExpressibleByArgumentArrays: ParsableArguments { + struct Name: RawRepresentable, Equatable { + var rawValue: String + } + @Option(parsing: .singleValue, transform: Name.init) var singleValue: [Name]? + @Option(parsing: .unconditionalSingleValue, transform: Name.init) var unconditionalSingleValue: [Name]? + @Option(parsing: .upToNextOption, transform: Name.init) var upToNextOption: [Name]? + @Option(parsing: .remaining, transform: Name.init) var remaining: [Name]? +} + +extension OptionalEndToEndTests { + func testParsing_Optional_NonExpressibleByArgument_Arrays() throws { + AssertParse(NonExpressibleByArgumentArrays.self, []) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--single-value"]) { foo in + XCTAssertEqual(foo.singleValue, []) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--single-value", "a"]) { foo in + XCTAssertEqual(foo.singleValue, [NonExpressibleByArgumentArrays.Name(rawValue: "a")]) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--unconditional-single-value"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertEqual(foo.unconditionalSingleValue, []) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--unconditional-single-value", "b"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertEqual(foo.unconditionalSingleValue, [NonExpressibleByArgumentArrays.Name(rawValue: "b")]) + XCTAssertNil(foo.upToNextOption) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--up-to-next-option"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertEqual(foo.upToNextOption, []) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--up-to-next-option", "c"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertEqual(foo.upToNextOption, [NonExpressibleByArgumentArrays.Name(rawValue: "c")]) + XCTAssertNil(foo.remaining) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--remaining"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertEqual(foo.remaining, []) + } + + AssertParse(NonExpressibleByArgumentArrays.self, ["--remaining", "d"]) { foo in + XCTAssertNil(foo.singleValue) + XCTAssertNil(foo.unconditionalSingleValue) + XCTAssertNil(foo.upToNextOption) + XCTAssertEqual(foo.remaining, [NonExpressibleByArgumentArrays.Name(rawValue: "d")]) + } + } +} diff --git a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift index cd5cddeb0..6de758c8f 100644 --- a/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/RepeatingEndToEndTests.swift @@ -28,6 +28,10 @@ extension RepeatingEndToEndTests { XCTAssertTrue(bar.name.isEmpty) } + AssertParse(Bar.self, ["--name"]) { bar in + XCTAssertEqual(bar.name, []) + } + AssertParse(Bar.self, ["--name", "Bar"]) { bar in XCTAssertEqual(bar.name.count, 1) XCTAssertEqual(bar.name.first, "Bar") @@ -65,8 +69,12 @@ extension RepeatingEndToEndTests { // MARK: - fileprivate struct Baz: ParsableArguments { + struct Tag: RawRepresentable, Equatable { + var rawValue: String + } @Flag var verbose: Bool = false @Option(parsing: .remaining) var names: [String] = [] + @Option(parsing: .remaining, transform: Tag.init) var tags: [Tag] = [.init(rawValue: "swift")] } extension RepeatingEndToEndTests { @@ -74,6 +82,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, []) { baz in XCTAssertFalse(baz.verbose) XCTAssertTrue(baz.names.isEmpty) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -81,6 +90,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--names"]) { baz in XCTAssertFalse(baz.verbose) XCTAssertTrue(baz.names.isEmpty) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -88,6 +98,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--names", "one"]) { baz in XCTAssertFalse(baz.verbose) XCTAssertEqual(baz.names, ["one"]) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -95,6 +106,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--names", "one", "two"]) { baz in XCTAssertFalse(baz.verbose) XCTAssertEqual(baz.names, ["one", "two"]) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -102,6 +114,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--verbose", "--names", "one", "two"]) { baz in XCTAssertTrue(baz.verbose) XCTAssertEqual(baz.names, ["one", "two"]) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -109,6 +122,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--names", "one", "two", "--verbose"]) { baz in XCTAssertFalse(baz.verbose) XCTAssertEqual(baz.names, ["one", "two", "--verbose"]) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -116,6 +130,7 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--verbose", "--names", "one", "two", "--verbose"]) { baz in XCTAssertTrue(baz.verbose) XCTAssertEqual(baz.names, ["one", "two", "--verbose"]) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) } } @@ -123,6 +138,16 @@ extension RepeatingEndToEndTests { AssertParse(Baz.self, ["--verbose", "--names", "one", "two", "--verbose", "--other", "three"]) { baz in XCTAssertTrue(baz.verbose) XCTAssertEqual(baz.names, ["one", "two", "--verbose", "--other", "three"]) + XCTAssertEqual(baz.tags, [Baz.Tag(rawValue: "swift")]) + } + } + + func testParsing_repeatingStringRemaining_9() { + AssertParse(Baz.self, ["--tags"]) { baz in + XCTAssertFalse(baz.verbose) + XCTAssertTrue(baz.names.isEmpty) + print(baz.tags) + XCTAssertTrue(baz.tags.isEmpty) } } } @@ -169,6 +194,12 @@ extension RepeatingEndToEndTests { XCTAssertNil(qux.extra) } + AssertParse(Qux.self, ["--names"]) { qux in + XCTAssertFalse(qux.verbose) + XCTAssertTrue(qux.names.isEmpty) + XCTAssertNil(qux.extra) + } + AssertParse(Qux.self, ["--names", "one"]) { qux in XCTAssertFalse(qux.verbose) XCTAssertEqual(qux.names, ["one"]) @@ -216,8 +247,6 @@ extension RepeatingEndToEndTests { XCTAssertThrowsError(try Qux.parse(["--names", "one", "--other"])) XCTAssertThrowsError(try Qux.parse(["--names", "one", "two", "--other"])) XCTAssertThrowsError(try Qux.parse(["--names", "--other"])) - XCTAssertThrowsError(try Qux.parse(["--names", "--verbose"])) - XCTAssertThrowsError(try Qux.parse(["--names", "--verbose", "three"])) } } @@ -250,6 +279,24 @@ extension RepeatingEndToEndTests { XCTAssertTrue(wobble.evenMoreNames.isEmpty) } + AssertParse(Wobble.self, ["--names"]) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertTrue(wobble.moreNames.isEmpty) + XCTAssertTrue(wobble.evenMoreNames.isEmpty) + } + + AssertParse(Wobble.self, ["--more-names"]) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertTrue(wobble.moreNames.isEmpty) + XCTAssertTrue(wobble.evenMoreNames.isEmpty) + } + + AssertParse(Wobble.self, ["--even-more-names"]) { wobble in + XCTAssertTrue(wobble.names.isEmpty) + XCTAssertTrue(wobble.moreNames.isEmpty) + XCTAssertTrue(wobble.evenMoreNames.isEmpty) + } + AssertParse(Wobble.self, names) { wobble in XCTAssertEqual(wobble.names.map { $0.value }, ["one", "two"]) XCTAssertTrue(wobble.moreNames.isEmpty) diff --git a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift index 9280f8878..d92200efc 100644 --- a/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift +++ b/Tests/ArgumentParserEndToEndTests/SourceCompatEndToEndTests.swift @@ -119,6 +119,14 @@ fileprivate struct AllOptions: ParsableArguments { @Option() var e11: [Int] = [1, 2] @Option(parsing: .singleValue) var e12: [Int] @Option(help: "") var e13: [Int] + @Option(name: .long, parsing: .singleValue, help: "") var e14: [Int]? + @Option(parsing: .singleValue, help: "") var e15: [Int]? + @Option(name: .long, help: "") var e16: [Int]? + @Option(help: "") var e17: [Int]? + @Option(parsing: .singleValue) var e18: [Int]? + @Option(name: .long) var e19: [Int]? + @Option() var e20: [Int]? + @Option var e21: [Int]? @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f: [Int] = [1, 2] @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f1: [Int] = [1, 2] @@ -134,6 +142,13 @@ fileprivate struct AllOptions: ParsableArguments { @Option(transform: { _ in 0 }) var f11: [Int] = [1, 2] @Option(parsing: .singleValue, transform: { _ in 0 }) var f12: [Int] @Option(help: "", transform: { _ in 0 }) var f13: [Int] + @Option(name: .long, parsing: .singleValue, help: "", transform: { _ in 0 }) var f14: [Int]? + @Option(parsing: .singleValue, help: "", transform: { _ in 0 }) var f15: [Int]? + @Option(name: .long, help: "", transform: { _ in 0 }) var f16: [Int]? + @Option(help: "", transform: { _ in 0 }) var f17: [Int]? + @Option(parsing: .singleValue, transform: { _ in 0 }) var f18: [Int]? + @Option(name: .long, transform: { _ in 0 }) var f19: [Int]? + @Option(transform: { _ in 0 }) var f20: [Int]? } struct AllFlags: ParsableArguments {