diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt b/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt index 3175660..75c3b41 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt @@ -1,21 +1,20 @@ package com.infendro.cli.command import com.infendro.cli.parser.* -import com.infendro.cli.parser.Parser -sealed class Argument( +sealed class Argument( val parser: Parser, ) { abstract val required: Boolean - class Required( + class Required( parser: Parser, ) : Argument(parser) { override val required: Boolean get() = true } - class OrElse( + class OrElse( val other: T, parser: Parser, ) : Argument(parser) { @@ -23,14 +22,14 @@ sealed class Argument( get() = false } - class OrNull( + class OrNull( parser: Parser, ) : Argument(parser) { override val required: Boolean get() = false } - class Variable( + class Variable( parser: Parser, ) : Argument(parser) { override val required: Boolean diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt index 36794ad..877c916 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt @@ -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() @@ -30,22 +32,19 @@ class Command private constructor( private val options = mutableListOf>() 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() diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Context.kt b/src/commonMain/kotlin/com/infendro/cli/command/Context.kt deleted file mode 100644 index 9801d79..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/command/Context.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.infendro.cli.command - -import kotlin.reflect.KProperty - -class Context internal constructor( - val command: Command, - val commands: List, - val options: List, - val arguments: List, -) { - 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 Argument.index: Int - get() = command.arguments.indexOf(this) - - @Suppress("UNCHECKED_CAST") - private val Argument.value: T? - get() = arguments.getOrNull(index)?.value as? T - - @Suppress("UNCHECKED_CAST") - private val Argument.Variable.values: List - get() = arguments.drop(index).map { it.value as T } - - operator fun Argument.Required.getValue(thisRef: Any?, property: KProperty<*>): T = value!! - operator fun Argument.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other - operator fun Argument.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = value - operator fun Argument.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = values - - @Suppress("UNCHECKED_CAST") - private val Option.value: T? - get() = options.firstOrNull { it.name in names }?.value as? T - - @Suppress("UNCHECKED_CAST") - private val Option.Variable.values: List - get() = options.filter { it.name in names }.map { it.value as T } - - operator fun Option.Required.getValue(thisRef: Any?, property: KProperty<*>): T = value!! - operator fun Option.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other - operator fun Option.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = value - operator fun Option.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = values -} diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt b/src/commonMain/kotlin/com/infendro/cli/command/Option.kt index 23f9af6..37e6eac 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Option.kt @@ -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( - val names: List, +sealed class Option( val parser: Parser, + val fallback: T?, + val names: List, ) { + val flag: Boolean + get() = fallback != null + val name: String get() = names[0] @@ -15,89 +18,114 @@ sealed class Option( init { if (names.isEmpty()) - throw NoOptionNameException() + throw MissingOptionNameException() } - class Required( - names: List, + class Required( parser: Parser, - ) : Option(names, parser) { + fallback: T?, + names: List, + ) : Option(parser, fallback, names) { + constructor( + parser: Parser, + names: List, + ) : this(parser, null, names) + override val required: Boolean get() = true } - class OrElse( + class OrElse( + parser: Parser, + fallback: T?, names: List, val other: T, - parser: Parser, - ) : Option(names, parser) { + ) : Option(parser, fallback, names) { + constructor( + parser: Parser, + names: List, + other: T, + ) : this(parser, null, names, other) + override val required: Boolean get() = false } - class OrNull( - names: List, + class OrNull( parser: Parser, - ) : Option(names, parser) { + fallback: T?, + names: List, + ) : Option(parser, fallback, names) { + constructor( + parser: Parser, + names: List, + ) : this(parser, null, names) + override val required: Boolean get() = false } - class Variable( - names: List, + class Variable( parser: Parser, - ) : Option(names, parser) { + fallback: T?, + names: List, + ) : Option(parser, fallback, names) { + constructor( + parser: Parser, + names: List, + ) : 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 > enum(vararg names: String) = - Required(names.asList(), enumParser()) + Required(enumParser(), names.asList()) inline fun > enumOrElse(vararg names: String, other: T) = - OrElse(names.asList(), other, enumParser()) + OrElse(enumParser(), names.asList(), other) inline fun > enumOrNull(vararg names: String) = - OrNull(names.asList(), enumParser()) + OrNull(enumParser(), names.asList()) inline fun > enums(vararg names: String) = - Variable(names.asList(), enumParser()) + Variable(enumParser(), names.asList()) } } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt b/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt new file mode 100644 index 0000000..037d2e4 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt @@ -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, + val options: List, + val arguments: List, +) { + 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 Argument.index: Int + get() = command.arguments.indexOf(this) + + @Suppress("UNCHECKED_CAST") + private val Argument.value: T? + get() = arguments.getOrNull(index)?.value as? T + + @Suppress("UNCHECKED_CAST") + private val Argument.Variable.values: List + get() = arguments.drop(index).map { it.value as T } + + operator fun Argument.Required.getValue(thisRef: Any?, property: KProperty<*>): T = value!! + operator fun Argument.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other + operator fun Argument.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = value + operator fun Argument.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = values + + @Suppress("UNCHECKED_CAST") + private val Option.value: T? + get() = options.firstOrNull { it.name in names }?.value as? T + + @Suppress("UNCHECKED_CAST") + private val Option.Variable.values: List + get() = options.filter { it.name in names }.map { it.value as T } + + operator fun Option.Required.getValue(thisRef: Any?, property: KProperty<*>): T = value!! + operator fun Option.OrElse.getValue(thisRef: Any?, property: KProperty<*>): T = value ?: other + operator fun Option.OrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = value + operator fun Option.Variable.getValue(thisRef: Any?, property: KProperty<*>): List = values +} diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Parser.kt b/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt similarity index 85% rename from src/commonMain/kotlin/com/infendro/cli/command/Parser.kt rename to src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt index 9e95189..d6a8e7b 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Parser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt @@ -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('=') -> { diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionNameException.kt new file mode 100644 index 0000000..bd969f8 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionNameException.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class MissingOptionNameException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/NoOptionNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/NoOptionNameException.kt deleted file mode 100644 index 475aca6..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/NoOptionNameException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception.build - -class NoOptionNameException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValueException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValueException.kt new file mode 100644 index 0000000..24b2cb7 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValueException.kt @@ -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"""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt index f60da95..ce894ab 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt @@ -3,8 +3,8 @@ package com.infendro.cli.parser import com.infendro.cli.exception.run.ParseException object BooleanParser : Parser { - 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") } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt index df04cbf..b4247a4 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt @@ -7,7 +7,7 @@ class EnumParser>( private val name: String, private val values: List, ) : Parser { - 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) diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt index 1ea0e77..750920a 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt @@ -3,8 +3,8 @@ package com.infendro.cli.parser import com.infendro.cli.exception.run.ParseException object IntParser : Parser { - override fun parse(text: String?): Int = try { - text!!.toInt() + override fun parse(text: String): Int = try { + text.toInt() } catch (_: Exception) { throw ParseException(text, "Int") } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt index 44626f8..fc15d53 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt @@ -1,5 +1,5 @@ package com.infendro.cli.parser interface Parser { - fun parse(text: String?): T + fun parse(text: String): T } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt index 3387c03..17248d5 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt @@ -1,7 +1,5 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException - object StringParser : Parser { - override fun parse(text: String?): String = text ?: throw ParseException(text, "String") + override fun parse(text: String): String = text } diff --git a/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt b/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt index aebe5f6..1eeb52c 100644 --- a/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt +++ b/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt @@ -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]+)*""") }