Skip to content

Commit

Permalink
Report hints for options in subcommands
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt committed Sep 20, 2024
1 parent ff54fee commit d2eabfc
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 8 deletions.
6 changes: 5 additions & 1 deletion clikt/api/clikt.api
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,10 @@ public final class com/github/ajalt/clikt/core/NoSuchArgument : com/github/ajalt
}

public final class com/github/ajalt/clikt/core/NoSuchOption : com/github/ajalt/clikt/core/UsageError {
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun formatMessage (Lcom/github/ajalt/clikt/output/Localization;Lcom/github/ajalt/clikt/output/ParameterFormatter;)Ljava/lang/String;
}

Expand Down Expand Up @@ -686,6 +688,7 @@ public abstract interface class com/github/ajalt/clikt/output/Localization {
public abstract fun missingOption (Ljava/lang/String;)Ljava/lang/String;
public abstract fun mutexGroupException (Ljava/lang/String;Ljava/util/List;)Ljava/lang/String;
public abstract fun noSuchOption (Ljava/lang/String;Ljava/util/List;)Ljava/lang/String;
public abstract fun noSuchOptionWithSubCommandPossibility (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
public abstract fun noSuchSubcommand (Ljava/lang/String;Ljava/util/List;)Ljava/lang/String;
public abstract fun optionsMetavar ()Ljava/lang/String;
public abstract fun optionsTitle ()Ljava/lang/String;
Expand Down Expand Up @@ -745,6 +748,7 @@ public final class com/github/ajalt/clikt/output/Localization$DefaultImpls {
public static fun missingOption (Lcom/github/ajalt/clikt/output/Localization;Ljava/lang/String;)Ljava/lang/String;
public static fun mutexGroupException (Lcom/github/ajalt/clikt/output/Localization;Ljava/lang/String;Ljava/util/List;)Ljava/lang/String;
public static fun noSuchOption (Lcom/github/ajalt/clikt/output/Localization;Ljava/lang/String;Ljava/util/List;)Ljava/lang/String;
public static fun noSuchOptionWithSubCommandPossibility (Lcom/github/ajalt/clikt/output/Localization;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
public static fun noSuchSubcommand (Lcom/github/ajalt/clikt/output/Localization;Ljava/lang/String;Ljava/util/List;)Ljava/lang/String;
public static fun optionsMetavar (Lcom/github/ajalt/clikt/output/Localization;)Ljava/lang/String;
public static fun optionsTitle (Lcom/github/ajalt/clikt/output/Localization;)Ljava/lang/String;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.github.ajalt.clikt.output.ParameterFormatter
import com.github.ajalt.clikt.parameters.arguments.Argument
import com.github.ajalt.clikt.parameters.options.Option
import com.github.ajalt.clikt.parameters.options.longestName
import kotlin.jvm.JvmOverloads

/**
* An exception during command line processing that should be shown to the user.
Expand Down Expand Up @@ -238,15 +239,21 @@ class NoSuchSubcommand(
}

/** An option was provided that does not exist. */
class NoSuchOption(
class NoSuchOption @JvmOverloads constructor(
// TODO (6.0): remove JvmOverloads
paramName: String,
private val possibilities: List<String> = emptyList(),
private val subcommand: String? = null,
) : UsageError(null, paramName) {
override fun formatMessage(localization: Localization, formatter: ParameterFormatter): String {
return localization.noSuchOption(
paramName?.let(formatter::formatOption) ?: "",
possibilities.map(formatter::formatOption)
)
val name = paramName?.let(formatter::formatOption) ?: ""
return if (subcommand != null) {
localization.noSuchOptionWithSubCommandPossibility(
name, formatter.formatSubcommand(formatter.formatSubcommand(subcommand))
)
} else {
localization.noSuchOption(name, possibilities.map(formatter::formatOption))
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ interface Localization {
}
}

/**
* Message for [NoSuchOption] when a subcommand has an option with the same name
*/
fun noSuchOptionWithSubCommandPossibility(name: String, subcommand: String): String {
return "no such option $name. hint: $subcommand has an option $name"
}

/**
* Message for [IncorrectOptionValueCount]
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ private class CommandParser<T : BaseCliktCommand<T>>(
name,
optionsByName.filterNot { it.value.hidden }.keys.toList()
)
return OptParseResult(1, err = NoSuchOption(name, possibilities))
return OptParseResult(1, err = createNoSuchOption(name, possibilities))
}

return parseOptValues(option, name, attachedValue)
Expand All @@ -258,7 +258,7 @@ private class CommandParser<T : BaseCliktCommand<T>>(
prefix == "-" && "-$tok" in optionsByName -> listOf("-$tok")
else -> emptyList()
}
return OptParseResult(1, err = NoSuchOption(name, possibilities))
return OptParseResult(1, err = createNoSuchOption(name, possibilities))
}
if (option.nvalues.last > 0) {
val value = if (i < tok.lastIndex) tok.drop(i + 1) else null
Expand All @@ -271,6 +271,14 @@ private class CommandParser<T : BaseCliktCommand<T>>(
return OptParseResult(1, invocations)
}

private fun createNoSuchOption(name: String, possibilities: List<String>): NoSuchOption {
if (possibilities.isEmpty()) {
val c = allSubcommands.values.find { it._options.any { o -> name in o.names } }
if (c != null) return NoSuchOption(name, subcommand = c.commandName)
}
return NoSuchOption(name, possibilities)
}

private fun parseOptValues(
option: Option,
name: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ class OptionTest {
c.getFormattedHelp(err) shouldContain "Error: custom message"
}

@Test
@JsName("no_such_option_subcommand_hint")
fun `no such option subcommand hint`() {
class C : TestCommand(called = false)
class Sub: TestCommand(called = false) {
val foo by option()
}

val c = C().subcommands(Sub())
shouldThrow<NoSuchOption> {
c.parse("--foo")
}.formattedMessage shouldBe "no such option --foo. hint: sub has an option --foo"
}

@Test
@JsName("one_option")
fun `one option`() = forAll(
Expand Down

0 comments on commit d2eabfc

Please sign in to comment.