Files
Nate Cook a76f59f446 Use output streams when printing prompts
When deriving input from a non-interactive source, send the prompts
to a null output stream rather than to stdout.
2023-10-07 13:31:56 -05:00

224 lines
8.0 KiB
Swift

//===----------------------------------------------------------*- 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
//
//===----------------------------------------------------------------------===//
enum _EitherOutputStream<A: TextOutputStream, B: TextOutputStream>: TextOutputStream {
case a(A)
case b(B)
mutating func write(_ string: String) {
switch self {
case .a(var stream):
stream.write(string)
self = .a(stream)
case .b(var stream):
stream.write(string)
self = .b(stream)
}
}
init(_ useA: Bool, _ a: A, _ b: B) {
self = useA ? .a(a) : .b(b)
}
}
extension CommandParser {
/// Get input from the user's typing or from the parameters of the test.
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()
}
}
/// Try to fix parsing error by interacting with the user.
/// - Parameters:
/// - 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 {
// Check if it's under test.
if lineStack == nil {
guard Platform.isInteractive else { return false }
}
var output = _EitherOutputStream(
lineStack == nil, Platform.standardOutput, Platform.nullOutput)
guard rootCommand.configuration.shouldPromptForMissing else { return false }
guard let error = error as? ParserError else { return false }
guard case let .missingValueForOption(inputOrigin, name) = error else { return false }
let input = ask(
"? Please enter value for '\(name.synopsisString)': ",
to: &output,
getInput: { getInput() })
let inputIndex = inputOrigin.elements.first!.baseIndex! + 1
split._elements.insert(.init(value: .value(input),
index: .init(inputIndex: .init(rawValue: inputIndex))),
at: inputIndex)
for index in (inputIndex + 1) ..< split.count {
split._elements[index].index = .init(inputIndex: .init(rawValue: index))
}
split.originalInput.insert(input, at: inputIndex)
return true
}
/// Try to fix decoding error by interacting with the user.
/// - Parameters:
/// - error: A decoding error thrown by `ParsableCommand.init(from:)`.
/// - 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 {
// Check if it's under test.
if lineStack == nil {
guard Platform.isInteractive else { return false }
}
var output = _EitherOutputStream(
lineStack == nil, Platform.standardOutput, Platform.nullOutput)
guard rootCommand.configuration.shouldPromptForMissing else { return false }
guard let error = error as? ParserError else { return false }
switch error {
case let .noValue(forKey: key):
let label = key.name
guard label != "generateCompletionScript" else { break }
// Retrieve the correct `ArgumentDefinition` for the required transformation
// before storing the new value received from the user.
let args = arguments.filter { $0.help.keys.contains(key) }
let possibilities: [String] = args.compactMap {
$0.help.visibility.base == .default
? $0.nonOptional.synopsis
: nil
}
if possibilities.count == 1 {
// Missing expected argument
let definition = args.first!
guard case let .unary(update) = definition.update else { break }
let name = definition.names.first
let updateBy: (String) throws -> Void = { string in
try update(InputOrigin(elements: [.interactive]), name, string, &values)
}
// All possible strings that can be converted to value
// of this CaseIterable enum type.
let allValues = definition.help.allValues
if allValues.isEmpty {
storeNormalValues(label: label, updateBy: updateBy, arguments: arguments)
} else {
let selected = choose("? Please select '\(label)': ",
from: allValues,
to: &output,
getInput: { self.getInput() })
let strs = selected.map { allValues[$0] }
for str in strs {
try! update(InputOrigin(elements: [.interactive]), name, str, &values)
}
if values.elements[InputKey(name: label, parent: nil)]!.value is [Any] {
print("You select '\(strs.joined(separator: "', '"))'.\n", to: &output)
} else {
print("You select '\(strs.last!)'.\n", to: &output)
}
}
} else {
// Enumerable Flag
let selected = choose("? Please select '\(label)': ",
from: possibilities,
to: &output,
getInput: { self.getInput() })
let strs = selected.map { possibilities[$0] }
for str in strs {
let definition = args.first { str == "\($0)" }!
guard case let .nullary(update) = definition.update else { continue }
let name = definition.names.first
do {
try update(InputOrigin(elements: [.interactive]), name, &values)
} catch {
print("You select '\(strs[0])'.\n", to: &output)
return true
}
}
print("You select '\(strs.joined(separator: "', '"))'.\n", to: &output)
}
return true
default: break
}
return false
}
fileprivate mutating func storeNormalValues(
label: String,
updateBy: (String) throws -> Void,
arguments: ArgumentSet
) {
var output = _EitherOutputStream(
lineStack == nil, Platform.standardOutput, Platform.nullOutput)
let strs = ask(
"? Please enter '\(label)': ",
type: [String].self,
to: &output,
getInput: { self.getInput() })
for str in strs {
do {
try updateBy(str)
} catch {
// Handle ParserError
guard
let error = error as? ParserError,
case let .unableToParseValue(_, _, original, _, _) = error
else { return }
let generator = ErrorMessageGenerator(arguments: arguments, error: error)
let description = generator.makeErrorMessage() ?? error.localizedDescription
print("Error: " + description + ".\n", to: &output)
replaceInvalidValue(original: original, updateBy: updateBy, arguments: arguments)
}
}
}
fileprivate mutating func replaceInvalidValue(
original: String,
updateBy: (String) throws -> Void,
arguments: ArgumentSet
) {
var output = _EitherOutputStream(
lineStack == nil, Platform.standardOutput, Platform.nullOutput)
let input = ask("? Please replace '\(original)': ", to: &output, getInput: { self.getInput() })
do {
try updateBy(input)
} catch {
// Handle ParserError
guard
let error = error as? ParserError,
case let .unableToParseValue(_, _, original, _, _) = error
else { return }
let generator = ErrorMessageGenerator(arguments: arguments, error: error)
let description = generator.makeErrorMessage() ?? error.localizedDescription
print("Error: " + description + ".\n", to: &output)
replaceInvalidValue(original: original, updateBy: updateBy, arguments: arguments)
}
}
}