package com.infendro.cli.command.context import com.infendro.cli.command.Command import com.infendro.cli.error.run.* import com.infendro.cli.parser.Parser internal class ContextParser( private val root: Command, private val args: Array, ) { sealed class Result { data class Success( val context: Context, ) : Result() data class Help( val path: List, ) : Result() data class Failure( val path: List, val failure: RuntimeFailure, ) : Result() } private fun success(context: Context) = Result.Success(context) private fun help(path: List) = Result.Help(path) private fun failure(path: List, exception: RuntimeFailure) = Result.Failure(path, exception) private var index = 0 private val current: String get() = args[index] private val next: String? get() = args.getOrNull(index) private fun hasNext() = index < args.size private fun consume() { index++ } fun parse(): Result { // command var command = root val path = mutableListOf(root) val commands = mutableMapOf, Any>() while (hasNext()) { if (current.startsWith("-")) break val cmd = command.commands.firstOrNull { (it is Command.Named && it.name == current) || it is Command.Fallback<*> } if (cmd == null) { when { command.arguments.isEmpty() -> return failure(path, UnknownCommand(current)) else -> break } } command = cmd path += cmd if (cmd is Command.Fallback<*>) { val value = when (val result = cmd.parser.parse(current)) { is Parser.Result.Success<*> -> result.value is Parser.Result.Failure<*> -> return failure(path, result.failure) } commands[cmd.key] = value } consume() } // arguments and options val arguments = command.arguments.associateWith { mutableListOf() } val options = command.options.associateWith { mutableListOf() } var endOfOptions = false var argumentIndex = 0 var argumentCount = 0 while (hasNext()) { val dashes = current.takeWhile { it == '-' }.count() val trimmed = current.drop(dashes) when { // argument endOfOptions || dashes == 0 -> { val argument = command.arguments.getOrNull(argumentIndex) ?: return failure(path, UnexpectedArgument()) val value = when (val result = argument.parser.parse(trimmed)) { is Parser.Result.Success<*> -> result.value is Parser.Result.Failure<*> -> return failure(path, result.failure) } arguments[argument]!! += value argumentCount++ if (argumentCount == argument.count) { argumentCount = 0 argumentIndex++ } } // option dashes in 1..2 -> { if (dashes == 2 && trimmed.isEmpty()) { endOfOptions = true consume() continue } if (dashes == 2 && trimmed == "help") return help(path) val (name, text) = when { trimmed.contains('=') -> { val (name, value) = trimmed.split('=', limit = 2) name to value } else -> { if (next?.startsWith('-') == false) { consume() trimmed to next } else { trimmed to null } } } val names = when { dashes == 1 -> name.map { "$it" } else -> listOf(name) } for (name in names) { val option = command.options.firstOrNull { name in it.names } ?: return failure(path, UnknownOption(name)) val value = when { text != null -> when (val result = option.parser.parse(current)) { is Parser.Result.Success<*> -> result.value is Parser.Result.Failure<*> -> return failure(path, result.failure) } option.flag -> option.fallback!! else -> return failure(path, MissingOptionValue(option)) } options[option]!! += value } } // malformed option else -> { when { current.contains('=') -> { val (option) = current.split('=', limit = 2) return failure(path, MalformedOption(option)) } else -> return failure(path, MalformedOption(current)) } } } consume() } // validate arguments for (argument in command.arguments) { val count = arguments[argument]!!.count() if (count !in argument.min..argument.count) return failure(path, InvalidArgumentCount(argument, count)) } // validate options for (option in command.options) { val count = options[option]!!.count() if (count !in option.min..option.count) return failure(path, InvalidOptionCount(option, count)) } val context = Context(path, commands, arguments, options) return success(context) } }