From d2eabfc5f11b2b516f609f536bb096ebbab7c5db Mon Sep 17 00:00:00 2001 From: AJ Date: Thu, 19 Sep 2024 19:13:27 -0700 Subject: [PATCH] Report hints for options in subcommands --- clikt/api/clikt.api | 6 +++++- .../com/github/ajalt/clikt/core/exceptions.kt | 17 ++++++++++++----- .../github/ajalt/clikt/output/Localization.kt | 7 +++++++ .../ajalt/clikt/parsers/ParserInternals.kt | 12 ++++++++++-- .../github/ajalt/clikt/parameters/OptionTest.kt | 14 ++++++++++++++ 5 files changed, 48 insertions(+), 8 deletions(-) diff --git a/clikt/api/clikt.api b/clikt/api/clikt.api index 052a8895..25d052cd 100644 --- a/clikt/api/clikt.api +++ b/clikt/api/clikt.api @@ -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 (Ljava/lang/String;)V public fun (Ljava/lang/String;Ljava/util/List;)V - public synthetic fun (Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V + public synthetic fun (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; } @@ -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; @@ -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; diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/exceptions.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/exceptions.kt index 18d93a91..66e96624 100644 --- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/exceptions.kt +++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/exceptions.kt @@ -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. @@ -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 = 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)) + } } } diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/output/Localization.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/output/Localization.kt index ebef73fd..97a01ac4 100644 --- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/output/Localization.kt +++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/output/Localization.kt @@ -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] * diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/ParserInternals.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/ParserInternals.kt index 0e9a4b51..534eacdb 100644 --- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/ParserInternals.kt +++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/ParserInternals.kt @@ -232,7 +232,7 @@ private class CommandParser>( 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) @@ -258,7 +258,7 @@ private class CommandParser>( 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 @@ -271,6 +271,14 @@ private class CommandParser>( return OptParseResult(1, invocations) } + private fun createNoSuchOption(name: String, possibilities: List): 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, diff --git a/test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt b/test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt index fd675af9..2bcfbd87 100644 --- a/test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt +++ b/test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt @@ -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 { + c.parse("--foo") + }.formattedMessage shouldBe "no such option --foo. hint: sub has an option --foo" + } + @Test @JsName("one_option") fun `one option`() = forAll(