mirror of
https://github.com/apple/swift-argument-parser.git
synced 2026-06-06 20:18:23 +00:00
452 lines
16 KiB
Swift
452 lines
16 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
struct CommandError: Error {
|
|
var commandStack: [ParsableCommand.Type]
|
|
var parserError: ParserError
|
|
}
|
|
|
|
struct HelpRequested: Error {
|
|
var visibility: ArgumentVisibility
|
|
}
|
|
|
|
struct CommandParser {
|
|
let commandTree: Tree<ParsableCommand.Type>
|
|
var currentNode: Tree<ParsableCommand.Type>
|
|
var decodedArguments: [DecodedArguments] = []
|
|
/// Used to simulate user input to test interactive mode.
|
|
///
|
|
/// When not optional, an interactive shell will be mocked for testing.
|
|
var lineStack: [String]? = nil
|
|
|
|
var rootCommand: ParsableCommand.Type {
|
|
commandTree.element
|
|
}
|
|
|
|
var commandStack: [ParsableCommand.Type] {
|
|
let result = decodedArguments.compactMap { $0.commandType }
|
|
if currentNode.element == result.last {
|
|
return result
|
|
} else {
|
|
return result + [currentNode.element]
|
|
}
|
|
}
|
|
|
|
internal init(_ rootCommand: ParsableCommand.Type, lines: [String]) {
|
|
self.init(rootCommand)
|
|
self.lineStack = lines
|
|
lineStack?.reverse()
|
|
}
|
|
|
|
init(_ rootCommand: ParsableCommand.Type) {
|
|
do {
|
|
self.commandTree = try Tree(root: rootCommand)
|
|
} catch Tree<ParsableCommand.Type>.InitializationError.recursiveSubcommand(let command) {
|
|
fatalError("The ParsableCommand \"\(command)\" can't have itself as its own subcommand.")
|
|
} catch {
|
|
fatalError("Unexpected error: \(error).")
|
|
}
|
|
self.currentNode = commandTree
|
|
|
|
// A command tree that has a depth greater than zero gets a `help`
|
|
// subcommand.
|
|
if !commandTree.isLeaf {
|
|
commandTree.addChild(Tree(HelpCommand.self))
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CommandParser {
|
|
/// Consumes the next argument in `split` if it matches a subcommand at the
|
|
/// current node of the command tree.
|
|
///
|
|
/// If a matching subcommand is found, the subcommand argument is consumed
|
|
/// in `split`.
|
|
///
|
|
/// - Returns: A node for the matched subcommand if one was found;
|
|
/// otherwise, `nil`.
|
|
fileprivate func consumeNextCommand(split: inout SplitArguments) -> Tree<ParsableCommand.Type>? {
|
|
guard let (origin, element) = split.peekNext(),
|
|
element.isValue,
|
|
let value = split.originalInput(at: origin),
|
|
let subcommandNode = currentNode.firstChild(withName: value)
|
|
else { return nil }
|
|
_ = split.popNextValue()
|
|
return subcommandNode
|
|
}
|
|
|
|
/// Throws a `HelpRequested` error if the user has specified any of the
|
|
/// built-in flags.
|
|
///
|
|
/// - Parameters:
|
|
/// - split: The remaining arguments to examine.
|
|
/// - requireSoloArgument: `true` if the built-in flag must be the only
|
|
/// one remaining for this to catch it.
|
|
func checkForBuiltInFlags(
|
|
_ split: SplitArguments,
|
|
requireSoloArgument: Bool = false
|
|
) throws {
|
|
guard !requireSoloArgument || split.count == 1 else { return }
|
|
|
|
// Look for help flags
|
|
guard !split.contains(anyOf: self.commandStack.getHelpNames(visibility: .default)) else {
|
|
throw HelpRequested(visibility: .default)
|
|
}
|
|
|
|
// Look for help-hidden flags
|
|
guard !split.contains(anyOf: self.commandStack.getHelpNames(visibility: .hidden)) else {
|
|
throw HelpRequested(visibility: .hidden)
|
|
}
|
|
|
|
// Look for dump-help flag
|
|
guard !split.contains(Name.long("experimental-dump-help")) else {
|
|
throw CommandError(commandStack: commandStack, parserError: .dumpHelpRequested)
|
|
}
|
|
|
|
// Look for a version flag if any commands in the stack define a version
|
|
if commandStack.contains(where: { !$0.configuration.version.isEmpty }) {
|
|
guard !split.contains(Name.long("version")) else {
|
|
throw CommandError(commandStack: commandStack, parserError: .versionRequested)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the last parsed value if there are no remaining unused arguments.
|
|
///
|
|
/// If there are remaining arguments or if no commands have been parsed,
|
|
/// this throws an error.
|
|
fileprivate func extractLastParsedValue(_ split: SplitArguments) throws -> ParsableCommand {
|
|
try checkForBuiltInFlags(split)
|
|
|
|
// We should have used up all arguments at this point:
|
|
guard !split.containsNonTerminatorArguments else {
|
|
// Check if one of the arguments is an unknown option
|
|
for element in split.elements {
|
|
if case .option(let argument) = element.value {
|
|
throw ParserError.unknownOption(InputOrigin.Element.argumentIndex(element.index), argument.name)
|
|
}
|
|
}
|
|
|
|
let extra = split.coalescedExtraElements()
|
|
throw ParserError.unexpectedExtraValues(extra)
|
|
}
|
|
|
|
guard let lastCommand = decodedArguments.lazy.compactMap({ $0.command }).last else {
|
|
throw ParserError.invalidState
|
|
}
|
|
|
|
return lastCommand
|
|
}
|
|
|
|
/// Extracts the current command from `split`, throwing if decoding isn't
|
|
/// possible.
|
|
fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws -> ParsableCommand {
|
|
// Parse the arguments, ignoring anything unexpected.
|
|
var parser = LenientParser(currentNode.element, split)
|
|
|
|
func getValues() throws -> ParsedValues {
|
|
do {
|
|
// Parse the arguments, ignoring anything unexpected
|
|
return try parser.parse()
|
|
} catch {
|
|
try checkForBuiltInFlags(split)
|
|
|
|
// Try to fix error by interacting with the user
|
|
guard canInteract(error: error, split: &split) else { throw error }
|
|
parser.inputArguments = split
|
|
return try getValues()
|
|
}
|
|
}
|
|
var values = try getValues()
|
|
|
|
if currentNode.element.includesAllUnrecognizedArgument {
|
|
// If this command includes an all-unrecognized argument, any built-in
|
|
// flags will have been parsed into that argument. Check for flags
|
|
// before decoding.
|
|
try checkForBuiltInFlags(values.capturedUnrecognizedArguments)
|
|
}
|
|
|
|
func getDecoderAndResult() throws -> (ArgumentDecoder, ParsableCommand) {
|
|
// Decode the values from ParsedValues into the ParsableCommand:
|
|
let decoder = ArgumentDecoder(values: values, previouslyDecoded: decodedArguments)
|
|
|
|
do {
|
|
let decodedResult = try currentNode.element.init(from: decoder)
|
|
return (decoder, decodedResult)
|
|
} catch {
|
|
try checkForBuiltInFlags(split)
|
|
|
|
// Try to fix error by interacting with the user.
|
|
if canInteract(error: error, arguments: parser.argumentSet, values: &values) {
|
|
return try getDecoderAndResult()
|
|
} else {
|
|
// Propagating the parsing failure if we aren't interacting.
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
let (decoder, decodedResult) = try getDecoderAndResult()
|
|
|
|
// Decoding was successful, so remove the arguments that were used
|
|
// by the decoder.
|
|
split.removeAll(in: decoder.usedOrigins)
|
|
|
|
// Save the decoded results to add to the next command.
|
|
let newDecodedValues = decoder.previouslyDecoded
|
|
.filter { prev in !decodedArguments.contains(where: { $0.type == prev.type }) }
|
|
decodedArguments.append(contentsOf: newDecodedValues)
|
|
decodedArguments.append(DecodedArguments(type: currentNode.element, value: decodedResult))
|
|
|
|
return decodedResult
|
|
}
|
|
|
|
/// Starting with the current node, extracts commands out of `split` and
|
|
/// descends into subcommands as far as possible.
|
|
internal mutating func descendingParse(_ split: inout SplitArguments) throws {
|
|
while true {
|
|
var parsedCommand = try parseCurrent(&split)
|
|
|
|
// after decoding a command, make sure to validate it
|
|
do {
|
|
try parsedCommand.validate()
|
|
var lastArgument = decodedArguments.removeLast()
|
|
lastArgument.value = parsedCommand
|
|
decodedArguments.append(lastArgument)
|
|
} catch {
|
|
try checkForBuiltInFlags(split)
|
|
throw CommandError(commandStack: commandStack, parserError: ParserError.userValidationError(error))
|
|
}
|
|
|
|
// Look for next command in the argument list.
|
|
if let nextCommand = consumeNextCommand(split: &split) {
|
|
currentNode = nextCommand
|
|
continue
|
|
}
|
|
|
|
// Look for the help flag before falling back to a default command.
|
|
try checkForBuiltInFlags(split, requireSoloArgument: true)
|
|
|
|
// No command was found, so fall back to the default subcommand.
|
|
if let defaultSubcommand = currentNode.element.configuration.defaultSubcommand {
|
|
guard let subcommandNode = currentNode.firstChild(equalTo: defaultSubcommand) else {
|
|
throw ParserError.invalidState
|
|
}
|
|
currentNode = subcommandNode
|
|
continue
|
|
}
|
|
|
|
// No more subcommands to parse.
|
|
return
|
|
}
|
|
}
|
|
|
|
/// Returns the fully-parsed matching command for `arguments`, or an
|
|
/// appropriate error.
|
|
///
|
|
/// - Parameter arguments: The array of arguments to parse. This should not
|
|
/// include the command name as the first argument.
|
|
mutating func parse(arguments: [String]) -> Result<ParsableCommand, CommandError> {
|
|
do {
|
|
try handleCustomCompletion(arguments)
|
|
} catch {
|
|
return .failure(CommandError(commandStack: [commandTree.element], parserError: error as! ParserError))
|
|
}
|
|
|
|
var split: SplitArguments
|
|
do {
|
|
split = try SplitArguments(arguments: arguments)
|
|
} catch let error as ParserError {
|
|
return .failure(CommandError(commandStack: [commandTree.element], parserError: error))
|
|
} catch {
|
|
return .failure(CommandError(commandStack: [commandTree.element], parserError: .invalidState))
|
|
}
|
|
|
|
do {
|
|
try checkForCompletionScriptRequest(&split)
|
|
try descendingParse(&split)
|
|
let result = try extractLastParsedValue(split)
|
|
|
|
// HelpCommand is a valid result, but needs extra information about
|
|
// the tree from the parser to build its stack of commands.
|
|
if var helpResult = result as? HelpCommand {
|
|
try helpResult.buildCommandStack(with: self)
|
|
return .success(helpResult)
|
|
}
|
|
return .success(result)
|
|
} catch let error as CommandError {
|
|
return .failure(error)
|
|
} catch let error as ParserError {
|
|
let error = arguments.isEmpty ? ParserError.noArguments(error) : error
|
|
return .failure(CommandError(commandStack: commandStack, parserError: error))
|
|
} catch let helpRequest as HelpRequested {
|
|
return .success(HelpCommand(
|
|
commandStack: commandStack,
|
|
visibility: helpRequest.visibility))
|
|
} catch {
|
|
return .failure(CommandError(commandStack: commandStack, parserError: .invalidState))
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Completion Script Support
|
|
|
|
struct GenerateCompletions: ParsableCommand {
|
|
@Option() var generateCompletionScript: String
|
|
}
|
|
|
|
struct AutodetectedGenerateCompletions: ParsableCommand {
|
|
@Flag() var generateCompletionScript = false
|
|
}
|
|
|
|
extension CommandParser {
|
|
func checkForCompletionScriptRequest(_ split: inout SplitArguments) throws {
|
|
// Pseudo-commands don't support `--generate-completion-script` flag
|
|
guard rootCommand.configuration._superCommandName == nil else {
|
|
return
|
|
}
|
|
|
|
// We don't have the ability to check for `--name [value]`-style args yet,
|
|
// so we need to try parsing two different commands.
|
|
|
|
// First look for `--generate-completion-script <shell>`
|
|
var completionsParser = CommandParser(GenerateCompletions.self)
|
|
if let result = try? completionsParser.parseCurrent(&split) as? GenerateCompletions {
|
|
throw CommandError(commandStack: commandStack, parserError: .completionScriptRequested(shell: result.generateCompletionScript))
|
|
}
|
|
|
|
// Check for for `--generate-completion-script` without a value
|
|
var autodetectedParser = CommandParser(AutodetectedGenerateCompletions.self)
|
|
if let result = try? autodetectedParser.parseCurrent(&split) as? AutodetectedGenerateCompletions,
|
|
result.generateCompletionScript
|
|
{
|
|
throw CommandError(commandStack: commandStack, parserError: .completionScriptRequested(shell: nil))
|
|
}
|
|
}
|
|
|
|
func handleCustomCompletion(_ arguments: [String]) throws {
|
|
// Completion functions use a custom format:
|
|
//
|
|
// <command> ---completion [<subcommand> ...] -- <argument-name> [<completion-text>]
|
|
//
|
|
// The triple-dash prefix makes '---completion' invalid syntax for regular
|
|
// arguments, so it's safe to use for this internal purpose.
|
|
guard arguments.first == "---completion"
|
|
else { return }
|
|
|
|
var args = arguments.dropFirst()
|
|
var current = commandTree
|
|
while let subcommandName = args.popFirst() {
|
|
// A double dash separates the subcommands from the argument information
|
|
if subcommandName == "--" { break }
|
|
|
|
guard let nextCommandNode = current.firstChild(withName: subcommandName)
|
|
else { throw ParserError.invalidState }
|
|
current = nextCommandNode
|
|
}
|
|
|
|
// Some kind of argument name is the next required element
|
|
guard let argToMatch = args.popFirst() else {
|
|
throw ParserError.invalidState
|
|
}
|
|
// Completion text is optional here
|
|
let completionValues = Array(args)
|
|
|
|
// Generate the argument set and parse the argument to find in the set
|
|
let argset = ArgumentSet(current.element, visibility: .private, parent: nil)
|
|
let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first!
|
|
|
|
// Look up the specified argument and retrieve its custom completion function
|
|
let completionFunction: ([String]) -> [String]
|
|
|
|
switch parsedArgument.value {
|
|
case .option(let parsed):
|
|
guard let matchedArgument = argset.first(matching: parsed),
|
|
case .custom(let f) = matchedArgument.completion.kind
|
|
else { throw ParserError.invalidState }
|
|
completionFunction = f
|
|
|
|
case .value(let str):
|
|
guard let matchedArgument = argset.firstPositional(named: str),
|
|
case .custom(let f) = matchedArgument.completion.kind
|
|
else { throw ParserError.invalidState }
|
|
completionFunction = f
|
|
|
|
case .terminator:
|
|
throw ParserError.invalidState
|
|
}
|
|
|
|
// Parsing and retrieval successful! We don't want to continue with any
|
|
// other parsing here, so after printing the result of the completion
|
|
// function, exit with a success code.
|
|
let output = completionFunction(completionValues).joined(separator: "\n")
|
|
throw ParserError.completionScriptCustomResponse(output)
|
|
}
|
|
}
|
|
|
|
// MARK: Building Command Stacks
|
|
|
|
extension CommandParser {
|
|
/// Builds an array of commands that matches the given command names.
|
|
///
|
|
/// This stops building the stack if it encounters any command names that
|
|
/// aren't in the command tree, so it's okay to pass a list of arbitrary
|
|
/// commands. Will always return at least the root of the command tree.
|
|
func commandStack(for commandNames: [String]) -> [ParsableCommand.Type] {
|
|
var node = commandTree
|
|
var result = [node.element]
|
|
|
|
for name in commandNames {
|
|
guard let nextNode = node.firstChild(withName: name) else {
|
|
// Reached a non-command argument.
|
|
// Ignore anything after this point
|
|
return result
|
|
}
|
|
result.append(nextNode.element)
|
|
node = nextNode
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func commandStack(for subcommand: ParsableCommand.Type) -> [ParsableCommand.Type] {
|
|
let path = commandTree.path(to: subcommand)
|
|
return path.isEmpty
|
|
? [commandTree.element]
|
|
: path
|
|
}
|
|
}
|
|
|
|
extension SplitArguments {
|
|
func contains(_ needle: Name) -> Bool {
|
|
self.elements.contains {
|
|
switch $0.value {
|
|
case .option(.name(let name)),
|
|
.option(.nameWithValue(let name, _)):
|
|
return name == needle
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
func contains(anyOf names: [Name]) -> Bool {
|
|
self.elements.contains {
|
|
switch $0.value {
|
|
case .option(.name(let name)),
|
|
.option(.nameWithValue(let name, _)):
|
|
return names.contains(name)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|