diff --git a/Sources/ArgumentParser/Parsable Properties/Option.swift b/Sources/ArgumentParser/Parsable Properties/Option.swift index 024a0232a..972facc40 100644 --- a/Sources/ArgumentParser/Parsable Properties/Option.swift +++ b/Sources/ArgumentParser/Parsable Properties/Option.swift @@ -408,6 +408,51 @@ extension Option { }) } + /// Creates a required property that reads its value from a labeled option, + /// parsing with the given closure. + /// + /// This initializer is used when you declare an `@Option`-attributed property + /// with a transform closure and without a default value: + /// + /// ```swift + /// @Option(transform: { $0.first ?? " " }) + /// var char: Character + /// ``` + /// + /// - Parameters: + /// - name: A specification for what names are allowed for this option. + /// - parsingStrategy: The behavior to use when looking for this option's + /// value. + /// - help: Information about how to use this option. + /// - completion: The type of command-line completion provided for this + /// option. + /// - transform: A closure that converts a string into this property's + /// type, or else throws an error. + @preconcurrency + public init( + wrappedValue: Value, + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @Sendable @escaping (_ name: String, _ value: String) throws -> Value + ) { + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) + } + /// Creates a required property that reads its value from a labeled option, /// parsing with the given closure. /// @@ -452,6 +497,30 @@ extension Option { return ArgumentSet(arg) }) } + + @preconcurrency + public init( + name: NameSpecification = .long, + parsing parsingStrategy: SingleValueParsingStrategy = .next, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @Sendable @escaping (_ name: String, _ value: String) throws -> Value + ) { + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Bare.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } } // MARK: - @Option Optional Initializers @@ -867,6 +936,31 @@ extension Option { }) } + @preconcurrency + public init( + wrappedValue: [T], + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @Sendable @escaping (_ name: String, _ value: String) throws -> T + ) where Value == [T] { + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: wrappedValue, + completion: completion) + + return ArgumentSet(arg) + }) + } + /// Creates a required array property that reads its values from zero or /// more labeled options, parsing each element with the given closure. /// @@ -910,4 +1004,28 @@ extension Option { return ArgumentSet(arg) }) } + + @preconcurrency + public init( + name: NameSpecification = .long, + parsing parsingStrategy: ArrayParsingStrategy = .singleValue, + help: ArgumentHelp? = nil, + completion: CompletionKind? = nil, + transform: @Sendable @escaping (_ name: String, _ value: String) throws -> T + ) where Value == [T] { + self.init( + _parsedValue: .init { key in + let arg = ArgumentDefinition( + container: Array.self, + key: key, + kind: .name(key: key, specification: name), + help: help, + parsingStrategy: parsingStrategy.base, + transform: transform, + initial: nil, + completion: completion) + + return ArgumentSet(arg) + }) + } } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index b59aa80a8..1d74bf09c 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -296,6 +296,36 @@ extension ArgumentDefinition { completion: completion) } + init( + container: Container.Type, + key: InputKey, + kind: ArgumentDefinition.Kind, + help: ArgumentHelp?, + parsingStrategy: ParsingStrategy, + transform: @escaping (_ name: String, _ valueString: String) throws -> Container.Contained, + initial: Container.Initial?, + completion: CompletionKind? + ) where Container: ArgumentDefinitionContainer { + self.init( + container: Container.self, + key: key, + kind: kind, + allValueStrings: [], + help: help, + defaultValueDescription: nil, + parsingStrategy: parsingStrategy, + parser: { (key, origin, name, valueString) -> Container.Contained in + do { + return try transform(name?.valueString ?? "", valueString) + } catch { + throw ParserError.unableToParseValue( + origin, name, valueString, forKey: key, originalError: error) + } + }, + initial: initial, + completion: completion) + } + private init( container: Container.Type, key: InputKey, diff --git a/Tests/ArgumentParserEndToEndTests/OrderedOptionsEndToEndTests.swift b/Tests/ArgumentParserEndToEndTests/OrderedOptionsEndToEndTests.swift new file mode 100644 index 000000000..bd29f0a94 --- /dev/null +++ b/Tests/ArgumentParserEndToEndTests/OrderedOptionsEndToEndTests.swift @@ -0,0 +1,73 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import ArgumentParserTestHelpers +import XCTest + +final class OrderedOptionsEndToEndTests: XCTestCase {} + +private enum Operation: Equatable { + case resize(String) + case blur(Double) + case crop(String) + case rotate(Double) +} + +private struct ImageTool: ParsableArguments { + @Option( + name: [.customLong("resize"), .customLong("blur"), .customLong("crop"), .customLong("rotate")], + transform: { optionName, value in + switch optionName { + case "resize": return .resize(value) + case "blur": return .blur(Double(value)!) + case "crop": return .crop(value) + case "rotate": return .rotate(Double(value)!) + default: fatalError("Unexpected option name: \(optionName)") + } + } + ) var operations: [Operation] = [] +} + +extension OrderedOptionsEndToEndTests { + func testOrderedOptions() throws { + AssertParse(ImageTool.self, [ + "--resize", "50%", + "--blur", "3", + "--crop", "100x100", + "--rotate", "90", + "--blur", "1" + ]) { tool in + XCTAssertEqual(tool.operations, [ + .resize("50%"), + .blur(3.0), + .crop("100x100"), + .rotate(90.0), + .blur(1.0) + ]) + } + } + + func testMixedShortAndLongNames() throws { + struct MixedTool: ParsableArguments { + @Option( + name: [.customShort("r"), .customLong("resize")], + transform: { name, value in + return "\(name):\(value)" + } + ) var ops: [String] = [] + } + + AssertParse(MixedTool.self, ["-r", "1", "--resize", "2", "-r", "3"]) { tool in + XCTAssertEqual(tool.ops, ["r:1", "resize:2", "r:3"]) + } + } +} diff --git a/build_output.txt b/build_output.txt new file mode 100644 index 000000000..d9c3ce1bd Binary files /dev/null and b/build_output.txt differ