implement automatic help

This commit is contained in:
2026-01-07 18:53:35 +01:00
parent 4e9b358ef8
commit ecd255479a
18 changed files with 277 additions and 88 deletions

View File

@@ -10,8 +10,8 @@ import kotlin.reflect.KProperty
class Context internal constructor(
val command: Command,
val commands: List<Cmd>,
val options: List<Opt>,
val arguments: List<Arg>,
val options: List<Opt>,
) {
class Cmd(
val name: String,
@@ -28,6 +28,10 @@ class Context internal constructor(
internal fun execute() = (command.execute)()
fun help() {
command.help?.let { println(it) }
}
operator fun Command.Key.getValue(thisRef: Any?, property: KProperty<*>): String = commands[index].name
private val <T : Any> Argument<T>.index: Int

View File

@@ -4,26 +4,55 @@ import com.infendro.cli.command.Command
import com.infendro.cli.command.context.Context.*
import com.infendro.cli.exception.run.*
internal object Parser {
fun parse(root: Command, args: Array<String>): Context {
internal class Parser(
private val root: Command,
private val args: Array<String>,
) {
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++
}
sealed class Result {
data class Success(
val context: Context,
) : Result()
data class Help(
val command: Command,
) : Result()
data class Failure(
val command: Command,
val exception: RunException,
) : Result()
}
fun success(context: Context) = Result.Success(context)
fun help(command: Command) = Result.Help(command)
fun failure(command: Command, exception: RunException) = Result.Failure(command, exception)
fun parse(): Result {
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]
// command
while (hasNext()) {
if (current.startsWith("-"))
break
val c = command.commands.firstOrNull { it.name == current || it.fallback }
if (c == null) {
when {
command.arguments.isEmpty() -> throw UnknownCommand(current)
command.arguments.isEmpty() -> return failure(command, UnknownCommand(current))
else -> break
}
}
@@ -31,24 +60,25 @@ internal object Parser {
command = c
cmd += Cmd(current)
i++
consume()
}
// arguments and options
var endOfOptions = false
var argumentIndex = 0
var argumentCount = 0
while (i < args.size) {
val dashes = args[i].takeWhile { it == '-' }.count()
val current = args[i].drop(dashes)
while (hasNext()) {
val dashes = current.takeWhile { it == '-' }.count()
val trimmed = current.drop(dashes)
when {
// argument
endOfOptions || dashes == 0 -> {
val argument = command.arguments.getOrNull(argumentIndex)
?: throw UnexpectedArgument()
?: return failure(command, UnexpectedArgument())
val value = argument.parser.parse(current)
val value = argument.parser.parse(trimmed)
arg += Arg(value)
argumentCount++
@@ -60,25 +90,26 @@ internal object Parser {
// option
dashes in 1..2 -> {
if (dashes == 2 && current.isEmpty()) {
if (dashes == 2 && trimmed.isEmpty()) {
endOfOptions = true
i++
consume()
continue
}
if (dashes == 2 && trimmed == "help")
return help(command)
val (name, text) = when {
current.contains('=') -> {
val (name, value) = current.split('=', limit = 2)
trimmed.contains('=') -> {
val (name, value) = trimmed.split('=', limit = 2)
name to value
}
else -> {
val next = args.getOrNull(i + 1)
if (next != null && !next.startsWith('-')) {
i++
current to next
if (next?.startsWith('-') == false) {
consume()
trimmed to next
} else {
current to null
trimmed to null
}
}
}
@@ -90,12 +121,12 @@ internal object Parser {
for (name in names) {
val option = command.options.firstOrNull { name in it.names }
?: throw UnknownOption(name)
?: return failure(command, UnknownOption(name))
val value = when {
text != null -> option.parser.parse(text)
option.flag -> option.fallback!!
else -> throw MissingOptionValue(option)
else -> return failure(command, MissingOptionValue(option))
}
opt += Opt(name, value)
}
@@ -104,40 +135,36 @@ internal object Parser {
// malformed option
else -> {
when {
args[i].contains('=') -> {
val (option) = args[i].split('=', limit = 2)
throw MalformedOption(option)
current.contains('=') -> {
val (option) = current.split('=', limit = 2)
return failure(command, MalformedOption(option))
}
else -> throw MalformedOption(args[i])
else -> return failure(command, MalformedOption(current))
}
}
}
i++
consume()
}
validateArguments(command, arg)
validateOptions(command, opt)
return Context(command, cmd, opt, arg)
}
private fun validateArguments(command: Command, arg: List<Arg>) {
// validate arguments
val required = command.arguments.filter { it.required }
var consumed = 0
for (argument in required) {
val needed = if (argument === required.last()) argument.min else argument.count
val delta = arg.size - consumed
if (delta < needed) throw InvalidArgumentCount(argument, delta)
if (delta < needed) return failure(command, InvalidArgumentCount(argument, delta))
consumed += needed
}
}
private fun validateOptions(command: Command, opt: List<Opt>) {
// validate options
for (option in command.options) {
val count = opt.count { it.name in option.names }
if (count !in option.min..option.count) throw InvalidOptionCount(option, count)
if (count !in option.min..option.count) return failure(command, InvalidOptionCount(option, count))
}
val context = Context(command, cmd, arg, opt)
return success(context)
}
}