implement automatic help
This commit is contained in:
@@ -10,8 +10,8 @@ import kotlin.reflect.KProperty
|
||||
class Context internal constructor(
|
||||
val command: Command,
|
||||
val commands: List<Cmd>,
|
||||
val options: List<Opt>,
|
||||
val arguments: List<Arg>,
|
||||
val options: List<Opt>,
|
||||
) {
|
||||
class Cmd(
|
||||
val name: String,
|
||||
@@ -28,6 +28,10 @@ class Context internal constructor(
|
||||
|
||||
internal fun execute() = (command.execute)()
|
||||
|
||||
fun help() {
|
||||
command.help?.let { println(it) }
|
||||
}
|
||||
|
||||
operator fun Command.Key.getValue(thisRef: Any?, property: KProperty<*>): String = commands[index].name
|
||||
|
||||
private val <T : Any> Argument<T>.index: Int
|
||||
|
||||
@@ -4,26 +4,55 @@ import com.infendro.cli.command.Command
|
||||
import com.infendro.cli.command.context.Context.*
|
||||
import com.infendro.cli.exception.run.*
|
||||
|
||||
internal object Parser {
|
||||
fun parse(root: Command, args: Array<String>): Context {
|
||||
internal class Parser(
|
||||
private val root: Command,
|
||||
private val args: Array<String>,
|
||||
) {
|
||||
private var index = 0
|
||||
private val current: String
|
||||
get() = args[index]
|
||||
private val next: String?
|
||||
get() = args.getOrNull(index)
|
||||
|
||||
private fun hasNext() = index < args.size
|
||||
private fun consume() {
|
||||
index++
|
||||
}
|
||||
|
||||
sealed class Result {
|
||||
data class Success(
|
||||
val context: Context,
|
||||
) : Result()
|
||||
|
||||
data class Help(
|
||||
val command: Command,
|
||||
) : Result()
|
||||
|
||||
data class Failure(
|
||||
val command: Command,
|
||||
val exception: RunException,
|
||||
) : Result()
|
||||
}
|
||||
|
||||
fun success(context: Context) = Result.Success(context)
|
||||
fun help(command: Command) = Result.Help(command)
|
||||
fun failure(command: Command, exception: RunException) = Result.Failure(command, exception)
|
||||
|
||||
fun parse(): Result {
|
||||
var command = root
|
||||
val cmd = mutableListOf<Cmd>()
|
||||
val arg = mutableListOf<Arg>()
|
||||
val opt = mutableListOf<Opt>()
|
||||
|
||||
var i = 0
|
||||
|
||||
// commands
|
||||
while (i < args.size) {
|
||||
val current = args[i]
|
||||
|
||||
// command
|
||||
while (hasNext()) {
|
||||
if (current.startsWith("-"))
|
||||
break
|
||||
|
||||
val c = command.commands.firstOrNull { it.name == current || it.fallback }
|
||||
if (c == null) {
|
||||
when {
|
||||
command.arguments.isEmpty() -> throw UnknownCommand(current)
|
||||
command.arguments.isEmpty() -> return failure(command, UnknownCommand(current))
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
@@ -31,24 +60,25 @@ internal object Parser {
|
||||
command = c
|
||||
cmd += Cmd(current)
|
||||
|
||||
i++
|
||||
consume()
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
while (hasNext()) {
|
||||
val dashes = current.takeWhile { it == '-' }.count()
|
||||
val trimmed = current.drop(dashes)
|
||||
|
||||
when {
|
||||
// argument
|
||||
endOfOptions || dashes == 0 -> {
|
||||
val argument = command.arguments.getOrNull(argumentIndex)
|
||||
?: throw UnexpectedArgument()
|
||||
?: return failure(command, UnexpectedArgument())
|
||||
|
||||
val value = argument.parser.parse(current)
|
||||
val value = argument.parser.parse(trimmed)
|
||||
arg += Arg(value)
|
||||
argumentCount++
|
||||
|
||||
@@ -60,25 +90,26 @@ internal object Parser {
|
||||
|
||||
// option
|
||||
dashes in 1..2 -> {
|
||||
if (dashes == 2 && current.isEmpty()) {
|
||||
if (dashes == 2 && trimmed.isEmpty()) {
|
||||
endOfOptions = true
|
||||
i++
|
||||
consume()
|
||||
continue
|
||||
}
|
||||
|
||||
if (dashes == 2 && trimmed == "help")
|
||||
return help(command)
|
||||
|
||||
val (name, text) = when {
|
||||
current.contains('=') -> {
|
||||
val (name, value) = current.split('=', limit = 2)
|
||||
trimmed.contains('=') -> {
|
||||
val (name, value) = trimmed.split('=', limit = 2)
|
||||
name to value
|
||||
}
|
||||
|
||||
else -> {
|
||||
val next = args.getOrNull(i + 1)
|
||||
if (next != null && !next.startsWith('-')) {
|
||||
i++
|
||||
current to next
|
||||
if (next?.startsWith('-') == false) {
|
||||
consume()
|
||||
trimmed to next
|
||||
} else {
|
||||
current to null
|
||||
trimmed to null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,12 +121,12 @@ internal object Parser {
|
||||
|
||||
for (name in names) {
|
||||
val option = command.options.firstOrNull { name in it.names }
|
||||
?: throw UnknownOption(name)
|
||||
?: return failure(command, UnknownOption(name))
|
||||
|
||||
val value = when {
|
||||
text != null -> option.parser.parse(text)
|
||||
option.flag -> option.fallback!!
|
||||
else -> throw MissingOptionValue(option)
|
||||
else -> return failure(command, MissingOptionValue(option))
|
||||
}
|
||||
opt += Opt(name, value)
|
||||
}
|
||||
@@ -104,40 +135,36 @@ internal object Parser {
|
||||
// malformed option
|
||||
else -> {
|
||||
when {
|
||||
args[i].contains('=') -> {
|
||||
val (option) = args[i].split('=', limit = 2)
|
||||
throw MalformedOption(option)
|
||||
current.contains('=') -> {
|
||||
val (option) = current.split('=', limit = 2)
|
||||
return failure(command, MalformedOption(option))
|
||||
}
|
||||
else -> throw MalformedOption(args[i])
|
||||
else -> return failure(command, MalformedOption(current))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i++
|
||||
consume()
|
||||
}
|
||||
|
||||
validateArguments(command, arg)
|
||||
validateOptions(command, opt)
|
||||
|
||||
return Context(command, cmd, opt, arg)
|
||||
}
|
||||
|
||||
private fun validateArguments(command: Command, arg: List<Arg>) {
|
||||
// validate arguments
|
||||
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)
|
||||
if (delta < needed) return failure(command, InvalidArgumentCount(argument, delta))
|
||||
|
||||
consumed += needed
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateOptions(command: Command, opt: List<Opt>) {
|
||||
// validate options
|
||||
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)
|
||||
if (count !in option.min..option.count) return failure(command, InvalidOptionCount(option, count))
|
||||
}
|
||||
|
||||
val context = Context(command, cmd, arg, opt)
|
||||
return success(context)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user