implement suggestions for unknown command/option
This commit is contained in:
@@ -53,7 +53,7 @@ internal class ContextParser(
|
||||
}
|
||||
if (cmd == null) {
|
||||
when {
|
||||
command.arguments.isEmpty() -> return failure(path, UnknownCommand(current))
|
||||
command.arguments.isEmpty() -> return failure(path, UnknownCommand(command, current))
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
@@ -87,9 +87,9 @@ internal class ContextParser(
|
||||
// argument
|
||||
endOfOptions || dashes == 0 -> {
|
||||
val argument = command.arguments.getOrNull(argumentIndex)
|
||||
?: return failure(path, UnexpectedArgument())
|
||||
?: return failure(path, UnexpectedArgument(current))
|
||||
|
||||
val value = when (val result = argument.parser.parse(trimmed)) {
|
||||
val value = when (val result = argument.parser.parse(current)) {
|
||||
is Parser.Result.Success<*> -> result.value
|
||||
is Parser.Result.Failure<*> -> return failure(path, result.failure)
|
||||
}
|
||||
@@ -135,7 +135,7 @@ internal class ContextParser(
|
||||
|
||||
for (name in names) {
|
||||
val option = command.options.firstOrNull { name in it.names }
|
||||
?: return failure(path, UnknownOption(name))
|
||||
?: return failure(path, UnknownOption(command, name))
|
||||
|
||||
val value = when {
|
||||
text != null ->
|
||||
|
||||
@@ -13,6 +13,6 @@ class InvalidArgumentCount(
|
||||
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"""
|
||||
return """Invalid number of values for argument "${argument.name}". Expected $expected but found $count."""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ class InvalidOptionCount(
|
||||
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"""
|
||||
return """Invalid number of values passed for option "${option.name}". Expected $expected but found $count."""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@ class InvalidValue(
|
||||
val type: KClass<*>,
|
||||
) : RuntimeFailure() {
|
||||
override val message: String
|
||||
get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}"""
|
||||
get() = """"$value" cannot be converted to ${type.simpleName ?: "Unknown"}."""
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ class MalformedOption(
|
||||
val option: String,
|
||||
) : RuntimeFailure() {
|
||||
override val message: String
|
||||
get() = """malformed option "$option""""
|
||||
get() = """Malformed option "$option"."""
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@ class MissingOptionValue(
|
||||
val option: Option<*>,
|
||||
) : RuntimeFailure() {
|
||||
override val message: String
|
||||
get() = """no value provided for option "${option.name}""""
|
||||
get() = """No value provided for option "${option.name}"."""
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.infendro.cli.error.run
|
||||
|
||||
class UnexpectedArgument : RuntimeFailure() {
|
||||
class UnexpectedArgument(
|
||||
val argument: String,
|
||||
) : RuntimeFailure() {
|
||||
override val message: String
|
||||
get() = """unexpected argument"""
|
||||
get() = """Unexpected argument "$argument"."""
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
package com.infendro.cli.error.run
|
||||
|
||||
import com.infendro.cli.command.Command
|
||||
import com.infendro.cli.util.distance
|
||||
import com.infendro.cli.util.threshold
|
||||
import kotlin.math.min
|
||||
|
||||
class UnknownCommand(
|
||||
val command: String,
|
||||
val command: Command,
|
||||
val name: String,
|
||||
) : RuntimeFailure() {
|
||||
override val message: String
|
||||
get() = """unknown command "$command""""
|
||||
get() = buildString {
|
||||
val suggestion = command.commands
|
||||
.map { it.name }
|
||||
.associateWith { distance(name, it) }
|
||||
.filter { it.value <= min(it.key.length - 1, threshold) }
|
||||
.minByOrNull { it.value }
|
||||
?.key
|
||||
|
||||
append("""Unknown command "$name".""")
|
||||
if (suggestion != null) append(""" Did you mean "$suggestion"?""")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
package com.infendro.cli.error.run
|
||||
|
||||
import com.infendro.cli.command.Command
|
||||
import com.infendro.cli.util.distance
|
||||
import com.infendro.cli.util.threshold
|
||||
import kotlin.math.min
|
||||
|
||||
class UnknownOption(
|
||||
val option: String,
|
||||
val command: Command,
|
||||
val name: String,
|
||||
) : RuntimeFailure() {
|
||||
override val message: String
|
||||
get() = """unknown option "$option""""
|
||||
get() = buildString {
|
||||
val suggestion = command.options
|
||||
.flatMap { it.names }
|
||||
.associateWith { distance(name, it) }
|
||||
.filter { it.value <= min(it.key.length - 1, threshold) }
|
||||
.minByOrNull { it.value }
|
||||
?.key
|
||||
|
||||
append("""Unknown option "$name".""")
|
||||
if (suggestion != null) append(""" Did you mean "$suggestion"?""")
|
||||
}
|
||||
}
|
||||
|
||||
48
src/commonMain/kotlin/com/infendro/cli/util/Distance.kt
Normal file
48
src/commonMain/kotlin/com/infendro/cli/util/Distance.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
package com.infendro.cli.util
|
||||
|
||||
internal const val threshold = 2
|
||||
|
||||
/**
|
||||
* Damerau–Levenshtein string distance
|
||||
*/
|
||||
internal fun distance(a: String, b: String): Int {
|
||||
if (a == b) return 0
|
||||
|
||||
val n = a.length
|
||||
val m = b.length
|
||||
|
||||
if (n == 0) return m
|
||||
if (m == 0) return n
|
||||
|
||||
var prev2 = IntArray(m + 1)
|
||||
var prev1 = IntArray(m + 1) { it }
|
||||
var curr = IntArray(m + 1)
|
||||
|
||||
for (i in 1..n) {
|
||||
curr[0] = i
|
||||
for (j in 1..m) {
|
||||
val cost = if (a[i - 1] == b[j - 1]) 0 else 1
|
||||
|
||||
curr[j] = minOf(
|
||||
prev1[j] + 1, // deletion
|
||||
curr[j - 1] + 1, // insertion
|
||||
prev1[j - 1] + cost, // substitution
|
||||
)
|
||||
|
||||
// transposition
|
||||
if (i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1]) {
|
||||
curr[j] = minOf(
|
||||
curr[j],
|
||||
prev2[j - 2] + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val tmp = prev2
|
||||
prev2 = prev1
|
||||
prev1 = curr
|
||||
curr = tmp
|
||||
}
|
||||
|
||||
return prev1[m]
|
||||
}
|
||||
Reference in New Issue
Block a user