From 74a93246608dc5b4d72034f4f544b5c36b91745c Mon Sep 17 00:00:00 2001 From: Infendro Date: Wed, 31 Dec 2025 18:42:22 +0100 Subject: [PATCH] rename Arguments to Options, implement Arguments --- .../com/infendro/cli/argument/Argument.kt | 44 ------- .../com/infendro/cli/argument/Boolean.kt | 30 ----- .../kotlin/com/infendro/cli/argument/Int.kt | 33 ----- .../com/infendro/cli/argument/String.kt | 27 ---- .../com/infendro/cli/command/Argument.kt | 79 ++++++++++++ .../com/infendro/cli/command/Command.kt | 110 +++++++++------- .../com/infendro/cli/command/Context.kt | 56 ++++++++ .../com/infendro/cli/command/Execution.kt | 94 -------------- .../kotlin/com/infendro/cli/command/Option.kt | 93 ++++++++++++++ .../kotlin/com/infendro/cli/command/Parser.kt | 120 ++++++++++++++++++ .../infendro/cli/exception/CliException.kt | 3 - .../exception/build/ArgumentOrderException.kt | 3 + ...eException.kt => CommandOrderException.kt} | 2 +- .../build/DuplicateArgumentException.kt | 5 - ...ception.kt => DuplicateOptionException.kt} | 2 +- .../build/IllegalOptionNameException.kt | 5 + .../build/InvalidFallbackException.kt | 3 + .../build/NoArgumentNameException.kt | 3 - .../exception/build/NoOptionNameException.kt | 3 + .../cli/exception/run/CliException.kt | 3 + .../run/DuplicateArgumentException.kt | 10 -- .../exception/run/DuplicateOptionException.kt | 8 ++ .../run/MalformedArgumentException.kt | 10 -- .../exception/run/MalformedOptionException.kt | 8 ++ .../exception/run/MissingArgumentException.kt | 8 +- .../exception/run/MissingOptionException.kt | 8 ++ .../cli/exception/run/ParseException.kt | 2 - .../run/TooManyArgumentsException.kt | 6 + .../exception/run/UnknownArgumentException.kt | 10 -- .../exception/run/UnknownCommandException.kt | 2 - .../exception/run/UnknownOptionException.kt | 8 ++ .../com/infendro/cli/parser/BooleanParser.kt | 11 ++ .../com/infendro/cli/parser/IntParser.kt | 11 ++ .../kotlin/com/infendro/cli/parser/Parser.kt | 5 + .../com/infendro/cli/parser/StringParser.kt | 7 + .../kotlin/com/infendro/cli/util/Regex.kt | 6 +- 36 files changed, 506 insertions(+), 332 deletions(-) delete mode 100644 src/commonMain/kotlin/com/infendro/cli/argument/Argument.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/argument/Boolean.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/argument/Int.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/argument/String.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/command/Argument.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/command/Context.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/command/Execution.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/command/Option.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/command/Parser.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/ArgumentOrderException.kt rename src/commonMain/kotlin/com/infendro/cli/exception/build/{NoExecuteException.kt => CommandOrderException.kt} (50%) delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateArgumentException.kt rename src/commonMain/kotlin/com/infendro/cli/exception/build/{IllegalArgumentNameException.kt => DuplicateOptionException.kt} (68%) create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/NoArgumentNameException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/build/NoOptionNameException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/CliException.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateArgumentException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateOptionException.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedArgumentException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt delete mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownArgumentException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt create mode 100644 src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt diff --git a/src/commonMain/kotlin/com/infendro/cli/argument/Argument.kt b/src/commonMain/kotlin/com/infendro/cli/argument/Argument.kt deleted file mode 100644 index 540b622..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/argument/Argument.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.infendro.cli.argument - -import com.infendro.cli.exception.build.NoArgumentNameException - -sealed class Argument( - val names: List, - val parser: Parser, -) { - abstract val required: Boolean - - val name: String - get() = names[0] - - init { - if (names.isEmpty()) - throw NoArgumentNameException() - } - - interface Parser { - fun parse(text: String?): T - } - - class Single( - names: List, - parser: Parser, - ) : Argument(names, parser) { - override val required: Boolean = true - } - - class SingleOrElse( - names: List, - parser: Parser, - val other: T, - ) : Argument(names, parser) { - override val required: Boolean = false - } - - class SingleOrNull( - names: List, - parser: Parser, - ) : Argument(names, parser) { - override val required: Boolean = false - } -} diff --git a/src/commonMain/kotlin/com/infendro/cli/argument/Boolean.kt b/src/commonMain/kotlin/com/infendro/cli/argument/Boolean.kt deleted file mode 100644 index 776275e..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/argument/Boolean.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.infendro.cli.argument - -import com.infendro.cli.command.Command -import com.infendro.cli.exception.run.ParseException - -object BooleanParser : Argument.Parser { - override fun parse(text: String?): Boolean = try { - text?.toBooleanStrict() ?: true - } catch (_: IllegalArgumentException) { - throw ParseException(text, "Boolean") - } -} - -fun boolean(vararg names: String) = - Argument.Single(names.asList(), BooleanParser) - -fun booleanOrElse(vararg names: String, other: Boolean) = - Argument.SingleOrElse(names.asList(), BooleanParser, other) - -fun booleanOrNull(vararg names: String) = - Argument.SingleOrNull(names.asList(), BooleanParser) - -fun Command.Builder.boolean(vararg names: String) = - Argument.Single(names.asList(), BooleanParser).also { argument(it) } - -fun Command.Builder.booleanOrElse(vararg names: String, other: Boolean) = - Argument.SingleOrElse(names.asList(), BooleanParser, other).also { argument(it) } - -fun Command.Builder.booleanOrNull(vararg names: String) = - Argument.SingleOrNull(names.asList(), BooleanParser).also { argument(it) } diff --git a/src/commonMain/kotlin/com/infendro/cli/argument/Int.kt b/src/commonMain/kotlin/com/infendro/cli/argument/Int.kt deleted file mode 100644 index eb7f060..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/argument/Int.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.infendro.cli.argument - -import com.infendro.cli.command.Command -import com.infendro.cli.exception.run.ParseException - -object IntParser : Argument.Parser { - override fun parse(text: String?): Int = try { - text!!.toInt() - } catch (e: Exception) { - when (e) { - is NullPointerException, is IllegalArgumentException -> throw ParseException(text, "Int") - else -> throw e - } - } -} - -fun int(vararg names: String) = - Argument.Single(names.asList(), IntParser) - -fun intOrElse(vararg names: String, other: Int) = - Argument.SingleOrElse(names.asList(), IntParser, other) - -fun intOrNull(vararg names: String) = - Argument.SingleOrNull(names.asList(), IntParser) - -fun Command.Builder.int(vararg names: String) = - Argument.Single(names.asList(), IntParser).also { argument(it) } - -fun Command.Builder.intOrElse(vararg names: String, other: Int) = - Argument.SingleOrElse(names.asList(), IntParser, other).also { argument(it) } - -fun Command.Builder.intOrNull(vararg names: String) = - Argument.SingleOrNull(names.asList(), IntParser).also { argument(it) } diff --git a/src/commonMain/kotlin/com/infendro/cli/argument/String.kt b/src/commonMain/kotlin/com/infendro/cli/argument/String.kt deleted file mode 100644 index 5d835a9..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/argument/String.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.infendro.cli.argument - -import com.infendro.cli.command.Command -import com.infendro.cli.exception.run.ParseException - -object StringParser : Argument.Parser { - override fun parse(text: String?): String = - text ?: throw ParseException(text, "String") -} - -fun string(vararg names: String) = - Argument.Single(names.asList(), StringParser) - -fun stringOrElse(vararg names: String, other: String) = - Argument.SingleOrElse(names.asList(), StringParser, other) - -fun stringOrNull(vararg names: String) = - Argument.SingleOrNull(names.asList(), StringParser) - -fun Command.Builder.string(vararg names: String) = - Argument.Single(names.asList(), StringParser).also { argument(it) } - -fun Command.Builder.stringOrElse(vararg names: String, other: String) = - Argument.SingleOrElse(names.asList(), StringParser, other).also { argument(it) } - -fun Command.Builder.stringOrNull(vararg names: String) = - Argument.SingleOrNull(names.asList(), StringParser).also { argument(it) } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt b/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt new file mode 100644 index 0000000..53a4ec7 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt @@ -0,0 +1,79 @@ +package com.infendro.cli.command + +import com.infendro.cli.parser.BooleanParser +import com.infendro.cli.parser.IntParser +import com.infendro.cli.parser.Parser +import com.infendro.cli.parser.StringParser + +sealed class Argument( + val parser: Parser, +) { + abstract val required: Boolean + + class Required( + parser: Parser, + ) : Argument(parser) { + override val required: Boolean + get() = true + } + + class OrElse( + val other: T, + parser: Parser, + ) : Argument(parser) { + override val required: Boolean + get() = false + } + + class OrNull( + parser: Parser, + ) : Argument(parser) { + override val required: Boolean + get() = false + } + + class Variable( + parser: Parser, + ) : Argument(parser) { + override val required: Boolean + get() = false + } + + companion object { + fun string() = + Required(StringParser) + + fun stringOrElse(other: String) = + OrElse(other, StringParser) + + fun stringOrNull() = + OrNull(StringParser) + + fun strings() = + Variable(StringParser) + + fun int() = + Required(IntParser) + + fun intOrElse(other: Int) = + OrElse(other, IntParser) + + fun intOrNull() = + OrNull(IntParser) + + fun ints() = + Variable(IntParser) + + fun boolean() = + Required(BooleanParser) + + fun booleanOrElse(other: Boolean) = + OrElse(other, BooleanParser) + + fun booleanOrNull() = + OrNull(BooleanParser) + + fun booleans() = + Variable(BooleanParser) + } +} diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt index 550bf6d..36794ad 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt @@ -1,35 +1,22 @@ package com.infendro.cli.command -import com.infendro.cli.argument.Argument -import com.infendro.cli.exception.CliException import com.infendro.cli.exception.build.* -import com.infendro.cli.exception.run.UnknownCommandException -import com.infendro.cli.util.Regex.ARGUMENT_NAME -import com.infendro.cli.util.Regex.COMMAND_NAME +import com.infendro.cli.exception.run.CliException +import com.infendro.cli.util.Regex.COMMAND +import com.infendro.cli.util.Regex.OPTION class Command private constructor( val name: String?, val commands: List, + val options: List>, val arguments: List>, - private val block: Execution.() -> Unit, + internal val execute: Context.() -> Unit, ) { + val fallback: Boolean + get() = name == null + fun run(args: Array) = try { - val index = args.withIndex() - .firstOrNull { (_, value) -> value.startsWith("-") } - ?.index ?: args.size - - val cmd = args.slice(0..() private val arguments = mutableListOf>() - private lateinit var block: Execution.() -> Unit + private val options = mutableListOf>() + private lateinit var _execute: Context.() -> Unit private val nextIndex: Int get() = index + 1 fun command(name: String? = null, block: Builder.(Key) -> Unit) { - if (name != null && !name.matches(COMMAND_NAME)) - throw IllegalCommandNameException(name) + // validate + val last = commands.lastOrNull() + when { + last?.fallback == true -> throw CommandOrderException() + name?.matches(COMMAND) == false -> throw IllegalCommandNameException(name) + commands.any { it.name == name } -> throw DuplicateCommandException(name) + } - if (commands.any { it.name == name }) - throw DuplicateCommandException(name) - - val builder = Builder(nextIndex, name) - val key = Key(nextIndex) - builder.block(key) - val command = builder.build() - - commands.add(command) + commands += Builder(nextIndex, name).also { + val key = Key(nextIndex) + it.block(key) + }.build() } fun argument(argument: Argument<*>) { - for (name in argument.names) { - if (!name.matches(ARGUMENT_NAME)) - throw IllegalArgumentNameException(name) - - if (arguments.any { name in it.names }) - throw DuplicateArgumentException(name) + // validate + val last = arguments.lastOrNull() + when { + last is Argument.Variable -> throw ArgumentOrderException() + argument.required && last?.required == false -> throw ArgumentOrderException() } - arguments.add(argument) + arguments += argument } - fun arguments(vararg arguments: Argument<*>) = - arguments.forEach(::argument) + fun > T.register() = also(::argument) - fun execute(block: Execution.() -> Unit) { - if (this::block.isInitialized) + fun arguments(vararg arguments: Argument<*>) = arguments.forEach(::argument) + fun > Array.register() = also(::arguments) + + fun option(option: Option<*>) { + // validate + for (name in option.names) { + when { + !name.matches(OPTION) -> throw IllegalOptionNameException(name) + options.any { name in it.names } -> throw DuplicateOptionException(name) + } + } + + options += option + } + + fun > T.register() = also(::option) + + fun options(vararg options: Option<*>) = options.forEach(::option) + fun > Array.register() = also(::options) + + fun execute(block: Context.() -> Unit) { + if (::_execute.isInitialized) throw DuplicateExecuteException() - this.block = block + _execute = block } fun build(): Command { - if (!this::block.isInitialized) - throw NoExecuteException() + // validate + val last = commands.lastOrNull() + when { + last?.fallback == true && arguments.isNotEmpty() -> throw InvalidFallbackException() + } - return Command(name, commands, arguments, block) + if (!::_execute.isInitialized) + _execute = {} + + return Command(name, commands, options, arguments, _execute) } } diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Context.kt b/src/commonMain/kotlin/com/infendro/cli/command/Context.kt new file mode 100644 index 0000000..9801d79 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/Context.kt @@ -0,0 +1,56 @@ +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/Execution.kt b/src/commonMain/kotlin/com/infendro/cli/command/Execution.kt deleted file mode 100644 index 17a1449..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/command/Execution.kt +++ /dev/null @@ -1,94 +0,0 @@ -package com.infendro.cli.command - -import com.infendro.cli.argument.Argument -import com.infendro.cli.exception.run.DuplicateArgumentException -import com.infendro.cli.exception.run.MalformedArgumentException -import com.infendro.cli.exception.run.MissingArgumentException -import com.infendro.cli.exception.run.UnknownArgumentException -import kotlin.reflect.KProperty - -class Execution private constructor( - val commands: List, - val arguments: List, -) { - companion object { - fun from(command: Command, cmd: List, arg: List): Execution { - val commands = Cmd.from(cmd) - val arguments = Arg.from(command, arg) - return Execution(commands, arguments) - } - } - - class Cmd( - val name: String, - ) { - companion object { - fun from(cmd: List): List = cmd.map { Cmd(it) } - } - } - - class Arg( - val name: String, - val value: Any, - ) { - companion object { - fun from(command: Command, arg: List): List = buildList { - var i = 0 - while (i < arg.size) { - val dashes = arg[i].takeWhile { it == '-' }.count() - val trimmed = arg[i].substring(dashes) - - if (dashes !in 1..2 || trimmed.isEmpty()) - throw MalformedArgumentException(arg[i]) - - val (name, value) = when { - trimmed.contains('=') -> { - val (name, value) = trimmed.split('=', limit = 2) - name to value - } - - else -> { - val next = arg.getOrNull(i + 1) - if (next != null && !next.startsWith('-')) { - i++ - trimmed to next - } else { - trimmed to null - } - } - } - - val names = when { - dashes == 1 -> name.map { "$it" } - else -> listOf(name) - } - - for (name in names) { - val argument = command.arguments.firstOrNull { name in it.names } - ?: throw UnknownArgumentException(name) - - val value = argument.parser.parse(value) as Any - add(Arg(name, value)) - } - i++ - } - - for (argument in command.arguments) { - val count = count { it.name in argument.names } - when { - argument.required && count == 0 -> throw MissingArgumentException(argument.name) - count > 1 -> throw DuplicateArgumentException(argument.name) - } - } - } - } - } - - operator fun Command.Key.getValue(thisRef: Any?, property: KProperty<*>): String = commands[index].name - - @Suppress("UNCHECKED_CAST") - private fun Argument.getValue(): T? = arguments.firstOrNull { it.name in names }?.value as? T - operator fun Argument.Single.getValue(thisRef: Any?, property: KProperty<*>): T = getValue()!! - operator fun Argument.SingleOrElse.getValue(thisRef: Any?, property: KProperty<*>): T = getValue() ?: other - operator fun Argument.SingleOrNull.getValue(thisRef: Any?, property: KProperty<*>): T? = getValue() -} diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt b/src/commonMain/kotlin/com/infendro/cli/command/Option.kt new file mode 100644 index 0000000..8b342b5 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/Option.kt @@ -0,0 +1,93 @@ +package com.infendro.cli.command + +import com.infendro.cli.exception.build.NoOptionNameException +import com.infendro.cli.parser.BooleanParser +import com.infendro.cli.parser.IntParser +import com.infendro.cli.parser.Parser +import com.infendro.cli.parser.StringParser + +sealed class Option( + val names: List, + val parser: Parser, +) { + val name: String + get() = names[0] + + abstract val required: Boolean + + init { + if (names.isEmpty()) + throw NoOptionNameException() + } + + class Required( + names: List, + parser: Parser, + ) : Option(names, parser) { + override val required: Boolean + get() = true + } + + class OrElse( + names: List, + val other: T, + parser: Parser, + ) : Option(names, parser) { + override val required: Boolean + get() = false + } + + class OrNull( + names: List, + parser: Parser, + ) : Option(names, parser) { + override val required: Boolean + get() = false + } + + class Variable( + names: List, + parser: Parser, + ) : Option(names, parser) { + override val required: Boolean + get() = false + } + + companion object { + fun string(vararg names: String) = + Required(names.asList(), StringParser) + + fun stringOrElse(vararg names: String, other: String) = + OrElse(names.asList(), other, StringParser) + + fun stringOrNull(vararg names: String) = + OrNull(names.asList(), StringParser) + + fun strings(vararg names: String) = + Variable(names.asList(), StringParser) + + fun int(vararg names: String) = + Required(names.asList(), IntParser) + + fun intOrElse(vararg names: String, other: Int) = + OrElse(names.asList(), other, IntParser) + + fun intOrNull(vararg names: String) = + OrNull(names.asList(), IntParser) + + fun ints(vararg names: String) = + Variable(names.asList(), IntParser) + + fun boolean(vararg names: String) = + Required(names.asList(), BooleanParser) + + fun booleanOrElse(vararg names: String, other: Boolean) = + OrElse(names.asList(), other, BooleanParser) + + fun booleanOrNull(vararg names: String) = + OrNull(names.asList(), BooleanParser) + + fun booleans(vararg names: String) = + Variable(names.asList(), BooleanParser) + } +} diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Parser.kt b/src/commonMain/kotlin/com/infendro/cli/command/Parser.kt new file mode 100644 index 0000000..9e95189 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/command/Parser.kt @@ -0,0 +1,120 @@ +package com.infendro.cli.command + +import com.infendro.cli.command.Context.* +import com.infendro.cli.exception.run.* + +internal object Parser { + fun parse(root: Command, args: Array): Context { + var command = root + val cmd = mutableListOf() + val arg = mutableListOf() + val opt = mutableListOf() + + var i = 0 + + // commands + while (i < args.size) { + val current = args[i] + + if (current.startsWith("-")) + break + + val c = command.commands.firstOrNull { it.name == current || it.fallback } + if (c == null) { + when { + command.arguments.isEmpty() -> throw UnknownCommandException(current) + else -> break + } + } + + command = c + cmd += Cmd(current) + + i++ + } + + // arguments and options + var endOfOptions = false + var argumentIndex = 0 + while (i < args.size) { + val dashes = args[i].takeWhile { it == '-' }.count() + val current = args[i].drop(dashes) + + when { + endOfOptions || dashes == 0 -> { + val argument = command.arguments.getOrNull(argumentIndex) + ?: throw TooManyArgumentsException() + + if (argument !is Argument.Variable) + argumentIndex++ + + val value = argument.parser.parse(current) as Any + arg += Arg(value) + } + + dashes in 1..2 -> { + if (dashes == 2 && current.isEmpty()) { + endOfOptions = true + i++ + continue + } + + val (name, value) = when { + current.contains('=') -> { + val (name, value) = current.split('=', limit = 2) + name to value + } + + else -> { + val next = args.getOrNull(i + 1) + if (next != null && !next.startsWith('-')) { + i++ + current to next + } else { + current to null + } + } + } + + val names = when { + dashes == 1 -> name.map { "$it" } + else -> listOf(name) + } + + for (name in names) { + val option = command.options.firstOrNull { name in it.names } + ?: throw UnknownOptionException(name) + + val value = option.parser.parse(value) as Any + opt += Opt(name, value) + } + } + + else -> { + when { + args[i].contains('=') -> { + val (option) = args[i].split('=', limit = 2) + throw MalformedOptionException(option) + } + else -> throw MalformedOptionException(args[i]) + } + } + } + + i++ + } + + if (arg.size < command.arguments.count { it is Argument.Required }) + throw MissingArgumentException() + + for (option in command.options) { + val count = opt.count { it.name in option.names } + when { + option is Option.Required && count == 0 -> throw MissingOptionException(option.name) + option !is Option.Variable && count > 1 -> throw DuplicateOptionException(option.name) + } + } + + return Context(command, cmd, opt, arg) + } +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt deleted file mode 100644 index 913de16..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception - -abstract class CliException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/ArgumentOrderException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/ArgumentOrderException.kt new file mode 100644 index 0000000..f04bcf4 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/ArgumentOrderException.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class ArgumentOrderException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/NoExecuteException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/CommandOrderException.kt similarity index 50% rename from src/commonMain/kotlin/com/infendro/cli/exception/build/NoExecuteException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/CommandOrderException.kt index 4fc891b..6ed8242 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/NoExecuteException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/CommandOrderException.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class NoExecuteException : Exception() +class CommandOrderException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateArgumentException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateArgumentException.kt deleted file mode 100644 index 0d5d11d..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateArgumentException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.cli.exception.build - -class DuplicateArgumentException( - val argument: String, -) : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalArgumentNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOptionException.kt similarity index 68% rename from src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalArgumentNameException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOptionException.kt index 3ad2f2d..96ca86d 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalArgumentNameException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOptionException.kt @@ -1,5 +1,5 @@ package com.infendro.cli.exception.build -class IllegalArgumentNameException( +class DuplicateOptionException( val name: String, ) : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt new file mode 100644 index 0000000..88dcc73 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.exception.build + +class IllegalOptionNameException( + val name: String, +) : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt new file mode 100644 index 0000000..07e66ee --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class InvalidFallbackException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/NoArgumentNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/NoArgumentNameException.kt deleted file mode 100644 index b8692ea..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/NoArgumentNameException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception.build - -class NoArgumentNameException : 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 new file mode 100644 index 0000000..475aca6 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/NoOptionNameException.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class NoOptionNameException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/CliException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/CliException.kt new file mode 100644 index 0000000..c84ca0f --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/CliException.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.run + +abstract class CliException : Exception() \ No newline at end of file diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateArgumentException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateArgumentException.kt deleted file mode 100644 index 1e33ac3..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateArgumentException.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.infendro.cli.exception.run - -import com.infendro.cli.exception.CliException - -class DuplicateArgumentException( - val argument: String, -) : CliException() { - override val message: String - get() = """duplicate argument "$argument"""" -} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateOptionException.kt new file mode 100644 index 0000000..d6b84e7 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateOptionException.kt @@ -0,0 +1,8 @@ +package com.infendro.cli.exception.run + +class DuplicateOptionException( + val option: String, +) : CliException() { + override val message: String + get() = """duplicate option "$option"""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedArgumentException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedArgumentException.kt deleted file mode 100644 index 50e39f4..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedArgumentException.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.infendro.cli.exception.run - -import com.infendro.cli.exception.CliException - -class MalformedArgumentException( - val argument: String, -) : CliException() { - override val message: String - get() = """malformed argument "$argument"""" -} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt new file mode 100644 index 0000000..0e97e3d --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt @@ -0,0 +1,8 @@ +package com.infendro.cli.exception.run + +class MalformedOptionException( + val option: String, +) : CliException() { + override val message: String + get() = """malformed option "$option"""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingArgumentException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingArgumentException.kt index 49fab78..fba26c7 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingArgumentException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingArgumentException.kt @@ -1,10 +1,6 @@ package com.infendro.cli.exception.run -import com.infendro.cli.exception.CliException - -class MissingArgumentException( - val argument: String, -) : CliException() { +class MissingArgumentException : CliException() { override val message: String - get() = """missing argument "$argument"""" + get() = """missing argument""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionException.kt new file mode 100644 index 0000000..317956f --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionException.kt @@ -0,0 +1,8 @@ +package com.infendro.cli.exception.run + +class MissingOptionException( + val option: String, +) : CliException() { + override val message: String + get() = """missing option "$option"""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/ParseException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/ParseException.kt index b6966f0..2206a0b 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/ParseException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/ParseException.kt @@ -1,7 +1,5 @@ package com.infendro.cli.exception.run -import com.infendro.cli.exception.CliException - class ParseException( val text: String?, val type: String, diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt new file mode 100644 index 0000000..fe293ff --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt @@ -0,0 +1,6 @@ +package com.infendro.cli.exception.run + +class TooManyArgumentsException : CliException() { + override val message: String + get() = """too many arguments""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownArgumentException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownArgumentException.kt deleted file mode 100644 index 8cf98a2..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownArgumentException.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.infendro.cli.exception.run - -import com.infendro.cli.exception.CliException - -class UnknownArgumentException( - val argument: String, -) : CliException() { - override val message: String - get() = """unknown argument "$argument"""" -} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt index 795a923..5568c26 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt @@ -1,7 +1,5 @@ package com.infendro.cli.exception.run -import com.infendro.cli.exception.CliException - class UnknownCommandException( val command: String, ) : CliException() { diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt new file mode 100644 index 0000000..4fe2e48 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt @@ -0,0 +1,8 @@ +package com.infendro.cli.exception.run + +class UnknownOptionException( + val option: String, +) : CliException() { + override val message: String + get() = """unknown option "$option"""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt new file mode 100644 index 0000000..f60da95 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt @@ -0,0 +1,11 @@ +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 + "false", "f" -> false + else -> throw ParseException(text, "Boolean") + } +} diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt new file mode 100644 index 0000000..1ea0e77 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt @@ -0,0 +1,11 @@ +package com.infendro.cli.parser + +import com.infendro.cli.exception.run.ParseException + +object IntParser : Parser { + 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 new file mode 100644 index 0000000..44626f8 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/parser/Parser.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.parser + +interface Parser { + 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 new file mode 100644 index 0000000..3387c03 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/parser/StringParser.kt @@ -0,0 +1,7 @@ +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") +} diff --git a/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt b/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt index b7cd051..aebe5f6 100644 --- a/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt +++ b/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt @@ -1,6 +1,8 @@ package com.infendro.cli.util internal object Regex { - val COMMAND_NAME = Regex("""[a-z]+(?:-[a-z]+)*""") - val ARGUMENT_NAME = Regex("""[a-z]+(?:[\-.][a-z]+)*""") + 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}+)*""") }