Skip to content

Commit

Permalink
Add limit and option.split (#542)
Browse files Browse the repository at this point in the history
  • Loading branch information
ajalt authored Sep 7, 2024
1 parent 7e83887 commit 8626f4f
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 61 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- Added more options to `CliktCommand.test` to control the terminal interactivity. ([#517](https://github.com/ajalt/clikt/pull/517))
- 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))
- Added support for aliasing options to other options. ([#535](https://github.com/ajalt/clikt/pull/535))
- Added `limit` and `ignoreCase` parameters to `option().split()`. ([#541](https://github.com/ajalt/clikt/pull/541))

### Changed
- 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,25 @@ class OptionTest {
C().parse(argv)
}

@Test
@JsName("two_options_with_split_and_limit")
fun `two options with split and limit`() = forAll(
row("", null, null),
row("-x 5 -y a", listOf("5"), listOf("a")),
row("-x 5x6X7x8 -y a:b:c", listOf("5", "6", "7x8"), listOf("a", "b:c"))
) { argv, ex, ey ->
class C : TestCommand() {
val x by option("-x").split("x", ignoreCase = true, limit = 3)
val y by option("-y").split(Regex(":"), limit=2)
override fun run_() {
x shouldBe ex
y shouldBe ey
}
}

C().parse(argv)
}

@Test
@JsName("flag_options")
fun `flag options`() = forAll(
Expand Down
16 changes: 9 additions & 7 deletions clikt/api/clikt.api
Original file line number Diff line number Diff line change
Expand Up @@ -1104,8 +1104,8 @@ public final class com/github/ajalt/clikt/parameters/options/OptionTransformCont
}

public abstract interface class com/github/ajalt/clikt/parameters/options/OptionWithValues : com/github/ajalt/clikt/parameters/options/OptionDelegate {
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;
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;
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;
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;
public abstract fun getEnvvar ()Ljava/lang/String;
public abstract fun getExplicitCompletionCandidates ()Lcom/github/ajalt/clikt/completion/CompletionCandidates;
public abstract fun getHelpGetter ()Lkotlin/jvm/functions/Function1;
Expand All @@ -1114,12 +1114,12 @@ public abstract interface class com/github/ajalt/clikt/parameters/options/Option
public abstract fun getTransformEach ()Lkotlin/jvm/functions/Function2;
public abstract fun getTransformValidator ()Lkotlin/jvm/functions/Function2;
public abstract fun getTransformValue ()Lkotlin/jvm/functions/Function2;
public abstract fun getValueSplit ()Lkotlin/text/Regex;
public abstract fun getValueSplit ()Lkotlin/jvm/functions/Function1;
}

public final class com/github/ajalt/clikt/parameters/options/OptionWithValues$DefaultImpls {
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;
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;
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;
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;
public static fun getAcceptsNumberValueWithoutName (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Z
public static fun getAcceptsUnattachedValue (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Z
public static fun getCompletionCandidates (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/completion/CompletionCandidates;
Expand Down Expand Up @@ -1162,8 +1162,10 @@ public final class com/github/ajalt/clikt/parameters/options/OptionWithValuesKt
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;
public static final fun pair (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
public static final fun required (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/text/Regex;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Lkotlin/text/Regex;I)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
public static final fun split (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;[Ljava/lang/String;ZI)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
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;
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;
public static final fun splitPair (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;Ljava/lang/String;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
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;
public static final fun toMap (Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;)Lcom/github/ajalt/clikt/parameters/options/OptionWithValues;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ inline fun <InT : Any, ValueT : Any> NullableOption<InT, InT>.convert(


/**
* Change to option to take any number of values, separated by a [regex].
* Change to option to take any number of values, separated by matched of a [regex].
*
* This must be called after converting the value type, and before other transforms.
*
Expand All @@ -101,21 +101,24 @@ inline fun <InT : Any, ValueT : Any> NullableOption<InT, InT>.convert(
* Which can be called like this:
*
* `./program --opt 1,2,3`
*
* @param limit Non-negative value specifying the maximum number of substrings to return.
* Zero by default means no limit is set.
*/
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(regex: Regex)
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(regex: Regex, limit: Int = 0)
: OptionWithValues<List<ValueT>?, List<ValueT>, ValueT> {
return copy(
transformValue = transformValue,
transformEach = { it },
transformAll = defaultAllProcessor(),
validator = defaultValidator(),
nvalues = 1..1,
valueSplit = regex
valueSplit = { it.split(regex, limit) }
)
}

/**
* Change to option to take any number of values, separated by a string [delimiter].
* Change to option to take any number of values, separated by the [delimiters].
*
* This must be called after converting the value type, and before other transforms.
*
Expand All @@ -128,10 +131,24 @@ fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(regex: Regex)
* Which can be called like this:
*
* `./program --opt 1,2,3`
*
* @param delimiters One or more strings to be used as delimiters.
* @param ignoreCase `true` to ignore character case when matching a delimiter. By default `false`.
* @param limit The maximum number of substrings to return. Zero by default means no limit is set.
*/
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(delimiter: String)
: OptionWithValues<List<ValueT>?, List<ValueT>, ValueT> {
return split(Regex.fromLiteral(delimiter))
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.split(
vararg delimiters: String,
ignoreCase: Boolean = false,
limit: Int = 0,
): OptionWithValues<List<ValueT>?, List<ValueT>, ValueT> {
return copy(
transformValue = transformValue,
transformEach = { it },
transformAll = defaultAllProcessor(),
validator = defaultValidator(),
nvalues = 1..1,
valueSplit = { it.split(*delimiters, ignoreCase = ignoreCase, limit = limit) }
)
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,28 +140,22 @@ internal fun splitOptionPrefix(name: String): Pair<String, String> =
@PublishedApi
internal fun Option.longestName(): String? = names.maxByOrNull { it.length }

internal sealed class FinalValue {
data class Parsed(val values: List<OptionInvocation>) : FinalValue()
data class Sourced(val values: List<ValueSource.Invocation>) : FinalValue()
data class Envvar(val key: String, val value: String) : FinalValue()
}

internal fun Option.getFinalValue(
context: Context,
invocations: List<OptionInvocation>,
envvar: String?,
): FinalValue {
): List<OptionInvocation> {
return when {
// We don't look at envvars or the value source for eager options
eager || invocations.isNotEmpty() -> FinalValue.Parsed(invocations)
eager || invocations.isNotEmpty() -> invocations
context.readEnvvarBeforeValueSource -> {
readEnvVar(context, envvar) ?: readValueSource(context)
}

else -> {
readValueSource(context) ?: readEnvVar(context, envvar)
}
} ?: FinalValue.Parsed(emptyList())
} ?: emptyList()
}

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

private fun Option.readValueSource(context: Context): FinalValue? {
return context.valueSource?.getValues(context, this)?.ifEmpty { null }
?.let { FinalValue.Sourced(it) }
private fun Option.readValueSource(context: Context): List<OptionInvocation>? {
return context.valueSource?.getValues(context, this)
?.map { OptionInvocation("", it.values) }
?.ifEmpty { null }
}

private fun Option.readEnvVar(context: Context, envvar: String?): FinalValue? {
private fun Option.readEnvVar(context: Context, envvar: String?): List<OptionInvocation>? {
val env = inferEnvvar(names, envvar, context.autoEnvvarPrefix) ?: return null
return context.readEnvvar(env)?.let { FinalValue.Envvar(env, it) }
return context.readEnvvar(env)?.let { listOf(OptionInvocation(env, listOf(it))) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ interface OptionWithValues<AllT, EachT, ValueT> : OptionDelegate<AllT> {
/** A block that will return the help text for this option, or `null` if no getter has been specified */
val helpGetter: (HelpTransformContext.() -> String)?

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

/** Create a new option that is a copy of this one with different transforms. */
fun <AllT, EachT, ValueT> copy(
Expand All @@ -123,7 +123,7 @@ interface OptionWithValues<AllT, EachT, ValueT> : OptionDelegate<AllT> {
helpTags: Map<String, String> = this.helpTags,
valueSourceKey: String? = this.valueSourceKey,
envvar: String? = this.envvar,
valueSplit: Regex? = this.valueSplit,
valueSplit: (String) -> List<String> = this.valueSplit,
completionCandidates: CompletionCandidates? = explicitCompletionCandidates,
secondaryNames: Set<String> = this.secondaryNames,
acceptsNumberValueWithoutName: Boolean = this.acceptsNumberValueWithoutName,
Expand All @@ -142,7 +142,7 @@ interface OptionWithValues<AllT, EachT, ValueT> : OptionDelegate<AllT> {
helpTags: Map<String, String> = this.helpTags,
envvar: String? = this.envvar,
valueSourceKey: String? = this.valueSourceKey,
valueSplit: Regex? = this.valueSplit,
valueSplit: (String) -> List<String> = this.valueSplit,
completionCandidates: CompletionCandidates? = explicitCompletionCandidates,
secondaryNames: Set<String> = this.secondaryNames,
acceptsNumberValueWithoutName: Boolean = this.acceptsNumberValueWithoutName,
Expand All @@ -161,7 +161,7 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
override val helpTags: Map<String, String>,
override val valueSourceKey: String?,
override val envvar: String?,
override val valueSplit: Regex?,
override val valueSplit: (String) -> List<String>,
override val explicitCompletionCandidates: CompletionCandidates?,
override val secondaryNames: Set<String>,
override val acceptsNumberValueWithoutName: Boolean,
Expand All @@ -188,31 +188,13 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
}

override fun finalize(context: Context, invocations: List<OptionInvocation>) {
val invs = when (val v = getFinalValue(context, invocations, envvar)) {
is FinalValue.Parsed -> {
when (valueSplit) {
null -> {
invocations.find { it.values.size !in nvalues }?.let {
throw IncorrectOptionValueCount(this, it.name)
}
invocations
}
else -> invocations.map { inv ->
inv.copy(values = inv.values.flatMap { it.split(valueSplit) })
}
}
}

is FinalValue.Sourced -> {
v.values.map { OptionInvocation("", it.values) }
}

is FinalValue.Envvar -> {
when (valueSplit) {
null -> listOf(OptionInvocation(v.key, listOf(v.value)))
else -> listOf(OptionInvocation(v.key, v.value.split(valueSplit)))
}
val invs = getFinalValue(context, invocations, envvar).map { inv ->
// Only enforce nvalues if there are command line invocations, since some options like
// switches work differently for envvars.
if (invocations.isNotEmpty() && inv.values.size !in nvalues) {
throw IncorrectOptionValueCount(this, inv.name)
}
inv.copy(values = inv.values.flatMap { valueSplit(it) })
}

value = transformAll(OptionTransformContext(this, context), invs.map {
Expand Down Expand Up @@ -248,7 +230,7 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
helpTags: Map<String, String>,
valueSourceKey: String?,
envvar: String?,
valueSplit: Regex?,
valueSplit: (String) -> List<String>,
completionCandidates: CompletionCandidates?,
secondaryNames: Set<String>,
acceptsNumberValueWithoutName: Boolean,
Expand Down Expand Up @@ -288,7 +270,7 @@ private class OptionWithValuesImpl<AllT, EachT, ValueT>(
helpTags: Map<String, String>,
envvar: String?,
valueSourceKey: String?,
valueSplit: Regex?,
valueSplit: (String) -> List<String>,
completionCandidates: CompletionCandidates?,
secondaryNames: Set<String>,
acceptsNumberValueWithoutName: Boolean,
Expand Down Expand Up @@ -378,7 +360,7 @@ fun ParameterHolder.option(
helpTags = helpTags,
valueSourceKey = valueSourceKey,
envvar = envvar,
valueSplit = null,
valueSplit = ::listOf,
explicitCompletionCandidates = completionCandidates,
secondaryNames = emptySet(),
acceptsNumberValueWithoutName = false,
Expand Down

0 comments on commit 8626f4f

Please sign in to comment.