Skip to content

Commit 6072c7f

Browse files
committed
Add MutuallyExclusiveGroup.single
1 parent 8e273c6 commit 6072c7f

File tree

5 files changed

+55
-24
lines changed

5 files changed

+55
-24
lines changed

clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/groups/MutuallyExclusiveOption.kt

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import com.github.ajalt.clikt.parsers.OptionParser
77
import kotlin.properties.ReadOnlyProperty
88
import kotlin.reflect.KProperty
99

10-
class MutuallyExclusiveOptions<OptT, OutT>(
10+
class MutuallyExclusiveOptions<OptT : Any, OutT>(
1111
internal val options: List<OptionDelegate<OptT?>>,
1212
override val groupName: String?,
1313
override val groupHelp: String?,
14-
private val transform: (OptT?) -> OutT
14+
internal val transformAll: (List<OptT>) -> OutT
1515
) : ParameterGroupDelegate<OutT> {
1616
init {
1717
require(options.size > 1) { "must provide at least two options to a mutually exclusive group" }
@@ -22,17 +22,13 @@ class MutuallyExclusiveOptions<OptT, OutT>(
2222
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): OutT = value
2323

2424
override fun finalize(context: Context, invocationsByOption: Map<Option, List<OptionParser.Invocation>>) {
25-
if (invocationsByOption.size > 1) {
26-
throw MutuallyExclusiveGroupException(options.map { o -> o.names.maxBy { it.length }!! })
27-
}
28-
2925
for ((option, invocations) in invocationsByOption) {
3026
check(option in options) { "Internal Clikt Error: finalizing unregistered option [${option.names}]" }
3127
option.finalize(context, invocations)
3228
}
3329

34-
val values = options.filter { it.value != null }.map { it.value }
35-
value = transform(values.firstOrNull())
30+
val values = options.mapNotNull { it.value }
31+
value = transformAll(values)
3632
}
3733

3834
override operator fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty<CliktCommand, OutT> {
@@ -47,15 +43,15 @@ class MutuallyExclusiveOptions<OptT, OutT>(
4743
return this
4844
}
4945

50-
fun <T> copy(transform: (OptT?) -> T) = MutuallyExclusiveOptions(options, groupName, groupHelp, transform)
46+
fun <T> copy(transformAll: (List<OptT>) -> T) = MutuallyExclusiveOptions(options, groupName, groupHelp, transformAll)
5147
}
5248

5349
/**
5450
* Declare a set of two or more mutually exclusive options.
5551
*
5652
* If none of the options are given on the command line, the value of this delegate will be null.
5753
* If one option is given, the value will be that option's value.
58-
* If more than one option is given, a [UsageError] is thrown.
54+
* If more than one option is given, the value of the last one is used.
5955
*
6056
* All options in the group must have a name specified. All options must be nullable (they cannot
6157
* use [flag], [required] etc.). If you want flags, you should use [switch] instead.
@@ -80,20 +76,33 @@ fun <T : Any> ParameterHolder.mutuallyExclusiveOptions(
8076
name: String? = null,
8177
help: String? = null
8278
): MutuallyExclusiveOptions<T, T?> {
83-
return MutuallyExclusiveOptions(listOf(option1, option2) + options, name, help) { it }
79+
return MutuallyExclusiveOptions(listOf(option1, option2) + options, name, help) { it.lastOrNull() }
80+
}
81+
82+
/**
83+
* If more than one of the group's options are given on the command line, throw a [MutuallyExclusiveGroupException]
84+
*/
85+
fun <T : Any> MutuallyExclusiveOptions<T, T?>.single(): MutuallyExclusiveOptions<T, T?> = copy {
86+
if (it.size > 1) {
87+
throw MutuallyExclusiveGroupException(options.map { o -> o.longestName()!! })
88+
}
89+
it.lastOrNull()
8490
}
8591

8692
/**
8793
* Make a [mutuallyExclusiveOptions] group required. If none of the options in the group are given,
8894
* a [UsageError] is thrown.
8995
*/
90-
fun <T : Any> MutuallyExclusiveOptions<T, T?>.required(): MutuallyExclusiveOptions<T, T> = copy {
91-
it ?: throw UsageError("Must provide one of ${options.joinToString { it.names.maxBy { it.length }!! }}")
96+
fun <T : Any> MutuallyExclusiveOptions<T, T?>.required(): MutuallyExclusiveOptions<T, T> {
97+
return copy {
98+
transformAll(it)
99+
?: throw UsageError("Must provide one of ${options.joinToString { it.longestName()!! }}")
100+
}
92101
}
93102

94103
/**
95104
* If none of the options in a [mutuallyExclusiveOptions] group are given on the command line, us [value] for the group.
96105
*/
97-
fun <T : Any> MutuallyExclusiveOptions<T, T?>.default(value: T): MutuallyExclusiveOptions<T, T> = copy {
98-
it ?: value
106+
fun <T : Any> MutuallyExclusiveOptions<T, T?>.default(value: T): MutuallyExclusiveOptions<T, T> {
107+
return copy { transformAll(it) ?: value }
99108
}

clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/options/Option.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,5 @@ internal fun <EachT, AllT> deprecationTransformer(
121121
}
122122
transformAll(it)
123123
}
124+
125+
internal fun Option.longestName(): String? = names.maxBy { it.length }

clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/options/OptionWithValues.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -562,7 +562,7 @@ fun <T : Any> NullableOption<T, T>.prompt(
562562
confirmationPrompt: String = "Repeat for confirmation: ",
563563
promptSuffix: String = ": ",
564564
showDefault: Boolean = true): OptionWithValues<T, T, T> = transformAll {
565-
val promptText = text ?: names.maxBy { it.length }?.let { splitOptionPrefix(it).second }
565+
val promptText = text ?: longestName()?.let { splitOptionPrefix(it).second }
566566
?.replace(Regex("\\W"), " ")?.capitalize() ?: "Value"
567567

568568
when (val provided = it.lastOrNull()) {

clikt/src/test/kotlin/com/github/ajalt/clikt/parameters/groups/OptionGroupsTest.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.github.ajalt.clikt.parameters.options.required
1212
import com.github.ajalt.clikt.testing.NeverCalledCliktCommand
1313
import com.github.ajalt.clikt.testing.splitArgv
1414
import io.kotlintest.data.forall
15+
import io.kotlintest.fail
1516
import io.kotlintest.shouldBe
1617
import io.kotlintest.shouldThrow
1718
import io.kotlintest.tables.row
@@ -88,7 +89,7 @@ class OptionGroupsTest {
8889
fun `mutually exclusive group`() = forall(
8990
row("", null, "d"),
9091
row("--x=1", "1", "d"),
91-
row("--x=1 --x=2", "2", "d"),
92+
row("--x=1 --y=2", "2", "d"),
9293
row("--y=3", "3", "d"),
9394
row("--x=4 --o=5", "4", "5")
9495
) { argv, eg, eo ->
@@ -105,14 +106,21 @@ class OptionGroupsTest {
105106
}
106107

107108
@Test
108-
fun `mutually exclusive group collision`() {
109-
class C : NeverCalledCliktCommand() {
110-
val g by mutuallyExclusiveOptions(option("--x"), option("--y"), option("--z"))
109+
fun `mutually exclusive group single`() {
110+
class C(val runAllowed: Boolean) : CliktCommand() {
111+
val g by mutuallyExclusiveOptions(option("--x"), option("--y"), option("--z")).single()
112+
override fun run() {
113+
if (!runAllowed) fail("run should not be called")
114+
}
111115
}
112-
shouldThrow<MutuallyExclusiveGroupException> { C().parse(splitArgv("--x=1 --y=2")) }
116+
117+
C(true).apply { parse(splitArgv("--x=1")) }.g shouldBe "1"
118+
C(true).apply { parse(splitArgv("--y=1 --y=2")) }.g shouldBe "2"
119+
120+
shouldThrow<MutuallyExclusiveGroupException> { C(false).parse(splitArgv("--x=1 --y=2")) }
113121
.message shouldBe "option --x cannot be used with --y or --z"
114122

115-
shouldThrow<MutuallyExclusiveGroupException> { C().parse(splitArgv("--y=1 --z=2")) }
123+
shouldThrow<MutuallyExclusiveGroupException> { C(false).parse(splitArgv("--y=1 --z=2")) }
116124
.message shouldBe "option --x cannot be used with --y or --z"
117125
}
118126

docs/options.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ If [`choice`](#choice-options) or [`switch`](#feature-switch-flags) options aren
402402
you can use
403403
[`mutuallyExclusiveOptions`](api/clikt/com.github.ajalt.clikt.parameters.groups/mutually-exclusive-options.html)
404404
to group any nullable options into a mutually exclusive group. If more than one of the options in
405-
the group is given on the command line, an error is reported.
405+
the group is given on the command line, the last value is used.
406406

407407
If you want different types for each option, you can wrap them in a sealed class.
408408

@@ -428,14 +428,26 @@ Apples(count=10)
428428
$ ./order --oranges=small
429429
Oranges(size=small)
430430
431+
$ ./order --apples=10 --oranges=large
432+
Oranges(size=large)
433+
434+
Usage: order [OPTIONS]
435+
436+
Error: option --apples cannot be used with --oranges
437+
```
438+
439+
You can enforce that only one of the options is given with
440+
[`single`](api/clikt/com.github.ajalt.clikt.parameters.groups/single.html):
441+
442+
```
431443
$ ./order --apples=10 --oranges=small
432444
Usage: order [OPTIONS]
433445
434446
Error: option --apples cannot be used with --oranges
435447
```
436448

437449
Like regular options, you can make the entire group
438-
[`required`](api/clikt/com.github.ajalt.clikt.parameters.groups/required.html) or give it a
450+
[`required`](api/clikt/com.github.ajalt.clikt.parameters.groups/required.html), or give it a
439451
[`default`](api/clikt/com.github.ajalt.clikt.parameters.groups/required.html) value.
440452

441453
Like [other option groups](documenting/#grouping-options-in-help), you can specify a `name` and

0 commit comments

Comments
 (0)