Skip to content

Commit

Permalink
Add limit parameter to counted options (#484)
Browse files Browse the repository at this point in the history
This PR adds a limit parameter to counted(), and a clamp parameter that matches the way restrictTo() works.
  • Loading branch information
ajalt committed Feb 7, 2024
1 parent 11956bd commit 5cb5ab1
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 5 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<InT, OutT> = OptionTransformContext.(InT) -> OutT
Expand Down Expand Up @@ -78,9 +79,20 @@ inline fun <OutT> OptionWithValues<Boolean, Boolean, Boolean>.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<Int, Int, Int> {
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<Int, Int, Int> {
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)
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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_() {
Expand All @@ -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<UsageError> { 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(
Expand Down
6 changes: 5 additions & 1 deletion docs/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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
Expand Down

0 comments on commit 5cb5ab1

Please sign in to comment.