Files
cli.kt/src/commonMain/kotlin/com/infendro/cli/command/Command.kt
2026-01-10 09:32:55 +01:00

185 lines
6.1 KiB
Kotlin

package com.infendro.cli.command
import com.infendro.cli.Dsl
import com.infendro.cli.argument.Argument
import com.infendro.cli.command.context.Context
import com.infendro.cli.command.context.ContextParser
import com.infendro.cli.command.help.DefaultHelpRenderer
import com.infendro.cli.command.help.HelpRenderer
import com.infendro.cli.option.Option
import com.infendro.cli.error.build.*
import com.infendro.cli.parser.Parser
import com.infendro.cli.util.Regex
sealed class Command private constructor(
val name: String,
val description: String?,
val commands: List<Command>,
val arguments: List<Argument<*>>,
val options: List<Option<*>>,
internal val execute: Context.() -> Unit,
internal val renderer: HelpRenderer,
) {
init {
if (!name.matches(Regex.COMMAND)) throw InvalidCommandName(name)
}
fun run(vararg args: String) {
when (val result = ContextParser(this, arrayOf(*args)).parse()) {
is ContextParser.Result.Success -> result.context.execute()
is ContextParser.Result.Help -> {
val command = result.path.last()
val help = command.renderer.render(result.path)
if (help != null) print(help)
}
is ContextParser.Result.Failure -> {
println("error: ${result.failure.message}")
val command = result.path.last()
val help = command.renderer.render(result.path)
if (help != null) print("\n$help")
}
}
}
class Named(
name: String,
description: String?,
commands: List<Command>,
arguments: List<Argument<*>>,
options: List<Option<*>>,
execute: Context.() -> Unit,
renderer: HelpRenderer,
) : Command(name, description, commands, arguments, options, execute, renderer) {
class Builder(
name: String,
description: String?,
) : Command.Builder<Named>(name, description) {
override fun create() =
Named(name, description, commands, arguments, options, execute, renderer)
}
}
class Fallback<T : Any>(
val key: Key<T>,
val parser: Parser<T>,
name: String,
description: String?,
commands: List<Command>,
arguments: List<Argument<*>>,
options: List<Option<*>>,
execute: Context.() -> Unit,
renderer: HelpRenderer,
) : Command(name, description, commands, arguments, options, execute, renderer) {
class Builder<T : Any>(
val parser: Parser<T>,
name: String,
description: String?,
) : Command.Builder<Fallback<T>>(name, description) {
internal val key = Key<T>()
override fun create() =
Fallback(key, parser, name, description, commands, arguments, options, execute, renderer)
}
class Key<T>
}
@Dsl
sealed class Builder<COMMAND : Command>(
protected val name: String,
protected val description: String?,
) {
protected val commands = mutableListOf<Command>()
protected val arguments = mutableListOf<Argument<*>>()
protected val options = mutableListOf<Option<*>>()
private var _execute: (Context.() -> Unit)? = null
private var _renderer: HelpRenderer? = null
protected val execute: Context.() -> Unit
get() = _execute!!
protected val renderer: HelpRenderer
get() = _renderer!!
fun register(command: Command) {
validateCommand(command)
commands += command
}
fun register(vararg commands: Command) = commands.forEach(::register)
operator fun <T : Command> T.unaryPlus() = also(::register)
fun register(argument: Argument<*>) {
validateArgument(argument)
arguments += argument
}
fun register(vararg arguments: Argument<*>) = arguments.forEach(::register)
operator fun <T : Argument<*>> T.unaryPlus() = also(::register)
fun register(option: Option<*>) {
validateOption(option)
options += option
}
fun register(vararg options: Option<*>) = options.forEach(::register)
operator fun <T : Option<*>> T.unaryPlus() = also(::register)
fun execute(block: Context.() -> Unit) {
validateExecute()
_execute = block
}
fun renderer(renderer: HelpRenderer) {
validateRenderer()
_renderer = renderer
}
internal abstract fun create(): COMMAND
internal fun build(): Command {
if (_execute == null) _execute = {}
if (_renderer == null) _renderer = DefaultHelpRenderer
validate()
return create()
}
private fun validateCommand(command: Command) {
if (commands.isNotEmpty()) {
val last = commands.last()
if (last is Fallback<*>) throw InvalidCommandOrder()
}
if (commands.any { it.name == command.name }) throw DuplicateCommand(command)
}
private fun validateArgument(argument: Argument<*>) {
if (arguments.isNotEmpty()) {
val last = arguments.last()
when {
last.unbounded -> throw InvalidArgumentOrder()
argument.required && last.optional -> throw InvalidArgumentOrder()
}
}
if (arguments.any { it.name == argument.name }) throw DuplicateArgument(argument)
}
private fun validateOption(option: Option<*>) {
for (name in option.names) {
if (options.any { name in it.names }) throw DuplicateOption(option)
}
}
private fun validateExecute() {
if (_execute != null) throw DuplicateExecute()
}
private fun validateRenderer() {
if (_renderer != null) throw DuplicateRenderer()
}
private fun validate() {
if (commands.any { it is Fallback<*> } && arguments.isNotEmpty()) throw InvalidFallback()
}
}
}