184 lines
6.3 KiB
Kotlin
184 lines
6.3 KiB
Kotlin
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<String>,
|
|
) {
|
|
sealed class Result {
|
|
data class Success(
|
|
val context: Context,
|
|
) : Result()
|
|
|
|
data class Help(
|
|
val path: List<Command>,
|
|
) : Result()
|
|
|
|
data class Failure(
|
|
val path: List<Command>,
|
|
val failure: RuntimeFailure,
|
|
) : Result()
|
|
}
|
|
|
|
private fun success(context: Context) = Result.Success(context)
|
|
private fun help(path: List<Command>) = Result.Help(path)
|
|
private fun failure(path: List<Command>, 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 + 1)
|
|
|
|
private fun hasNext() = index < args.size
|
|
private fun consume() {
|
|
index++
|
|
}
|
|
|
|
fun parse(): Result {
|
|
// command
|
|
var command = root
|
|
val path = mutableListOf(root)
|
|
val commands = mutableMapOf<Command.Fallback.Key<*>, 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(command, 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<Any>() }
|
|
val options = command.options.associateWith { mutableListOf<Any>() }
|
|
|
|
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(current))
|
|
|
|
val value = when (val result = argument.parser.parse(current)) {
|
|
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 current
|
|
} 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(command, name))
|
|
|
|
val value = when {
|
|
text != null ->
|
|
when (val result = option.parser.parse(text)) {
|
|
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)
|
|
}
|
|
}
|