From 999ea760035ac9d75c19be83e4bbb84f0e426946 Mon Sep 17 00:00:00 2001 From: AJ Date: Wed, 7 Feb 2024 11:57:15 -0800 Subject: [PATCH 1/3] Add limit parameter to counted options --- CHANGELOG.md | 4 ++++ .../github/ajalt/clikt/output/Localization.kt | 6 ++++++ .../clikt/parameters/options/FlagOption.kt | 13 +++++++++++-- .../ajalt/clikt/parameters/OptionTest.kt | 19 +++++++++++++++++-- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b88e3c89..247097087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased +### Added +- Added `limit` parameter to `option().counted()` to limit the number of times the option can be used. You can either clamp the value to the limit, or throw an error if the limit is exceeded. ([#483](https://github.com/ajalt/clikt/issues/483)) + ## 4.2.2 ### Changed - Options and arguments can now reference option groups in their `defaultLazy` and other finalization blocks. They can also freely reference each other, including though chains of references. ([#473](https://github.com/ajalt/clikt/issues/473)) 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 7b4f792bf..ebef73fde 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 @@ -145,6 +145,12 @@ interface Localization { fun rangeExceededBoth(value: String, min: String, max: String) = "$value is not in the valid range of $min to $max." + /** + * A counted option was given more times than its limit + */ + fun countedOptionExceededLimit(count: Int, limit: Int): String = + "option was given $count times, but only $limit times are allowed" + /** * Invalid value for `choice` parameter * diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt index 27b908443..7caf92bc6 100644 --- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt +++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt @@ -5,6 +5,7 @@ import com.github.ajalt.clikt.core.BadParameterValue import com.github.ajalt.clikt.parameters.types.boolean import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.mordant.terminal.YesNoPrompt +import kotlin.jvm.JvmOverloads /** A block that converts a flag value from one type to another */ typealias FlagConverter = OptionTransformContext.(InT) -> OutT @@ -79,8 +80,16 @@ inline fun OptionWithValues.convert( /** * Turn an option into a flag that counts the number of times it occurs on the command line. */ -fun RawOption.counted(): OptionWithValues { - return int().transformValues(0..0) { it.lastOrNull() ?: 1 }.transformAll { it.sum() } +@JvmOverloads // TODO(5.0): remove this annotation +fun RawOption.counted(limit: Int = 0, clamp: Boolean = true): OptionWithValues { + return int().transformValues(0..0) { it.lastOrNull() ?: 1 }.transformAll { + val s = it.sum() + when { + limit > 0 && clamp -> s.coerceAtMost(limit) + limit in 1.. fail(context.localization.countedOptionExceededLimit(s, limit)) + else -> s + } + } } /** diff --git a/clikt/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt b/clikt/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt index b07e7c454..51d6a584a 100644 --- a/clikt/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt +++ b/clikt/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt @@ -361,10 +361,11 @@ class OptionTest { row("-xyx", 2, true, null), row("-xyxzxyz", 2, true, "xyz"), row("-xyzxyz", 1, true, "xyz"), - row("-xzfoo", 1, false, "foo") + row("-xzfoo", 1, false, "foo"), + row("-xxxxxx", 4, false, null), ) { argv, ex, ey, ez -> class C : TestCommand() { - val x by option("-x", "--xx").counted() + val x by option("-x", "--xx").counted(limit = 4) val y by option("-y", "--yy").flag() val z by option("-z", "--zz") override fun run_() { @@ -377,6 +378,20 @@ class OptionTest { C().parse(argv) } + @Test + @JsName("counted_option_clamp_false") + fun `counted option clamp=false`() { + class C(called: Boolean) : TestCommand(called) { + val x by option("-x").counted(limit = 2, clamp = false) + } + + C(true).parse("").x shouldBe 0 + C(true).parse("-xx").x shouldBe 2 + + shouldThrow { C(false).parse("-xxxx") } + .formattedMessage shouldBe "invalid value for -x: option was given 4 times, but only 2 times are allowed" + } + @Test @JsName("default_option") fun `default option`() = forAll( From ce12892d60261cc623902eaeddd7ba5b33158635 Mon Sep 17 00:00:00 2001 From: AJ Date: Wed, 7 Feb 2024 12:07:54 -0800 Subject: [PATCH 2/3] Add docs --- docs/options.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/options.md b/docs/options.md index 67e5c454a..faf9426bd 100644 --- a/docs/options.md +++ b/docs/options.md @@ -445,10 +445,13 @@ line: You might want a flag option that counts the number of times it occurs on the command line. You can use [`counted`][counted] for this. +You can specify a `limit` for the number of times the [`counted`][counted] option can be given, +and either `clamp` the value or show an error if the limit is exceeded. + === "Example" ```kotlin class Log : CliktCommand() { - val verbosity by option("-v").counted() + val verbosity by option("-v").counted(limit=3, clamp=true) override fun run() { echo("Verbosity level: $verbosity") } @@ -461,6 +464,7 @@ use [`counted`][counted] for this. Verbosity level: 3 ``` + ## Feature Switch Flags Another way to use flags is to assign a value to each option name. You can do this with From 0a710f8f53efe9ee0f609ffe915262146102f2be Mon Sep 17 00:00:00 2001 From: AJ Date: Wed, 7 Feb 2024 12:45:02 -0800 Subject: [PATCH 3/3] Allow any value for limit --- .../ajalt/clikt/parameters/options/FlagOption.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt index 7caf92bc6..e8c26620b 100644 --- a/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt +++ b/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/FlagOption.kt @@ -79,16 +79,19 @@ inline fun OptionWithValues.convert( /** * Turn an option into a flag that counts the number of times it occurs on the command line. + * + * @param limit The maximum number of times the option can be given. (defaults to no limit) + * @param clamp If `true`, the counted value will be clamped to the [limit] if it is exceeded. If + * `false`, an error will be shown isntead of clamping. */ @JvmOverloads // TODO(5.0): remove this annotation -fun RawOption.counted(limit: Int = 0, clamp: Boolean = true): OptionWithValues { +fun RawOption.counted(limit: Int = Int.MAX_VALUE, clamp: Boolean = true): OptionWithValues { return int().transformValues(0..0) { it.lastOrNull() ?: 1 }.transformAll { val s = it.sum() - when { - limit > 0 && clamp -> s.coerceAtMost(limit) - limit in 1.. fail(context.localization.countedOptionExceededLimit(s, limit)) - else -> s + if (!clamp && s > limit) { + fail(context.localization.countedOptionExceededLimit(s, limit)) } + s.coerceAtMost(limit) } }