implement optional variable argument/option ranges, refactor exception handling

adapt required/optional argument handling
implement bounded/unbounded variable argument handling
This commit is contained in:
2026-01-06 22:47:21 +01:00
parent 8e491751f5
commit 4e9b358ef8
49 changed files with 315 additions and 231 deletions

View File

@@ -31,7 +31,7 @@ class Context internal constructor(
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)
get() = command.arguments.takeWhile { it != this }.sumOf { it.count }
@Suppress("UNCHECKED_CAST")
private val <T : Any> Argument<T>.value: T?
@@ -39,7 +39,7 @@ class Context internal constructor(
@Suppress("UNCHECKED_CAST")
private val <T : Any> Argument.Variable<T>.values: List<T>
get() = arguments.drop(index).map { it.value as T }
get() = arguments.drop(index).take(count).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

View File

@@ -1,8 +1,6 @@
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.*
@@ -25,7 +23,7 @@ internal object Parser {
val c = command.commands.firstOrNull { it.name == current || it.fallback }
if (c == null) {
when {
command.arguments.isEmpty() -> throw UnknownCommandException(current)
command.arguments.isEmpty() -> throw UnknownCommand(current)
else -> break
}
}
@@ -39,6 +37,7 @@ internal object Parser {
// 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)
@@ -47,13 +46,16 @@ internal object Parser {
// argument
endOfOptions || dashes == 0 -> {
val argument = command.arguments.getOrNull(argumentIndex)
?: throw TooManyArgumentsException()
?: throw UnexpectedArgument()
if (argument !is Argument.Variable)
argumentIndex++
val value = argument.parser.parse(current) as Any
val value = argument.parser.parse(current)
arg += Arg(value)
argumentCount++
if (argumentCount == argument.count) {
argumentCount = 0
argumentIndex++
}
}
// option
@@ -88,12 +90,12 @@ internal object Parser {
for (name in names) {
val option = command.options.firstOrNull { name in it.names }
?: throw UnknownOptionException(name)
?: throw UnknownOption(name)
val value = when {
text != null -> option.parser.parse(text)
option.flag -> option.fallback!!
else -> throw MissingOptionValueException(name)
else -> throw MissingOptionValue(option)
}
opt += Opt(name, value)
}
@@ -104,9 +106,9 @@ internal object Parser {
when {
args[i].contains('=') -> {
val (option) = args[i].split('=', limit = 2)
throw MalformedOptionException(option)
throw MalformedOption(option)
}
else -> throw MalformedOptionException(args[i])
else -> throw MalformedOption(args[i])
}
}
}
@@ -114,17 +116,28 @@ internal object Parser {
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)
}
}
validateArguments(command, arg)
validateOptions(command, opt)
return Context(command, cmd, opt, arg)
}
private fun validateArguments(command: Command, arg: List<Arg>) {
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)
consumed += needed
}
}
private fun validateOptions(command: Command, opt: List<Opt>) {
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)
}
}
}