diff --git a/README.md b/README.md index 3d462be..ec38adf 100644 --- a/README.md +++ b/README.md @@ -28,19 +28,20 @@ dependencies { ## Usage ```kotlin -fun main(args: Array) = cli { +fun main(args: Array) = cli("application") { execute { // define execution of command (e.g., display help) + println("Usage: application [arguments] [options]") } // declare command "greet" command("greet") { // declare arguments - val speakersArg = Argument.string().variable().register() + val speakersArg by Argument.string("speakers").variable(min = 1) // declare options - val greetingOpt = Option.string("greeting", "greet", "g").orElse("Hello").register() - val nameOpt = Option.string("name", "n").orNull().register() + val greetingOpt by Option.string("greeting", "greet", "g").orElse("Hello") + val nameOpt by Option.string("name", "n").orNull() execute { // retrieve the arguments and options @@ -55,7 +56,7 @@ fun main(args: Array) = cli { } // declare fallback command - fallback { valueArg -> + fallback("value") { valueArg -> execute { // retrieve the value used for the fallback command val value by valueArg diff --git a/src/commonMain/kotlin/com/infendro/cli/Dsl.kt b/src/commonMain/kotlin/com/infendro/cli/Dsl.kt index 5705e9a..084718c 100644 --- a/src/commonMain/kotlin/com/infendro/cli/Dsl.kt +++ b/src/commonMain/kotlin/com/infendro/cli/Dsl.kt @@ -1,4 +1,4 @@ package com.infendro.cli @DslMarker -internal annotation class Dsl() +internal annotation class Dsl diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt b/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt index 05c0159..c9a39fd 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Argument.kt @@ -1,56 +1,71 @@ package com.infendro.cli.command +import com.infendro.cli.exception.build.InvalidRange import com.infendro.cli.parser.* sealed class Argument( val parser: Parser, + val name: String, + val min: Int, + val max: Int?, ) { - abstract val required: Boolean + init { + when { + min !in 0..count -> throw InvalidRange() + } + } + + val required: Boolean + get() = min > 0 + val optional: Boolean + get() = !required + + val bounded: Boolean + get() = max != null + val unbounded: Boolean + get() = !bounded + + val count: Int + get() = max ?: Int.MAX_VALUE class Required( parser: Parser, - ) : Argument(parser) { - override val required: Boolean - get() = true - - fun orElse(other: T) = OrElse(parser, other) - fun orNull() = OrNull(parser) - fun variable() = Variable(parser) + name: String, + ) : Argument(parser, name, min = 1, max = 1) { + fun orElse(other: T) = OrElse(parser, name, other) + fun orNull() = OrNull(parser, name) + fun variable(min: Int = 0, max: Int? = null) = Variable(parser, name, min, max) } class OrElse( parser: Parser, + name: String, val other: T, - ) : Argument(parser) { - override val required: Boolean - get() = false - } + ) : Argument(parser, name, min = 0, max = 1) class OrNull( parser: Parser, - ) : Argument(parser) { - override val required: Boolean - get() = false - } + name: String, + ) : Argument(parser, name, min = 0, max = 1) class Variable( parser: Parser, - ) : Argument(parser) { - override val required: Boolean - get() = false - } + name: String, + min: Int, + max: Int?, + ) : Argument(parser, name, min, max) companion object { - fun string() = argument(StringParser) - fun int() = argument(IntParser) - fun long() = argument(LongParser) - fun float() = argument(FloatParser) - fun double() = argument(DoubleParser) - fun boolean() = argument(BooleanParser) + fun string(name: String) = argument(StringParser, name) + fun int(name: String) = argument(IntParser, name) + fun long(name: String) = argument(LongParser, name) + fun float(name: String) = argument(FloatParser, name) + fun double(name: String) = argument(DoubleParser, name) + fun boolean(name: String) = argument(BooleanParser, name) - inline fun > enum() = argument(enumParser()) + inline fun > enum(name: String) = argument(enumParser(), name) } } -fun argument(parser: Parser) = - Argument.Required(parser) +fun argument(parser: Parser, name: String) = + Argument.Required(parser, name) diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt index 0d2d78d..a26886d 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Command.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Command.kt @@ -4,31 +4,31 @@ import com.infendro.cli.Dsl 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.exception.run.RunException +import com.infendro.cli.util.Regex.ARGUMENT import com.infendro.cli.util.Regex.COMMAND import com.infendro.cli.util.Regex.OPTION import kotlin.reflect.KProperty class Command private constructor( - val name: String?, + val name: String, + val fallback: Boolean, val commands: List, val arguments: List>, val options: List>, internal val execute: Context.() -> Unit, ) { - val fallback: Boolean - get() = name == null - fun run(args: Array) = try { Parser.parse(this, args).execute() - } catch (e: CliException) { + } catch (e: RunException) { println(e.message) } @Dsl class Builder internal constructor( private val level: Int, - private val name: String?, + private val name: String, + private val fallback: Boolean, ) { private val commands = mutableListOf() private val arguments = mutableListOf>() @@ -39,35 +39,17 @@ class Command private constructor( get() = Key(level) fun command(name: String, block: Builder.() -> Unit) { - // validate - val last = commands.lastOrNull() - when { - last != null && last.fallback -> throw CommandOrderException() - !name.matches(COMMAND) -> throw IllegalCommandNameException(name) - commands.any { it.name == name } -> throw DuplicateCommandException(name) - } - - commands += Builder(level + 1, name).also(block).build() + validateCommand(name) + commands += Builder(level + 1, name, fallback = false).apply(block).build() } - fun fallback(block: Builder.(Key) -> Unit) { - // validate - val last = commands.lastOrNull() - when { - last != null && last.fallback -> throw CommandOrderException() - } - - commands += Builder(level + 1, null).also { it.block(it.key) }.build() + fun fallback(name: String, block: Builder.(Key) -> Unit) { + validateCommand(name) + commands += Builder(level + 1, name, fallback = true).apply { block(key) }.build() } fun argument(argument: Argument<*>) { - // validate - val last = arguments.lastOrNull() - when { - last is Argument.Variable -> throw ArgumentOrderException() - argument.required && last != null && !last.required -> throw ArgumentOrderException() - } - + validateArgument(argument) arguments += argument } @@ -77,14 +59,7 @@ class Command private constructor( operator fun > T.getValue(thisRef: Nothing?, property: KProperty<*>) = this 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) - } - } - + validateOption(option) options += option } @@ -94,23 +69,62 @@ class Command private constructor( operator fun > T.getValue(thisRef: Nothing?, property: KProperty<*>) = this fun execute(block: Context.() -> Unit) { - if (::_execute.isInitialized) - throw DuplicateExecuteException() - + validateExecute() _execute = block } fun build(): Command { - // validate - val last = commands.lastOrNull() - when { - last != null && last.fallback && arguments.isNotEmpty() -> throw InvalidFallbackException() - } - if (!::_execute.isInitialized) _execute = {} - return Command(name, commands, arguments, options, _execute) + validate() + return Command(name, fallback, commands, arguments, options, _execute) + } + + private fun validateCommand(name: String) { + if (commands.isNotEmpty()) { + val last = commands.last() + when { + last.fallback -> throw InvalidCommandOrder() + } + } + when { + !name.matches(COMMAND) -> throw InvalidCommand(name) + commands.any { it.name == name } -> throw DuplicateCommand(name) + } + } + + private fun validateArgument(argument: Argument<*>) { + if (arguments.isNotEmpty()) { + val last = arguments.last() + when { + last.unbounded -> throw InvalidArgumentOrder() + argument.required && last.optional -> throw InvalidArgumentOrder() + } + } + when { + !argument.name.matches(ARGUMENT) -> throw InvalidArgument(argument.name) + } + } + + private fun validateOption(option: Option<*>) { + for (name in option.names) { + when { + !name.matches(OPTION) -> throw InvalidOption(name) + options.any { name in it.names } -> throw DuplicateOption(name) + } + } + } + + private fun validateExecute() { + if (::_execute.isInitialized) + throw DuplicateExecute() + } + + private fun validate() { + if (commands.any { it.fallback } && arguments.isNotEmpty()) { + throw InvalidFallback() + } } } @@ -119,5 +133,5 @@ class Command private constructor( ) } -fun cli(block: Command.Builder.() -> Unit) = - Command.Builder(-1, null).also(block).build() +fun cli(name: String, block: Command.Builder.() -> Unit) = + Command.Builder(-1, name, fallback = false).apply(block).build() diff --git a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt b/src/commonMain/kotlin/com/infendro/cli/command/Option.kt index b68836a..5da02f1 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/Option.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/Option.kt @@ -1,42 +1,55 @@ package com.infendro.cli.command -import com.infendro.cli.exception.build.MissingOptionNameException +import com.infendro.cli.exception.build.InvalidRange +import com.infendro.cli.exception.build.MissingOptionName import com.infendro.cli.parser.* sealed class Option( val parser: Parser, val fallback: T?, val names: List, + val min: Int, + val max: Int?, ) { + val name: String + get() = names.maxBy { it.length } + val flag: Boolean get() = fallback != null - val name: String - get() = names[0] + val required: Boolean + get() = min > 0 + val optional: Boolean + get() = !required - abstract val required: Boolean + val bounded: Boolean + get() = max != null + val unbounded: Boolean + get() = !bounded + + val count: Int + get() = max ?: Int.MAX_VALUE init { - if (names.isEmpty()) - throw MissingOptionNameException() + when { + names.isEmpty() -> throw MissingOptionName() + min !in 0..count -> throw InvalidRange() + } } class Required( parser: Parser, fallback: T?, names: List, - ) : Option(parser, fallback, names) { + ) : Option(parser, fallback, names, min = 1, max = 1) { constructor( parser: Parser, names: List, ) : this(parser, null, names) - override val required: Boolean - get() = true - fun orElse(other: T) = OrElse(parser, fallback, names, other) fun orNull() = OrNull(parser, fallback, names) - fun variable() = Variable(parser, fallback, names) + fun variable(min: Int = 0, max: Int? = null) = Variable(parser, fallback, names, min, max) } class OrElse( @@ -44,43 +57,38 @@ sealed class Option( fallback: T?, names: List, val other: T, - ) : Option(parser, fallback, names) { + ) : Option(parser, fallback, names, min = 0, max = 1) { constructor( parser: Parser, names: List, other: T, ) : this(parser, null, names, other) - - override val required: Boolean - get() = false } class OrNull( parser: Parser, fallback: T?, names: List, - ) : Option(parser, fallback, names) { + ) : Option(parser, fallback, names, min = 0, max = 1) { constructor( parser: Parser, names: List, ) : this(parser, null, names) - - override val required: Boolean - get() = false } class Variable( parser: Parser, fallback: T?, names: List, - ) : Option(parser, fallback, names) { + min: Int, + max: Int?, + ) : Option(parser, fallback, names, min, max) { constructor( parser: Parser, names: List, - ) : this(parser, null, names) - - override val required: Boolean - get() = false + min: Int, + max: Int?, + ) : this(parser, null, names, min, max) } companion object { diff --git a/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt b/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt index b1e53ad..42aa429 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/context/Context.kt @@ -31,7 +31,7 @@ class Context internal constructor( operator fun Command.Key.getValue(thisRef: Any?, property: KProperty<*>): String = commands[index].name private val Argument.index: Int - get() = command.arguments.indexOf(this) + get() = command.arguments.takeWhile { it != this }.sumOf { it.count } @Suppress("UNCHECKED_CAST") private val Argument.value: T? @@ -39,7 +39,7 @@ class Context internal constructor( @Suppress("UNCHECKED_CAST") private val Argument.Variable.values: List - get() = arguments.drop(index).map { it.value as T } + get() = arguments.drop(index).take(count).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 diff --git a/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt b/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt index d6a8e7b..42e2cdf 100644 --- a/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/command/context/Parser.kt @@ -1,8 +1,6 @@ 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 com.infendro.cli.command.context.Context.* import com.infendro.cli.exception.run.* @@ -25,7 +23,7 @@ internal object Parser { val c = command.commands.firstOrNull { it.name == current || it.fallback } if (c == null) { when { - command.arguments.isEmpty() -> throw UnknownCommandException(current) + command.arguments.isEmpty() -> throw UnknownCommand(current) else -> break } } @@ -39,6 +37,7 @@ internal object Parser { // arguments and options var endOfOptions = false var argumentIndex = 0 + var argumentCount = 0 while (i < args.size) { val dashes = args[i].takeWhile { it == '-' }.count() val current = args[i].drop(dashes) @@ -47,13 +46,16 @@ internal object Parser { // argument endOfOptions || dashes == 0 -> { val argument = command.arguments.getOrNull(argumentIndex) - ?: throw TooManyArgumentsException() + ?: throw UnexpectedArgument() - if (argument !is Argument.Variable) - argumentIndex++ - - val value = argument.parser.parse(current) as Any + val value = argument.parser.parse(current) arg += Arg(value) + argumentCount++ + + if (argumentCount == argument.count) { + argumentCount = 0 + argumentIndex++ + } } // option @@ -88,12 +90,12 @@ internal object Parser { for (name in names) { val option = command.options.firstOrNull { name in it.names } - ?: throw UnknownOptionException(name) + ?: throw UnknownOption(name) val value = when { text != null -> option.parser.parse(text) option.flag -> option.fallback!! - else -> throw MissingOptionValueException(name) + else -> throw MissingOptionValue(option) } opt += Opt(name, value) } @@ -104,9 +106,9 @@ internal object Parser { when { args[i].contains('=') -> { val (option) = args[i].split('=', limit = 2) - throw MalformedOptionException(option) + throw MalformedOption(option) } - else -> throw MalformedOptionException(args[i]) + else -> throw MalformedOption(args[i]) } } } @@ -114,17 +116,28 @@ internal object Parser { 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) - } - } + validateArguments(command, arg) + validateOptions(command, opt) return Context(command, cmd, opt, arg) } + + private fun validateArguments(command: Command, arg: List) { + val required = command.arguments.filter { it.required } + var consumed = 0 + for (argument in required) { + val needed = if (argument === required.last()) argument.min else argument.count + val delta = arg.size - consumed + if (delta < needed) throw InvalidArgumentCount(argument, delta) + + consumed += needed + } + } + + private fun validateOptions(command: Command, opt: List) { + for (option in command.options) { + val count = opt.count { it.name in option.names } + if (count !in option.min..option.count) throw InvalidOptionCount(option, count) + } + } } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt new file mode 100644 index 0000000..913de16 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/CliException.kt @@ -0,0 +1,3 @@ +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 deleted file mode 100644 index f04bcf4..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/ArgumentOrderException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception.build - -class ArgumentOrderException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt new file mode 100644 index 0000000..78c7397 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/BuildException.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.exception.build + +import com.infendro.cli.exception.CliException + +abstract class BuildException : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt new file mode 100644 index 0000000..5f02c1d --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommand.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.exception.build + +class DuplicateCommand( + val command: String, +) : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommandException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommandException.kt deleted file mode 100644 index c83df29..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateCommandException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.cli.exception.build - -class DuplicateCommandException( - val command: String?, -) : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/CommandOrderException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt similarity index 50% rename from src/commonMain/kotlin/com/infendro/cli/exception/build/CommandOrderException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt index 6ed8242..0a106cb 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/CommandOrderException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecute.kt @@ -1,3 +1,3 @@ package com.infendro.cli.exception.build -class CommandOrderException : Exception() +class DuplicateExecute : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecuteException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecuteException.kt deleted file mode 100644 index 91134a1..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateExecuteException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception.build - -class DuplicateExecuteException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt similarity index 57% rename from src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOptionException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt index 96ca86d..fe0bc83 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOptionException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/DuplicateOption.kt @@ -1,5 +1,5 @@ package com.infendro.cli.exception.build -class DuplicateOptionException( +class DuplicateOption( val name: String, -) : Exception() +) : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt similarity index 56% rename from src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt index 88dcc73..cc59440 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalOptionNameException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgument.kt @@ -1,5 +1,5 @@ package com.infendro.cli.exception.build -class IllegalOptionNameException( +class InvalidArgument( val name: String, -) : Exception() +) : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt new file mode 100644 index 0000000..c7628e6 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidArgumentOrder.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class InvalidArgumentOrder : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalCommandNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt similarity index 55% rename from src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalCommandNameException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt index f857b99..f869336 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/IllegalCommandNameException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommand.kt @@ -1,5 +1,5 @@ package com.infendro.cli.exception.build -class IllegalCommandNameException( +class InvalidCommand( val name: String, -) : Exception() +) : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt new file mode 100644 index 0000000..582268a --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidCommandOrder.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class InvalidCommandOrder : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt new file mode 100644 index 0000000..f510674 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallback.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class InvalidFallback : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt deleted file mode 100644 index 07e66ee..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidFallbackException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception.build - -class InvalidFallbackException : Exception() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt new file mode 100644 index 0000000..87ee939 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidOption.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.exception.build + +class InvalidOption( + val name: String, +) : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt new file mode 100644 index 0000000..989a180 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/InvalidRange.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class InvalidRange : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt new file mode 100644 index 0000000..7f19fd2 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionName.kt @@ -0,0 +1,3 @@ +package com.infendro.cli.exception.build + +class MissingOptionName : BuildException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionNameException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionNameException.kt deleted file mode 100644 index bd969f8..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/build/MissingOptionNameException.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.infendro.cli.exception.build - -class MissingOptionNameException : 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 deleted file mode 100644 index c84ca0f..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/CliException.kt +++ /dev/null @@ -1,3 +0,0 @@ -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/DuplicateOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateOptionException.kt deleted file mode 100644 index d6b84e7..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/DuplicateOptionException.kt +++ /dev/null @@ -1,8 +0,0 @@ -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/ExecuteException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/ExecuteException.kt deleted file mode 100644 index b826d7b..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/ExecuteException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.infendro.cli.exception.run - -class ExecuteException( - override val message: String, -) : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt new file mode 100644 index 0000000..86848b4 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidArgumentCount.kt @@ -0,0 +1,18 @@ +package com.infendro.cli.exception.run + +import com.infendro.cli.command.Argument + +class InvalidArgumentCount( + val argument: Argument<*>, + val count: Int, +) : RunException() { + override val message: String + get() { + val expected = when { + argument.min == argument.max -> "${argument.min}" + argument.unbounded -> "at least ${argument.min}" + else -> "${argument.min} to ${argument.max}" + } + return """invalid number of values for argument "${argument.name}": expected $expected but found $count""" + } +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt new file mode 100644 index 0000000..0b6c7fd --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidOptionCount.kt @@ -0,0 +1,18 @@ +package com.infendro.cli.exception.run + +import com.infendro.cli.command.Option + +class InvalidOptionCount( + val option: Option<*>, + val count: Int, +) : RunException() { + override val message: String + get() { + val expected = when { + option.min == option.max -> "${option.min}" + option.unbounded -> "at least ${option.min}" + else -> "${option.min} to ${option.max}" + } + return """invalid number of values passed for option "${option.name}": expected $expected but found $count""" + } +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt new file mode 100644 index 0000000..bdd69e3 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/InvalidValue.kt @@ -0,0 +1,11 @@ +package com.infendro.cli.exception.run + +import kotlin.reflect.KClass + +class InvalidValue( + val value: String, + val type: KClass<*>, +) : RunException() { + override val message: String + get() = """"$value" cannot be converted to ${type.simpleName}""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt similarity index 73% rename from src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt index 0e97e3d..9ea3d06 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOptionException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MalformedOption.kt @@ -1,8 +1,8 @@ package com.infendro.cli.exception.run -class MalformedOptionException( +class MalformedOption( val option: String, -) : CliException() { +) : RunException() { 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 deleted file mode 100644 index fba26c7..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingArgumentException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.infendro.cli.exception.run - -class MissingArgumentException : CliException() { - override val message: String - 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 deleted file mode 100644 index 317956f..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionException.kt +++ /dev/null @@ -1,8 +0,0 @@ -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/MissingOptionValue.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValue.kt new file mode 100644 index 0000000..20c2a27 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValue.kt @@ -0,0 +1,10 @@ +package com.infendro.cli.exception.run + +import com.infendro.cli.command.Option + +class MissingOptionValue( + val option: Option<*>, +) : RunException() { + override val message: String + get() = """no value provided for option "${option.name}"""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValueException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValueException.kt deleted file mode 100644 index 24b2cb7..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/MissingOptionValueException.kt +++ /dev/null @@ -1,8 +0,0 @@ -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/exception/run/ParseException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/ParseException.kt deleted file mode 100644 index 2206a0b..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/ParseException.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.infendro.cli.exception.run - -class ParseException( - val text: String?, - val type: String, -) : CliException() { - override val message: String - get() = """"$text" cannot be converted to $type""" -} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt new file mode 100644 index 0000000..5c8a626 --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/RunException.kt @@ -0,0 +1,5 @@ +package com.infendro.cli.exception.run + +import com.infendro.cli.exception.CliException + +abstract class RunException : CliException() diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt deleted file mode 100644 index fe293ff..0000000 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/TooManyArgumentsException.kt +++ /dev/null @@ -1,6 +0,0 @@ -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/UnexpectedArgument.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnexpectedArgument.kt new file mode 100644 index 0000000..bf63c2c --- /dev/null +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnexpectedArgument.kt @@ -0,0 +1,6 @@ +package com.infendro.cli.exception.run + +class UnexpectedArgument : RunException() { + override val message: String + get() = """unexpected argument""" +} diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt similarity index 74% rename from src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt index 5568c26..4fb1117 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommandException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownCommand.kt @@ -1,8 +1,8 @@ package com.infendro.cli.exception.run -class UnknownCommandException( +class UnknownCommand( val command: String, -) : CliException() { +) : RunException() { override val message: String get() = """unknown command "$command"""" } diff --git a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt similarity index 74% rename from src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt rename to src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt index 4fe2e48..38c9a41 100644 --- a/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOptionException.kt +++ b/src/commonMain/kotlin/com/infendro/cli/exception/run/UnknownOption.kt @@ -1,8 +1,8 @@ package com.infendro.cli.exception.run -class UnknownOptionException( +class UnknownOption( val option: String, -) : CliException() { +) : RunException() { 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 index ce894ab..5030413 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/BooleanParser.kt @@ -1,11 +1,11 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException +import com.infendro.cli.exception.run.InvalidValue object BooleanParser : Parser { override fun parse(text: String): Boolean = when (text) { "true", "t" -> true "false", "f" -> false - else -> throw ParseException(text, "Boolean") + else -> throw InvalidValue(text, Boolean::class) } } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt index df2cea5..20f8774 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/DoubleParser.kt @@ -1,8 +1,8 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException +import com.infendro.cli.exception.run.InvalidValue object DoubleParser : Parser { override fun parse(text: String): Double = text.toDoubleOrNull() - ?: throw ParseException(text, "Double") + ?: throw InvalidValue(text, Double::class) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt index 30dfa79..0dd7806 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/EnumParser.kt @@ -1,14 +1,15 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException +import com.infendro.cli.exception.run.InvalidValue import kotlin.enums.enumEntries +import kotlin.reflect.KClass class EnumParser>( - private val name: String, + private val type: KClass, private val values: List, ) : Parser { override fun parse(text: String): T = values.firstOrNull { it.name == text } - ?: throw ParseException(text, name) + ?: throw InvalidValue(text, type) } -inline fun > enumParser() = EnumParser(T::class.simpleName!!, enumEntries()) +inline fun > enumParser() = EnumParser(T::class, enumEntries()) diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt index cdef5ec..65a774a 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/FloatParser.kt @@ -1,8 +1,8 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException +import com.infendro.cli.exception.run.InvalidValue object FloatParser : Parser { override fun parse(text: String): Float = text.toFloatOrNull() - ?: throw ParseException(text, "Float") + ?: throw InvalidValue(text, Float::class) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt index 33be4af..4df6a2e 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/IntParser.kt @@ -1,8 +1,8 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException +import com.infendro.cli.exception.run.InvalidValue object IntParser : Parser { override fun parse(text: String): Int = text.toIntOrNull() - ?: throw ParseException(text, "Int") + ?: throw InvalidValue(text, Int::class) } diff --git a/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt b/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt index a234ab7..64152bc 100644 --- a/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt +++ b/src/commonMain/kotlin/com/infendro/cli/parser/LongParser.kt @@ -1,8 +1,8 @@ package com.infendro.cli.parser -import com.infendro.cli.exception.run.ParseException +import com.infendro.cli.exception.run.InvalidValue object LongParser : Parser { override fun parse(text: String): Long = text.toLongOrNull() - ?: throw ParseException(text, "Long") + ?: throw InvalidValue(text, Long::class) } diff --git a/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt b/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt index 1eeb52c..cf613b4 100644 --- a/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt +++ b/src/commonMain/kotlin/com/infendro/cli/util/Regex.kt @@ -2,5 +2,6 @@ package com.infendro.cli.util internal object Regex { val COMMAND = Regex("""[a-zA-Z][a-zA-Z0-9]*(?:-[a-zA-Z0-9]+)*""") + val ARGUMENT = 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]+)*""") }