Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add limit and option.split #542

Merged
merged 1 commit into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading