1.4.0 release #9
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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('=') -> {
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.infendro.cli.exception.build
|
||||
|
||||
class MissingOptionNameException : Exception()
|
||||
@@ -1,3 +0,0 @@
|
||||
package com.infendro.cli.exception.build
|
||||
|
||||
class NoOptionNameException : Exception()
|
||||
@@ -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""""
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package com.infendro.cli.parser
|
||||
import com.infendro.cli.exception.run.ParseException
|
||||
|
||||
object BooleanParser : Parser<Boolean> {
|
||||
override fun parse(text: String?): Boolean = when (text) {
|
||||
"true", "t", null -> true
|
||||
override fun parse(text: String): Boolean = when (text) {
|
||||
"true", "t" -> true
|
||||
"false", "f" -> false
|
||||
else -> throw ParseException(text, "Boolean")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ class EnumParser<T : Enum<T>>(
|
||||
private val name: String,
|
||||
private val values: List<T>,
|
||||
) : Parser<T> {
|
||||
override fun parse(text: String?): T = try {
|
||||
override fun parse(text: String): T = try {
|
||||
values.first { it.name == text }
|
||||
} catch (_: Exception) {
|
||||
throw ParseException(text, name)
|
||||
|
||||
@@ -3,8 +3,8 @@ package com.infendro.cli.parser
|
||||
import com.infendro.cli.exception.run.ParseException
|
||||
|
||||
object IntParser : Parser<Int> {
|
||||
override fun parse(text: String?): Int = try {
|
||||
text!!.toInt()
|
||||
override fun parse(text: String): Int = try {
|
||||
text.toInt()
|
||||
} catch (_: Exception) {
|
||||
throw ParseException(text, "Int")
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.infendro.cli.parser
|
||||
|
||||
interface Parser<T> {
|
||||
fun parse(text: String?): T
|
||||
fun parse(text: String): T
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.infendro.cli.parser
|
||||
|
||||
import com.infendro.cli.exception.run.ParseException
|
||||
|
||||
object StringParser : Parser<String> {
|
||||
override fun parse(text: String?): String = text ?: throw ParseException(text, "String")
|
||||
override fun parse(text: String): String = text
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.infendro.cli.util
|
||||
|
||||
internal object Regex {
|
||||
private const val L = "[a-zA-Z]"
|
||||
private const val AN = """[a-zA-Z0-9]"""
|
||||
val COMMAND = Regex("""${L}${AN}*(?:-${AN}+)*""")
|
||||
val OPTION = Regex("""${L}${AN}*(?:[\-.]${AN}+)*""")
|
||||
val COMMAND = Regex("""[a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)*""")
|
||||
val OPTION = Regex("""[a-zA-Z][a-zA-Z0-9]*(?:[\-.][a-zA-Z0-9]+)*""")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user