Skip to content

Commit 8626f4f

Browse files
authored
Add limit and option.split (#542)
1 parent 7e83887 commit 8626f4f

File tree

6 files changed

+78
-61
lines changed

6 files changed

+78
-61
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- Added more options to `CliktCommand.test` to control the terminal interactivity. ([#517](https://github.com/ajalt/clikt/pull/517))
1818
- Added `associate{}`, `associateBy{}`, and `associateWith{}` transforms for options that allow you to convert the keys and values of the map. ([#529](https://github.com/ajalt/clikt/pull/529))
1919
- Added support for aliasing options to other options. ([#535](https://github.com/ajalt/clikt/pull/535))
20+
- Added `limit` and `ignoreCase` parameters to `option().split()`. ([#541](https://github.com/ajalt/clikt/pull/541))
2021

2122
### Changed
2223
- In a subcommand with and an `argument()` with `multiple()` or `optional()`, the behavior is now the same regardless of the value of `allowMultipleSubcommands`: if a token matches a subcommand name, it's now treated as a subcommand rather than a positional argument.

clikt-mordant/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,25 @@ class OptionTest {
256256
C().parse(argv)
257257
}
258258

259+
@Test
260+
@JsName("two_options_with_split_and_limit")
261+
fun `two options with split and limit`() = forAll(
262+
row("", null, null),
263+
row("-x 5 -y a", listOf("5"), listOf("a")),
264+
row("-x 5x6X7x8 -y a:b:c", listOf("5", "6", "7x8"), listOf("a", "b:c"))
265+
) { argv, ex, ey ->
266+
class C : TestCommand() {
267+
val x by option("-x").split("x", ignoreCase = true, limit = 3)
268+
val y by option("-y").split(Regex(":"), limit=2)
269+
override fun run_() {
270+
x shouldBe ex
271+
y shouldBe ey
272+
}
273+
}
274+
275+
C().parse(argv)
276+
}
277+
259278
@Test
260279
@JsName("flag_options")
261280
fun `flag options`() = forAll(

clikt/api/clikt.api

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,8 +1104,8 @@ public final class com/github/ajalt/clikt/parameters/options/OptionTransformCont
11041104
}
11051105

11061106
public abstract interface class com/github/ajalt/clikt/parameters/options/OptionWithValues : com/github/ajalt/clikt/parameters/options/OptionDelegate {
1107-
public abstract fun copy (Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/text/Regex;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZ)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1108-
public abstract fun copy (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/text/Regex;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZ)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1107+
public abstract fun copy (Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZ)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1108+
public abstract fun copy (Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZ)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11091109
public abstract fun getEnvvar ()Ljava/lang/String;
11101110
public abstract fun getExplicitCompletionCandidates ()Lcom/github/ajalt/clikt/completion/CompletionCandidates;
11111111
public abstract fun getHelpGetter ()Lkotlin/jvm/functions/Function1;
@@ -1114,12 +1114,12 @@ public abstract interface class com/github/ajalt/clikt/parameters/options/Option
11141114
public abstract fun getTransformEach ()Lkotlin/jvm/functions/Function2;
11151115
public abstract fun getTransformValidator ()Lkotlin/jvm/functions/Function2;
11161116
public abstract fun getTransformValue ()Lkotlin/jvm/functions/Function2;
1117-
public abstract fun getValueSplit ()Lkotlin/text/Regex;
1117+
public abstract fun getValueSplit ()Lkotlin/jvm/functions/Function1;
11181118
}
11191119

11201120
public final class com/github/ajalt/clikt/parameters/options/OptionWithValues$DefaultImpls {
1121-
public static synthetic fun copy$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/text/Regex;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1122-
public static synthetic fun copy$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/text/Regex;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1121+
public static synthetic fun copy$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1122+
public static synthetic fun copy$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Ljava/util/Set;Lkotlin/jvm/functions/Function1;Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;ZLjava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lcom/github/ajalt/clikt/completion/CompletionCandidates;Ljava/util/Set;ZZZILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11231123
public static fun getAcceptsNumberValueWithoutName (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Z
11241124
public static fun getAcceptsUnattachedValue (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Z
11251125
public static fun getCompletionCandidates (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/completion/CompletionCandidates;
@@ -1162,8 +1162,10 @@ public final class com/github/ajalt/clikt/parameters/options/OptionWithValuesKt
11621162
public static synthetic fun optionalValueLazy$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;ZLkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11631163
public static final fun pair (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11641164
public static final fun required (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1165-
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1166-
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/text/Regex;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1165+
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/text/Regex;I)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1166+
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;[Ljava/lang/String;ZI)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1167+
public static synthetic fun split$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/text/Regex;IILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
1168+
public static synthetic fun split$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;[Ljava/lang/String;ZIILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11671169
public static final fun splitPair (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11681170
public static synthetic fun splitPair$default (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;ILjava/lang/Object;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
11691171
public static final fun toMap (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;

clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/Convert.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ inline fun <InT : Any, ValueT : Any> NullableOption<InT, InT>.convert(
8888

8989

9090
/**
91-
* Change to option to take any number of values, separated by a [regex].
91+
* Change to option to take any number of values, separated by matched of a [regex].
9292
*
9393
* This must be called after converting the value type, and before other transforms.
9494
*
@@ -101,21 +101,24 @@ inline fun <InT : Any, ValueT : Any> NullableOption<InT, InT>.convert(
101101
* Which can be called like this:
102102
*
103103
* `./program --opt 1,2,3`
104+
*
105+
* @param limit Non-negative value specifying the maximum number of substrings to return.
106+
* Zero by default means no limit is set.
104107
*/
105-
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(regex: Regex)
108+
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(regex: Regex, limit: Int = 0)
106109
: OptionWithValues<List<ValueT>?, List<ValueT>, ValueT> {
107110
return copy(
108111
transformValue = transformValue,
109112
transformEach = { it },
110113
transformAll = defaultAllProcessor(),
111114
validator = defaultValidator(),
112115
nvalues = 1..1,
113-
valueSplit = regex
116+
valueSplit = { it.split(regex, limit) }
114117
)
115118
}
116119

117120
/**
118-
* Change to option to take any number of values, separated by a string [delimiter].
121+
* Change to option to take any number of values, separated by the [delimiters].
119122
*
120123
* This must be called after converting the value type, and before other transforms.
121124
*
@@ -128,10 +131,24 @@ fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(regex: Regex)
128131
* Which can be called like this:
129132
*
130133
* `./program --opt 1,2,3`
134+
*
135+
* @param delimiters One or more strings to be used as delimiters.
136+
* @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`.
137+
* @param limit The maximum number of substrings to return. Zero by default means no limit is set.
131138
*/
132-
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(delimiter: String)
133-
: OptionWithValues<List<ValueT>?, List<ValueT>, ValueT> {
134-
return split(Regex.fromLiteral(delimiter))
139+
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(
140+
vararg delimiters: String,
141+
ignoreCase: Boolean = false,
142+
limit: Int = 0,
143+
): OptionWithValues<List<ValueT>?, List<ValueT>, ValueT> {
144+
return copy(
145+
transformValue = transformValue,
146+
transformEach = { it },
147+
transformAll = defaultAllProcessor(),
148+
validator = defaultValidator(),
149+
nvalues = 1..1,
150+
valueSplit = { it.split(*delimiters, ignoreCase = ignoreCase, limit = limit) }
151+
)
135152
}
136153

137154

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

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,28 +140,22 @@ internal fun splitOptionPrefix(name: String): Pair<String, String> =
140140
@PublishedApi
141141
internal fun Option.longestName(): String? = names.maxByOrNull { it.length }
142142

143-
internal sealed class FinalValue {
144-
data class Parsed(val values: List<OptionInvocation>) : FinalValue()
145-
data class Sourced(val values: List<ValueSource.Invocation>) : FinalValue()
146-
data class Envvar(val key: String, val value: String) : FinalValue()
147-
}
148-
149143
internal fun Option.getFinalValue(
150144
context: Context,
151145
invocations: List<OptionInvocation>,
152146
envvar: String?,
153-
): FinalValue {
147+
): List<OptionInvocation> {
154148
return when {
155149
// We don't look at envvars or the value source for eager options
156-
eager || invocations.isNotEmpty() -> FinalValue.Parsed(invocations)
150+
eager || invocations.isNotEmpty() -> invocations
157151
context.readEnvvarBeforeValueSource -> {
158152
readEnvVar(context, envvar) ?: readValueSource(context)
159153
}
160154

161155
else -> {
162156
readValueSource(context) ?: readEnvVar(context, envvar)
163157
}
164-
} ?: FinalValue.Parsed(emptyList())
158+
} ?: emptyList()
165159
}
166160

167161
// This is a pretty ugly hack: option groups need to enforce their constraints, including on options
@@ -172,17 +166,19 @@ internal fun Option.hasEnvvarOrSourcedValue(
172166
context: Context,
173167
invocations: List<OptionInvocation>,
174168
): Boolean {
169+
if (invocations.isNotEmpty()) return false
175170
val envvar = (this as? OptionWithValues<*, *, *>)?.envvar
176171
val final = this.getFinalValue(context, invocations, envvar)
177-
return final !is FinalValue.Parsed
172+
return final.isNotEmpty()
178173
}
179174

180-
private fun Option.readValueSource(context: Context): FinalValue? {
181-
return context.valueSource?.getValues(context, this)?.ifEmpty { null }
182-
?.let { FinalValue.Sourced(it) }
175+
private fun Option.readValueSource(context: Context): List<OptionInvocation>? {
176+
return context.valueSource?.getValues(context, this)
177+
?.map { OptionInvocation("", it.values) }
178+
?.ifEmpty { null }
183179
}
184180

185-
private fun Option.readEnvVar(context: Context, envvar: String?): FinalValue? {
181+
private fun Option.readEnvVar(context: Context, envvar: String?): List<OptionInvocation>? {
186182
val env = inferEnvvar(names, envvar, context.autoEnvvarPrefix) ?: return null
187-
return context.readEnvvar(env)?.let { FinalValue.Envvar(env, it) }
183+
return context.readEnvvar(env)?.let { listOf(OptionInvocation(env, listOf(it))) }
188184
}

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

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ interface OptionWithValues<AllT, EachT, ValueT> : OptionDelegate<AllT> {
106106
/** A block that will return the help text for this option, or `null` if no getter has been specified */
107107
val helpGetter: (HelpTransformContext.() -> String)?
108108

109-
/** A regex to split option values on before conversion, or `null` to leave them unsplit */
110-
val valueSplit: Regex?
109+
/** A function to split option values on before conversion */
110+
val valueSplit: (String) -> List<String>
111111

112112
/** Create a new option that is a copy of this one with different transforms. */
113113
fun <AllT, EachT, ValueT> copy(
@@ -123,7 +123,7 @@ interface OptionWithValues<AllT, EachT, ValueT> : OptionDelegate<AllT> {
123123
helpTags: Map<String, String> = this.helpTags,
124124
valueSourceKey: String? = this.valueSourceKey,
125125
envvar: String? = this.envvar,
126-
valueSplit: Regex? = this.valueSplit,
126+
valueSplit: (String) -> List<String> = this.valueSplit,
127127
completionCandidates: CompletionCandidates? = explicitCompletionCandidates,
128128
secondaryNames: Set<String> = this.secondaryNames,
129129
acceptsNumberValueWithoutName: Boolean = this.acceptsNumberValueWithoutName,
@@ -142,7 +142,7 @@ interface OptionWithValues<AllT, EachT, ValueT> : OptionDelegate<AllT> {
142142
helpTags: Map<String, String> = this.helpTags,
143143
envvar: String? = this.envvar,
144144
valueSourceKey: String? = this.valueSourceKey,
145-
valueSplit: Regex? = this.valueSplit,
145+
valueSplit: (String) -> List<String> = this.valueSplit,
146146
completionCandidates: CompletionCandidates? = explicitCompletionCandidates,
147147
secondaryNames: Set<String> = this.secondaryNames,
148148
acceptsNumberValueWithoutName: Boolean = this.acceptsNumberValueWithoutName,
@@ -161,7 +161,7 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
161161
override val helpTags: Map<String, String>,
162162
override val valueSourceKey: String?,
163163
override val envvar: String?,
164-
override val valueSplit: Regex?,
164+
override val valueSplit: (String) -> List<String>,
165165
override val explicitCompletionCandidates: CompletionCandidates?,
166166
override val secondaryNames: Set<String>,
167167
override val acceptsNumberValueWithoutName: Boolean,
@@ -188,31 +188,13 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
188188
}
189189

190190
override fun finalize(context: Context, invocations: List<OptionInvocation>) {
191-
val invs = when (val v = getFinalValue(context, invocations, envvar)) {
192-
is FinalValue.Parsed -> {
193-
when (valueSplit) {
194-
null -> {
195-
invocations.find { it.values.size !in nvalues }?.let {
196-
throw IncorrectOptionValueCount(this, it.name)
197-
}
198-
invocations
199-
}
200-
else -> invocations.map { inv ->
201-
inv.copy(values = inv.values.flatMap { it.split(valueSplit) })
202-
}
203-
}
204-
}
205-
206-
is FinalValue.Sourced -> {
207-
v.values.map { OptionInvocation("", it.values) }
208-
}
209-
210-
is FinalValue.Envvar -> {
211-
when (valueSplit) {
212-
null -> listOf(OptionInvocation(v.key, listOf(v.value)))
213-
else -> listOf(OptionInvocation(v.key, v.value.split(valueSplit)))
214-
}
191+
val invs = getFinalValue(context, invocations, envvar).map { inv ->
192+
// Only enforce nvalues if there are command line invocations, since some options like
193+
// switches work differently for envvars.
194+
if (invocations.isNotEmpty() && inv.values.size !in nvalues) {
195+
throw IncorrectOptionValueCount(this, inv.name)
215196
}
197+
inv.copy(values = inv.values.flatMap { valueSplit(it) })
216198
}
217199

218200
value = transformAll(OptionTransformContext(this, context), invs.map {
@@ -248,7 +230,7 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
248230
helpTags: Map<String, String>,
249231
valueSourceKey: String?,
250232
envvar: String?,
251-
valueSplit: Regex?,
233+
valueSplit: (String) -> List<String>,
252234
completionCandidates: CompletionCandidates?,
253235
secondaryNames: Set<String>,
254236
acceptsNumberValueWithoutName: Boolean,
@@ -288,7 +270,7 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
288270
helpTags: Map<String, String>,
289271
envvar: String?,
290272
valueSourceKey: String?,
291-
valueSplit: Regex?,
273+
valueSplit: (String) -> List<String>,
292274
completionCandidates: CompletionCandidates?,
293275
secondaryNames: Set<String>,
294276
acceptsNumberValueWithoutName: Boolean,
@@ -378,7 +360,7 @@ fun ParameterHolder.option(
378360
helpTags = helpTags,
379361
valueSourceKey = valueSourceKey,
380362
envvar = envvar,
381-
valueSplit = null,
363+
valueSplit = ::listOf,
382364
explicitCompletionCandidates = completionCandidates,
383365
secondaryNames = emptySet(),
384366
acceptsNumberValueWithoutName = false,

0 commit comments

Comments
 (0)