improve parsing and flag Options

This commit is contained in:
2025-12-31 22:45:43 +01:00
parent 06a2d1bdce
commit 0dc064b8cf
15 changed files with 171 additions and 131 deletions

View File

@@ -0,0 +1,59 @@
package com.infendro.cli.command.context
import com.infendro.cli.command.Argument
import com.infendro.cli.command.Command
import com.infendro.cli.command.Option
import kotlin.reflect.KProperty
class Context internal constructor(
val command: Command,
val commands: List<Cmd>,
val options: List<Opt>,
val arguments: List<Arg>,
) {
class Cmd(
val name: String,
)
class Arg(
val value: Any,
)
class Opt(
val name: String,
val value: Any,
)
internal fun execute() = (command.execute)()
operator fun Command.Key.getValue(thisRef: Any?, property: KProperty<*>): String = commands[index].name
private val <T : Any> Argument<T>.index: Int
get() = command.arguments.indexOf(this)
@Suppress("UNCHECKED_CAST")
private val <T : Any> Argument<T>.value: T?
get() = arguments.getOrNull(index)?.value as? T
@Suppress("UNCHECKED_CAST")
private val <T : Any> Argument.Variable<T>.values: List<T>
get() = arguments.drop(index).map { it.value as T }
operator fun <T : Any> Argument.Required<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value!!
operator fun <T : Any> Argument.OrElse<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other
operator fun <T : Any> Argument.OrNull<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = value
operator fun <T : Any> Argument.Variable<T>.getValue(thisRef: Any?, property: KProperty<*>): List<T> = values
@Suppress("UNCHECKED_CAST")
private val <T : Any> Option<T>.value: T?
get() = options.firstOrNull { it.name in names }?.value as? T
@Suppress("UNCHECKED_CAST")
private val <T : Any> Option.Variable<T>.values: List<T>
get() = options.filter { it.name in names }.map { it.value as T }
operator fun <T : Any> Option.Required<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value!!
operator fun <T : Any> Option.OrElse<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other
operator fun <T : Any> Option.OrNull<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = value
operator fun <T : Any> Option.Variable<T>.getValue(thisRef: Any?, property: KProperty<*>): List<T> = values
}

View File

@@ -0,0 +1,130 @@
package com.infendro.cli.command.context
import com.infendro.cli.command.Argument
import com.infendro.cli.command.Command
import com.infendro.cli.command.Option
import com.infendro.cli.command.context.Context.*
import com.infendro.cli.exception.run.*
internal object Parser {
fun parse(root: Command, args: Array<String>): Context {
var command = root
val cmd = mutableListOf<Cmd>()
val arg = mutableListOf<Arg>()
val opt = mutableListOf<Opt>()
var i = 0
// commands
while (i < args.size) {
val current = args[i]
if (current.startsWith("-"))
break
val c = command.commands.firstOrNull { it.name == current || it.fallback }
if (c == null) {
when {
command.arguments.isEmpty() -> throw UnknownCommandException(current)
else -> break
}
}
command = c
cmd += Cmd(current)
i++
}
// arguments and options
var endOfOptions = false
var argumentIndex = 0
while (i < args.size) {
val dashes = args[i].takeWhile { it == '-' }.count()
val current = args[i].drop(dashes)
when {
// argument
endOfOptions || dashes == 0 -> {
val argument = command.arguments.getOrNull(argumentIndex)
?: throw TooManyArgumentsException()
if (argument !is Argument.Variable)
argumentIndex++
val value = argument.parser.parse(current) as Any
arg += Arg(value)
}
// option
dashes in 1..2 -> {
if (dashes == 2 && current.isEmpty()) {
endOfOptions = true
i++
continue
}
val (name, text) = when {
current.contains('=') -> {
val (name, value) = current.split('=', limit = 2)
name to value
}
else -> {
val next = args.getOrNull(i + 1)
if (next != null && !next.startsWith('-')) {
i++
current to next
} else {
current 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 }
?: throw UnknownOptionException(name)
val value = when {
text != null -> option.parser.parse(text)
option.flag -> option.fallback!!
else -> throw MissingOptionValueException(name)
}
opt += Opt(name, value)
}
}
// malformed option
else -> {
when {
args[i].contains('=') -> {
val (option) = args[i].split('=', limit = 2)
throw MalformedOptionException(option)
}
else -> throw MalformedOptionException(args[i])
}
}
}
i++
}
if (arg.size < command.arguments.count { it is Argument.Required })
throw MissingArgumentException()
for (option in command.options) {
val count = opt.count { it.name in option.names }
when {
option is Option.Required && count == 0 -> throw MissingOptionException(option.name)
option !is Option.Variable && count > 1 -> throw DuplicateOptionException(option.name)
}
}
return Context(command, cmd, opt, arg)
}
}