Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Value>.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.
///
Expand Down Expand Up @@ -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<Value>.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<T: ExpressibleByArgument> Initializers
Expand Down Expand Up @@ -867,6 +936,31 @@ extension Option {
})
}

@preconcurrency
public init<T>(
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<T>.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.
///
Expand Down Expand Up @@ -910,4 +1004,28 @@ extension Option {
return ArgumentSet(arg)
})
}

@preconcurrency
public init<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<T>.self,
key: key,
kind: .name(key: key, specification: name),
help: help,
parsingStrategy: parsingStrategy.base,
transform: transform,
initial: nil,
completion: completion)

return ArgumentSet(arg)
})
}
}
30 changes: 30 additions & 0 deletions Sources/ArgumentParser/Parsing/ArgumentDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,36 @@ extension ArgumentDefinition {
completion: completion)
}

init<Container>(
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: Container.Type,
key: InputKey,
Expand Down
Original file line number Diff line number Diff line change
@@ -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"])
}
}
}
Binary file added build_output.txt
Binary file not shown.