1.4.0 release #9

Merged
Infendro merged 20 commits from develop into main 2026-01-10 07:37:21 +00:00
15 changed files with 171 additions and 131 deletions
Showing only changes of commit 0dc064b8cf - Show all commits

View File

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

View File

@@ -1,5 +1,7 @@
package com.infendro.cli.command 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.build.*
import com.infendro.cli.exception.run.CliException import com.infendro.cli.exception.run.CliException
import com.infendro.cli.util.Regex.COMMAND import com.infendro.cli.util.Regex.COMMAND
@@ -22,7 +24,7 @@ class Command private constructor(
} }
class Builder internal constructor( class Builder internal constructor(
private val index: Int, private val level: Int,
private val name: String?, private val name: String?,
) { ) {
private val commands = mutableListOf<Command>() private val commands = mutableListOf<Command>()
@@ -30,22 +32,19 @@ class Command private constructor(
private val options = mutableListOf<Option<*>>() private val options = mutableListOf<Option<*>>()
private lateinit var _execute: Context.() -> Unit private lateinit var _execute: Context.() -> Unit
private val nextIndex: Int private val key: Key
get() = index + 1 get() = Key(level)
fun command(name: String? = null, block: Builder.(Key) -> Unit) { fun command(name: String? = null, block: Builder.(Key) -> Unit) {
// validate // validate
val last = commands.lastOrNull() val last = commands.lastOrNull()
when { when {
last?.fallback == true -> throw CommandOrderException() last != null && last.fallback -> throw CommandOrderException()
name?.matches(COMMAND) == false -> throw IllegalCommandNameException(name) name != null && !name.matches(COMMAND) -> throw IllegalCommandNameException(name)
commands.any { it.name == name } -> throw DuplicateCommandException(name) commands.any { it.name == name } -> throw DuplicateCommandException(name)
} }
commands += Builder(nextIndex, name).also { commands += Builder(level + 1, name).also { it.block(it.key) }.build()
val key = Key(nextIndex)
it.block(key)
}.build()
} }
fun argument(argument: Argument<*>) { fun argument(argument: Argument<*>) {
@@ -53,7 +52,7 @@ class Command private constructor(
val last = arguments.lastOrNull() val last = arguments.lastOrNull()
when { when {
last is Argument.Variable -> throw ArgumentOrderException() last is Argument.Variable -> throw ArgumentOrderException()
argument.required && last?.required == false -> throw ArgumentOrderException() argument.required && last != null && !last.required -> throw ArgumentOrderException()
} }
arguments += argument arguments += argument
@@ -92,7 +91,7 @@ class Command private constructor(
// validate // validate
val last = commands.lastOrNull() val last = commands.lastOrNull()
when { when {
last?.fallback == true && arguments.isNotEmpty() -> throw InvalidFallbackException() last != null && last.fallback && arguments.isNotEmpty() -> throw InvalidFallbackException()
} }
if (!::_execute.isInitialized) if (!::_execute.isInitialized)
@@ -107,8 +106,5 @@ class Command private constructor(
) )
} }
fun cli(block: Command.Builder.() -> Unit): Command { fun cli(block: Command.Builder.() -> Unit) =
val builder = Command.Builder(-1, null) Command.Builder(-1, null).also(block).build()
builder.block()
return builder.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 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.*
import com.infendro.cli.parser.Parser
sealed class Option<T>( sealed class Option<T : Any>(
val names: List<String>,
val parser: Parser<T>, val parser: Parser<T>,
val fallback: T?,
val names: List<String>,
) { ) {
val flag: Boolean
get() = fallback != null
val name: String val name: String
get() = names[0] get() = names[0]
@@ -15,89 +18,114 @@ sealed class Option<T>(
init { init {
if (names.isEmpty()) if (names.isEmpty())
throw NoOptionNameException() throw MissingOptionNameException()
} }
class Required<T>( class Required<T : Any>(
names: List<String>,
parser: Parser<T>, 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 override val required: Boolean
get() = true get() = true
} }
class OrElse<T>( class OrElse<T : Any>(
parser: Parser<T>,
fallback: T?,
names: List<String>, names: List<String>,
val other: T, val other: T,
) : Option<T>(parser, fallback, names) {
constructor(
parser: Parser<T>, parser: Parser<T>,
) : Option<T>(names, parser) { names: List<String>,
other: T,
) : this(parser, null, names, other)
override val required: Boolean override val required: Boolean
get() = false get() = false
} }
class OrNull<T>( class OrNull<T : Any>(
names: List<String>,
parser: Parser<T>, 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 override val required: Boolean
get() = false get() = false
} }
class Variable<T>( class Variable<T : Any>(
names: List<String>,
parser: Parser<T>, 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 override val required: Boolean
get() = false get() = false
} }
companion object { companion object {
fun string(vararg names: String) = fun string(vararg names: String) =
Required(names.asList(), StringParser) Required(StringParser, names.asList())
fun stringOrElse(vararg names: String, other: String) = fun stringOrElse(vararg names: String, other: String) =
OrElse(names.asList(), other, StringParser) OrElse(StringParser, names.asList(), other)
fun stringOrNull(vararg names: String) = fun stringOrNull(vararg names: String) =
OrNull(names.asList(), StringParser) OrNull(StringParser, names.asList())
fun strings(vararg names: String) = fun strings(vararg names: String) =
Variable(names.asList(), StringParser) Variable(StringParser, names.asList())
fun int(vararg names: String) = fun int(vararg names: String) =
Required(names.asList(), IntParser) Required(IntParser, names.asList())
fun intOrElse(vararg names: String, other: Int) = fun intOrElse(vararg names: String, other: Int) =
OrElse(names.asList(), other, IntParser) OrElse(IntParser, names.asList(), other)
fun intOrNull(vararg names: String) = fun intOrNull(vararg names: String) =
OrNull(names.asList(), IntParser) OrNull(IntParser, names.asList())
fun ints(vararg names: String) = fun ints(vararg names: String) =
Variable(names.asList(), IntParser) Variable(IntParser, names.asList())
fun boolean(vararg names: String) = fun boolean(vararg names: String) =
Required(names.asList(), BooleanParser) Required(BooleanParser, true, names.asList())
fun booleanOrElse(vararg names: String, other: Boolean) = fun booleanOrElse(vararg names: String, other: Boolean) =
OrElse(names.asList(), other, BooleanParser) OrElse(BooleanParser, true, names.asList(), other)
fun booleanOrNull(vararg names: String) = fun booleanOrNull(vararg names: String) =
OrNull(names.asList(), BooleanParser) OrNull(BooleanParser, true, names.asList())
fun booleans(vararg names: String) = fun booleans(vararg names: String) =
Variable(names.asList(), BooleanParser) Variable(BooleanParser, true, names.asList())
inline fun <reified T : Enum<T>> enum(vararg names: String) = 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) = 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) = 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) = 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.* import com.infendro.cli.exception.run.*
internal object Parser { internal object Parser {
@@ -41,6 +44,7 @@ internal object Parser {
val current = args[i].drop(dashes) val current = args[i].drop(dashes)
when { when {
// argument
endOfOptions || dashes == 0 -> { endOfOptions || dashes == 0 -> {
val argument = command.arguments.getOrNull(argumentIndex) val argument = command.arguments.getOrNull(argumentIndex)
?: throw TooManyArgumentsException() ?: throw TooManyArgumentsException()
@@ -52,6 +56,7 @@ internal object Parser {
arg += Arg(value) arg += Arg(value)
} }
// option
dashes in 1..2 -> { dashes in 1..2 -> {
if (dashes == 2 && current.isEmpty()) { if (dashes == 2 && current.isEmpty()) {
endOfOptions = true endOfOptions = true
@@ -59,7 +64,7 @@ internal object Parser {
continue continue
} }
val (name, value) = when { val (name, text) = when {
current.contains('=') -> { current.contains('=') -> {
val (name, value) = current.split('=', limit = 2) val (name, value) = current.split('=', limit = 2)
name to value name to value
@@ -85,11 +90,16 @@ internal object Parser {
val option = command.options.firstOrNull { name in it.names } val option = command.options.firstOrNull { name in it.names }
?: throw UnknownOptionException(name) ?: 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) opt += Opt(name, value)
} }
} }
// malformed option
else -> { else -> {
when { when {
args[i].contains('=') -> { args[i].contains('=') -> {

View File

@@ -0,0 +1,3 @@
package com.infendro.cli.exception.build
class MissingOptionNameException : Exception()

View File

@@ -1,3 +0,0 @@
package com.infendro.cli.exception.build
class NoOptionNameException : Exception()

View File

@@ -0,0 +1,8 @@
package com.infendro.cli.exception.run
class MissingOptionValueException(
val option: String,
) : CliException() {
override val message: String
get() = """no value provided for option "$option""""
}

View File

@@ -3,8 +3,8 @@ package com.infendro.cli.parser
import com.infendro.cli.exception.run.ParseException import com.infendro.cli.exception.run.ParseException
object BooleanParser : Parser<Boolean> { object BooleanParser : Parser<Boolean> {
override fun parse(text: String?): Boolean = when (text) { override fun parse(text: String): Boolean = when (text) {
"true", "t", null -> true "true", "t" -> true
"false", "f" -> false "false", "f" -> false
else -> throw ParseException(text, "Boolean") else -> throw ParseException(text, "Boolean")
} }

View File

@@ -7,7 +7,7 @@ class EnumParser<T : Enum<T>>(
private val name: String, private val name: String,
private val values: List<T>, private val values: List<T>,
) : Parser<T> { ) : Parser<T> {
override fun parse(text: String?): T = try { override fun parse(text: String): T = try {
values.first { it.name == text } values.first { it.name == text }
} catch (_: Exception) { } catch (_: Exception) {
throw ParseException(text, name) throw ParseException(text, name)

View File

@@ -3,8 +3,8 @@ package com.infendro.cli.parser
import com.infendro.cli.exception.run.ParseException import com.infendro.cli.exception.run.ParseException
object IntParser : Parser<Int> { object IntParser : Parser<Int> {
override fun parse(text: String?): Int = try { override fun parse(text: String): Int = try {
text!!.toInt() text.toInt()
} catch (_: Exception) { } catch (_: Exception) {
throw ParseException(text, "Int") throw ParseException(text, "Int")
} }

View File

@@ -1,5 +1,5 @@
package com.infendro.cli.parser package com.infendro.cli.parser
interface Parser<T> { interface Parser<T> {
fun parse(text: String?): T fun parse(text: String): T
} }

View File

@@ -1,7 +1,5 @@
package com.infendro.cli.parser package com.infendro.cli.parser
import com.infendro.cli.exception.run.ParseException
object StringParser : Parser<String> { object StringParser : Parser<String> {
override fun parse(text: String?): String = text ?: throw ParseException(text, "String") override fun parse(text: String): String = text
} }

View File

@@ -1,8 +1,6 @@
package com.infendro.cli.util package com.infendro.cli.util
internal object Regex { internal object Regex {
private const val L = "[a-zA-Z]" val COMMAND = Regex("""[a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)*""")
private const val AN = """[a-zA-Z0-9]""" val OPTION = Regex("""[a-zA-Z][a-zA-Z0-9]*(?:[\-.][a-zA-Z0-9]+)*""")
val COMMAND = Regex("""${L}${AN}*(?:-${AN}+)*""")
val OPTION = Regex("""${L}${AN}*(?:[\-.]${AN}+)*""")
} }