mirror of
https://github.com/apple/swift-argument-parser.git
synced 2026-06-06 20:18:23 +00:00
[GSoC] Conversation for required @Argument 2 (#457)
* Fix array parsing as supplemental input * Add test for `ExpressibleByArgument` * Add options for `CaseIterable` * Add test for `@Argument(transform:)`
This commit is contained in:
@@ -22,7 +22,7 @@ struct CommandParser {
|
||||
let commandTree: Tree<ParsableCommand.Type>
|
||||
var currentNode: Tree<ParsableCommand.Type>
|
||||
var decodedArguments: [DecodedArguments] = []
|
||||
var lineStack: [String]?
|
||||
var lineStack: [String]? = nil
|
||||
|
||||
var rootCommand: ParsableCommand.Type {
|
||||
commandTree.element
|
||||
@@ -37,7 +37,7 @@ struct CommandParser {
|
||||
}
|
||||
}
|
||||
|
||||
internal init(_ rootCommand: ParsableCommand.Type, lines: [String]?) {
|
||||
internal init(_ rootCommand: ParsableCommand.Type, lines: [String]) {
|
||||
self.init(rootCommand)
|
||||
self.lineStack = lines
|
||||
lineStack?.reverse()
|
||||
@@ -158,7 +158,7 @@ extension CommandParser {
|
||||
defaultCapturesAll: currentNode.element.defaultIncludesUnconditionalArguments)
|
||||
} catch {
|
||||
// Try to fix error by interacting with the user
|
||||
guard canInteract(error: error, split: &split) else { throw error }
|
||||
guard try canInteract(error: error, split: &split) else { throw error }
|
||||
return try getValues()
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ extension CommandParser {
|
||||
return (decoder, decodedResult)
|
||||
} catch {
|
||||
// Try to fix error by interacting with the user
|
||||
if canInteract(error: error, arguments: commandArguments, values: &values) {
|
||||
if try canInteract(error: error, arguments: commandArguments, values: &values) {
|
||||
return try getDecoderAndResult()
|
||||
} else {
|
||||
// If decoding this command failed, see if they were asking for
|
||||
|
||||
@@ -15,16 +15,13 @@ extension CommandParser {
|
||||
/// - error: A parsing error thrown by `lenientParse(_:subcommands:defaultCapturesAll:)`.
|
||||
/// - split: A collection of parsed arguments which needs to be modified.
|
||||
/// - Returns: Whether the dialog resolve the error.
|
||||
mutating func canInteract(error: Error, split: inout SplitArguments) -> Bool {
|
||||
mutating func canInteract(error: Error, split: inout SplitArguments) throws -> Bool {
|
||||
guard lineStack == nil || !lineStack!.isEmpty else { return false }
|
||||
guard let error = error as? ParserError else { return false }
|
||||
guard case let .missingValueForOption(inputOrigin, name) = error else { return false }
|
||||
|
||||
var input = lineStack?.removeLast()
|
||||
while input?.isEmpty ?? true {
|
||||
print("? Please enter value for '\(name.synopsisString)': ", terminator: "")
|
||||
input = readLine() ?? nil
|
||||
}
|
||||
print("? Please enter value for '\(name.synopsisString)': ", terminator: "")
|
||||
let input = getInput()
|
||||
|
||||
let inputIndex = inputOrigin.elements.first!.baseIndex! + 1
|
||||
split._elements.insert(.init(value: .value(input!),
|
||||
@@ -46,7 +43,7 @@ extension CommandParser {
|
||||
/// - arguments: A nested tree of argument definitions which can provide modification method.
|
||||
/// - values: The resulting values after parsing the arguments which needs to be modified.
|
||||
/// - Returns: Whether the dialog resolve the error.
|
||||
mutating func canInteract(error: Error, arguments: ArgumentSet, values: inout ParsedValues) -> Bool {
|
||||
mutating func canInteract(error: Error, arguments: ArgumentSet, values: inout ParsedValues) throws -> Bool {
|
||||
guard lineStack == nil || !lineStack!.isEmpty else { return false }
|
||||
guard let error = error as? ParserError else { return false }
|
||||
|
||||
@@ -55,22 +52,26 @@ extension CommandParser {
|
||||
let label = key.rawValue
|
||||
guard label != "generateCompletionScript" else { break }
|
||||
|
||||
// Extract the parameters used in the test.
|
||||
var input = lineStack?.removeLast()
|
||||
while input?.isEmpty ?? true {
|
||||
print("? Please enter '\(label)': ", terminator: "")
|
||||
input = readLine() ?? nil
|
||||
}
|
||||
|
||||
// Retrieve the correct `ArgumentDefinition` for the required transformation
|
||||
// before storing the new value received from the user.
|
||||
guard let definition = arguments.content.first(where: { $0.valueName == label }) else { break }
|
||||
let name = definition.names.first // (where: { $0.case == .long } )
|
||||
guard case let .unary(update) = definition.update else { break }
|
||||
let name = definition.names.first // (where: { $0.case == .long } )
|
||||
|
||||
let input: [String]
|
||||
let allValues = definition.help.allValues
|
||||
if allValues.isEmpty {
|
||||
// Get normal value
|
||||
input = getNormalValue(label: label)
|
||||
} else {
|
||||
allValues.enumerated().forEach { print("\($0 + 1). \($1)") }
|
||||
// Get CaseIterable Enum
|
||||
input = getCaseIterableEnum(label: label, allValues: allValues)
|
||||
}
|
||||
|
||||
// Split array input like "1 2 3".
|
||||
for input in input!.components(separatedBy: " ") {
|
||||
try! update(InputOrigin(elements: [.interactive]), name, input, &values)
|
||||
for element in input {
|
||||
try update(InputOrigin(elements: [.interactive]), name, element, &values)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -80,4 +81,48 @@ extension CommandParser {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate mutating func getNormalValue(label: String) -> [String] {
|
||||
print("? Please enter '\(label)': ", terminator: "")
|
||||
return getInput()?.components(separatedBy: " ") ?? [""]
|
||||
}
|
||||
|
||||
fileprivate mutating func getCaseIterableEnum(label: String, allValues: [String]) -> [String] {
|
||||
print("? Please select '\(label)': ", terminator: "")
|
||||
let strs = getInput()?.components(separatedBy: " ") ?? [""]
|
||||
|
||||
var nums: [String] = []
|
||||
let range = 1 ... allValues.count
|
||||
for str in strs {
|
||||
guard let index = Int(str) else {
|
||||
print("Error: '\(str)' is not a serial number.\n")
|
||||
return getCaseIterableEnum(label: label, allValues: allValues)
|
||||
}
|
||||
|
||||
guard range.contains(index) else {
|
||||
print("Error: '\(index)' is not in the range of \(range) \n")
|
||||
return getCaseIterableEnum(label: label, allValues: allValues)
|
||||
}
|
||||
|
||||
nums.append(allValues[index - 1])
|
||||
}
|
||||
|
||||
if nums.count == 1 {
|
||||
print("You select '\(nums[0])'.\n")
|
||||
} else {
|
||||
print("You select '\(nums.joined(separator: "', '"))'.\n")
|
||||
}
|
||||
|
||||
return nums
|
||||
}
|
||||
|
||||
fileprivate mutating func getInput() -> String? {
|
||||
if lineStack != nil {
|
||||
// Extract the parameters used in the test.
|
||||
return lineStack!.removeLast()
|
||||
} else {
|
||||
// Get values from user input.
|
||||
return readLine()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,50 @@ extension ArgumentInteractiveTests {
|
||||
|
||||
// MARK: -
|
||||
|
||||
private struct ExpressibleValue: ParsableCommand {
|
||||
enum Mode: String, ExpressibleByArgument {
|
||||
case foo, bar, baz
|
||||
}
|
||||
|
||||
@Argument var mode: Mode
|
||||
}
|
||||
|
||||
private struct TransformableValue: ParsableCommand {
|
||||
enum Format: Equatable {
|
||||
case text
|
||||
case other(String)
|
||||
|
||||
init(_ string: String) throws {
|
||||
if string == "text" {
|
||||
self = .text
|
||||
} else {
|
||||
self = .other(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Argument(transform: Format.init) var format: Format
|
||||
}
|
||||
|
||||
extension ArgumentInteractiveTests {
|
||||
func testParsing_ExpressibleValue() throws {
|
||||
AssertParseCommand(ExpressibleValue.self, ExpressibleValue.self, [], lines: ["foo"]) { value in
|
||||
XCTAssertEqual(value.mode, .foo)
|
||||
}
|
||||
}
|
||||
|
||||
func testParsing_TransformableValue() throws {
|
||||
AssertParseCommand(TransformableValue.self, TransformableValue.self, [], lines: ["text"]) { value in
|
||||
XCTAssertEqual(value.format, .text)
|
||||
}
|
||||
AssertParseCommand(TransformableValue.self, TransformableValue.self, [], lines: ["keynote"]) { value in
|
||||
XCTAssertEqual(value.format, .other("keynote"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private struct StringArray: ParsableCommand {
|
||||
@Argument var values: [String]
|
||||
}
|
||||
@@ -80,3 +124,75 @@ extension ArgumentInteractiveTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private struct PositionalArray1: ParsableCommand {
|
||||
@Argument var values: [Int]
|
||||
@Option var count: Int
|
||||
@Flag var verbose = false
|
||||
}
|
||||
|
||||
private struct PositionalArray2: ParsableCommand {
|
||||
@Option var count: Int
|
||||
@Argument var values: [Int]
|
||||
@Flag var verbose = false
|
||||
}
|
||||
|
||||
private struct PositionalArray3: ParsableCommand {
|
||||
@Option var count: Int
|
||||
@Flag var verbose = false
|
||||
@Argument var values: [Int]
|
||||
}
|
||||
|
||||
extension ArgumentInteractiveTests {
|
||||
func testParsing_PositionalArray() throws {
|
||||
AssertParseCommand(PositionalArray1.self, PositionalArray1.self, ["--count", "3", "--verbose"], lines: ["1 2"]) { value in
|
||||
XCTAssertEqual(value.count, 3)
|
||||
XCTAssertEqual(value.verbose, true)
|
||||
XCTAssertEqual(value.values, [1, 2])
|
||||
}
|
||||
|
||||
AssertParseCommand(PositionalArray2.self, PositionalArray2.self, ["--count", "3", "--verbose"], lines: ["1 2"]) { value in
|
||||
XCTAssertEqual(value.count, 3)
|
||||
XCTAssertEqual(value.verbose, true)
|
||||
XCTAssertEqual(value.values, [1, 2])
|
||||
}
|
||||
|
||||
AssertParseCommand(PositionalArray3.self, PositionalArray3.self, ["--count", "3", "--verbose"], lines: ["1 2"]) { value in
|
||||
XCTAssertEqual(value.count, 3)
|
||||
XCTAssertEqual(value.verbose, true)
|
||||
XCTAssertEqual(value.values, [1, 2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private struct CaseIterableArgument: ParsableCommand {
|
||||
enum Mode: String, CaseIterable, ExpressibleByArgument {
|
||||
case foo, bar, baz
|
||||
}
|
||||
|
||||
@Argument var mode: Mode
|
||||
@Argument var modes: [Mode]
|
||||
}
|
||||
|
||||
extension ArgumentInteractiveTests {
|
||||
func testParsing_CaseIterableArgument() throws {
|
||||
AssertParseCommand(CaseIterableArgument.self, CaseIterableArgument.self, ["foo"], lines: ["2 3"]) { value in
|
||||
XCTAssertEqual(value.mode, .foo)
|
||||
XCTAssertEqual(value.modes, [.bar, .baz])
|
||||
}
|
||||
|
||||
AssertParseCommand(CaseIterableArgument.self, CaseIterableArgument.self, [], lines: ["1", "2 3"]) { value in
|
||||
XCTAssertEqual(value.mode, .foo)
|
||||
XCTAssertEqual(value.modes, [.bar, .baz])
|
||||
}
|
||||
|
||||
AssertParseCommand(CaseIterableArgument.self, CaseIterableArgument.self, [], lines: ["foo", "0", "2 3", "1"]) { value in
|
||||
XCTAssertEqual(value.mode, .baz)
|
||||
XCTAssertEqual(value.modes, [.foo])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user