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

@@ -1,21 +1,20 @@
package com.infendro.cli.command
import com.infendro.cli.parser.*
import com.infendro.cli.parser.Parser
sealed class Argument<T>(
sealed class Argument<T : Any>(
val parser: Parser<T>,
) {
abstract val required: Boolean
class Required<T>(
class Required<T : Any>(
parser: Parser<T>,
) : Argument<T>(parser) {
override val required: Boolean
get() = true
}
class OrElse<T>(
class OrElse<T : Any>(
val other: T,
parser: Parser<T>,
) : Argument<T>(parser) {
@@ -23,14 +22,14 @@ sealed class Argument<T>(
get() = false
}
class OrNull<T>(
class OrNull<T : Any>(
parser: Parser<T>,
) : Argument<T>(parser) {
override val required: Boolean
get() = false
}
class Variable<T>(
class Variable<T : Any>(
parser: Parser<T>,
) : Argument<T>(parser) {
override val required: Boolean

View File

@@ -1,5 +1,7 @@
package com.infendro.cli.command
import com.infendro.cli.command.context.Context
import com.infendro.cli.command.context.Parser
import com.infendro.cli.exception.build.*
import com.infendro.cli.exception.run.CliException
import com.infendro.cli.util.Regex.COMMAND
@@ -22,7 +24,7 @@ class Command private constructor(
}
class Builder internal constructor(
private val index: Int,
private val level: Int,
private val name: String?,
) {
private val commands = mutableListOf<Command>()
@@ -30,22 +32,19 @@ class Command private constructor(
private val options = mutableListOf<Option<*>>()
private lateinit var _execute: Context.() -> Unit
private val nextIndex: Int
get() = index + 1
private val key: Key
get() = Key(level)
fun command(name: String? = null, block: Builder.(Key) -> Unit) {
// validate
val last = commands.lastOrNull()
when {
last?.fallback == true -> throw CommandOrderException()
name?.matches(COMMAND) == false -> throw IllegalCommandNameException(name)
last != null && last.fallback -> throw CommandOrderException()
name != null && !name.matches(COMMAND) -> throw IllegalCommandNameException(name)
commands.any { it.name == name } -> throw DuplicateCommandException(name)
}
commands += Builder(nextIndex, name).also {
val key = Key(nextIndex)
it.block(key)
}.build()
commands += Builder(level + 1, name).also { it.block(it.key) }.build()
}
fun argument(argument: Argument<*>) {
@@ -53,7 +52,7 @@ class Command private constructor(
val last = arguments.lastOrNull()
when {
last is Argument.Variable -> throw ArgumentOrderException()
argument.required && last?.required == false -> throw ArgumentOrderException()
argument.required && last != null && !last.required -> throw ArgumentOrderException()
}
arguments += argument
@@ -92,7 +91,7 @@ class Command private constructor(
// validate
val last = commands.lastOrNull()
when {
last?.fallback == true && arguments.isNotEmpty() -> throw InvalidFallbackException()
last != null && last.fallback && arguments.isNotEmpty() -> throw InvalidFallbackException()
}
if (!::_execute.isInitialized)
@@ -107,8 +106,5 @@ class Command private constructor(
)
}
fun cli(block: Command.Builder.() -> Unit): Command {
val builder = Command.Builder(-1, null)
builder.block()
return builder.build()
}
fun cli(block: Command.Builder.() -> Unit) =
Command.Builder(-1, null).also(block).build()

View File

@@ -1,56 +0,0 @@
package com.infendro.cli.command
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> Argument<T>.index: Int
get() = command.arguments.indexOf(this)
@Suppress("UNCHECKED_CAST")
private val <T> Argument<T>.value: T?
get() = arguments.getOrNull(index)?.value as? T
@Suppress("UNCHECKED_CAST")
private val <T> Argument.Variable<T>.values: List<T>
get() = arguments.drop(index).map { it.value as T }
operator fun <T> Argument.Required<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value!!
operator fun <T> Argument.OrElse<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other
operator fun <T> Argument.OrNull<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = value
operator fun <T> Argument.Variable<T>.getValue(thisRef: Any?, property: KProperty<*>): List<T> = values
@Suppress("UNCHECKED_CAST")
private val <T> Option<T>.value: T?
get() = options.firstOrNull { it.name in names }?.value as? T
@Suppress("UNCHECKED_CAST")
private val <T> Option.Variable<T>.values: List<T>
get() = options.filter { it.name in names }.map { it.value as T }
operator fun <T> Option.Required<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value!!
operator fun <T> Option.OrElse<T>.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other
operator fun <T> Option.OrNull<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = value
operator fun <T> Option.Variable<T>.getValue(thisRef: Any?, property: KProperty<*>): List<T> = values
}

View File

@@ -1,13 +1,16 @@
package com.infendro.cli.command
import com.infendro.cli.exception.build.NoOptionNameException
import com.infendro.cli.exception.build.MissingOptionNameException
import com.infendro.cli.parser.*
import com.infendro.cli.parser.Parser
sealed class Option<T>(
val names: List<String>,
sealed class Option<T : Any>(
val parser: Parser<T>,
val fallback: T?,
val names: List<String>,
) {
val flag: Boolean
get() = fallback != null
val name: String
get() = names[0]
@@ -15,89 +18,114 @@ sealed class Option<T>(
init {
if (names.isEmpty())
throw NoOptionNameException()
throw MissingOptionNameException()
}
class Required<T>(
names: List<String>,
class Required<T : Any>(
parser: Parser<T>,
) : Option<T>(names, parser) {
fallback: T?,
names: List<String>,
) : Option<T>(parser, fallback, names) {
constructor(
parser: Parser<T>,
names: List<String>,
) : this(parser, null, names)
override val required: Boolean
get() = true
}
class OrElse<T>(
class OrElse<T : Any>(
parser: Parser<T>,
fallback: T?,
names: List<String>,
val other: T,
parser: Parser<T>,
) : Option<T>(names, parser) {
) : Option<T>(parser, fallback, names) {
constructor(
parser: Parser<T>,
names: List<String>,
other: T,
) : this(parser, null, names, other)
override val required: Boolean
get() = false
}
class OrNull<T>(
names: List<String>,
class OrNull<T : Any>(
parser: Parser<T>,
) : Option<T>(names, parser) {
fallback: T?,
names: List<String>,
) : Option<T>(parser, fallback, names) {
constructor(
parser: Parser<T>,
names: List<String>,
) : this(parser, null, names)
override val required: Boolean
get() = false
}
class Variable<T>(
names: List<String>,
class Variable<T : Any>(
parser: Parser<T>,
) : Option<T>(names, parser) {
fallback: T?,
names: List<String>,
) : Option<T>(parser, fallback, names) {
constructor(
parser: Parser<T>,
names: List<String>,
) : this(parser, null, names)
override val required: Boolean
get() = false
}
companion object {
fun string(vararg names: String) =
Required(names.asList(), StringParser)
Required(StringParser, names.asList())
fun stringOrElse(vararg names: String, other: String) =
OrElse(names.asList(), other, StringParser)
OrElse(StringParser, names.asList(), other)
fun stringOrNull(vararg names: String) =
OrNull(names.asList(), StringParser)
OrNull(StringParser, names.asList())
fun strings(vararg names: String) =
Variable(names.asList(), StringParser)
Variable(StringParser, names.asList())
fun int(vararg names: String) =
Required(names.asList(), IntParser)
Required(IntParser, names.asList())
fun intOrElse(vararg names: String, other: Int) =
OrElse(names.asList(), other, IntParser)
OrElse(IntParser, names.asList(), other)
fun intOrNull(vararg names: String) =
OrNull(names.asList(), IntParser)
OrNull(IntParser, names.asList())
fun ints(vararg names: String) =
Variable(names.asList(), IntParser)
Variable(IntParser, names.asList())
fun boolean(vararg names: String) =
Required(names.asList(), BooleanParser)
Required(BooleanParser, true, names.asList())
fun booleanOrElse(vararg names: String, other: Boolean) =
OrElse(names.asList(), other, BooleanParser)
OrElse(BooleanParser, true, names.asList(), other)
fun booleanOrNull(vararg names: String) =
OrNull(names.asList(), BooleanParser)
OrNull(BooleanParser, true, names.asList())
fun booleans(vararg names: String) =
Variable(names.asList(), BooleanParser)
Variable(BooleanParser, true, names.asList())
inline fun <reified T : Enum<T>> enum(vararg names: String) =
Required(names.asList(), enumParser<T>())
Required(enumParser<T>(), names.asList())
inline fun <reified T : Enum<T>> enumOrElse(vararg names: String, other: T) =
OrElse(names.asList(), other, enumParser<T>())
OrElse(enumParser<T>(), names.asList(), other)
inline fun <reified T : Enum<T>> enumOrNull(vararg names: String) =
OrNull(names.asList(), enumParser<T>())
OrNull(enumParser<T>(), names.asList())
inline fun <reified T : Enum<T>> enums(vararg names: String) =
Variable(names.asList(), enumParser<T>())
Variable(enumParser<T>(), names.asList())
}
}

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

@@ -1,6 +1,9 @@
package com.infendro.cli.command
package com.infendro.cli.command.context
import 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 {
@@ -41,6 +44,7 @@ internal object Parser {
val current = args[i].drop(dashes)
when {
// argument
endOfOptions || dashes == 0 -> {
val argument = command.arguments.getOrNull(argumentIndex)
?: throw TooManyArgumentsException()
@@ -52,6 +56,7 @@ internal object Parser {
arg += Arg(value)
}
// option
dashes in 1..2 -> {
if (dashes == 2 && current.isEmpty()) {
endOfOptions = true
@@ -59,7 +64,7 @@ internal object Parser {
continue
}
val (name, value) = when {
val (name, text) = when {
current.contains('=') -> {
val (name, value) = current.split('=', limit = 2)
name to value
@@ -85,11 +90,16 @@ internal object Parser {
val option = command.options.firstOrNull { name in it.names }
?: throw UnknownOptionException(name)
val value = option.parser.parse(value) as Any
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('=') -> {