diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b88e3c8..24709708 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 7b4f792b..ebef73fd 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 27b90844..e8c26620 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 @@ -78,9 +79,20 @@ 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. */ -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 = Int.MAX_VALUE, clamp: Boolean = true): OptionWithValues { + return int().transformValues(0..0) { it.lastOrNull() ?: 1 }.transformAll { + val s = it.sum() + if (!clamp && s > limit) { + fail(context.localization.countedOptionExceededLimit(s, limit)) + } + s.coerceAtMost(limit) + } } /** 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 b07e7c45..51d6a584 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( diff --git a/docs/options.md b/docs/options.md index 67e5c454..faf9426b 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