Skip to content

Commit b820559

Browse files
committed
Run validation after finalization
1 parent fd5ed39 commit b820559

File tree

18 files changed

+327
-98
lines changed

18 files changed

+327
-98
lines changed

CHANGELOG.md

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,18 @@
22

33
## [Unreleased]
44
### Added
5-
- [Bash autocomplete script generation](https://ajalt.github.io/clikt/autocomplete/). A property named
6-
`completionCandidates` has been added to `Argument` and `Option` interfaces, and corresponding
7-
parameters have been added to the various implementation constructors, as well as the `convert`
8-
functions. You can use this to control the values autocomplete that will be suggested.
9-
- [`option().split()`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/split.html),
10-
and the corresponding
11-
[`OptionWithValues.valueSplit`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/-option-with-values/value-split.html).
5+
- [Bash autocomplete script generation](https://ajalt.github.io/clikt/autocomplete/). A property named `completionCandidates` has been added to `Argument` and `Option` interfaces, and corresponding parameters have been added to the various implementation constructors, as well as the `convert` functions. You can use this to control the values autocomplete that will be suggested.
6+
- [`option().split()`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/split.html), and the corresponding [`OptionWithValues.valueSplit`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/-option-with-values/value-split.html).
127
- Marking options as deprecated with [`option().deprecated()`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/deprecated.html)
138
- You can manually set the pattern to split envvars on by passing a pattern to the `envvarSplit` parameter of [`option()`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/option.html)
149
- [Option groups](https://ajalt.github.io/clikt/documenting/#grouping-options-in-help), [mutually exclusive groups](https://ajalt.github.io/clikt/options/#prompting-for-input), [co-occurring groups](https://ajalt.github.io/clikt/options/#co-occurring-option-groups), and [choice options with groups](https://ajalt.github.io/clikt/options/#choice-options-with-groups)
1510

1611
### Changed
17-
- If multiple `--` tokens are present on the command line, all subsequent occurrences after the first are now
18-
parsed as positional arguments. Previously, subsequent `--` tokens were skipped.
19-
- The `PlaintextHelpFormatter` has been replaced with `CliktHelpFormatter`, which is more
20-
customizable. See [the docs](https://ajalt.github.io/clikt/documenting/) for more info, or the
21-
[new sample](samples/ansicolors/README.md) for an example of customizing help output to use ANSI colors.
22-
- Some of the properties and constructor parameters for [`OptionWithValues`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/-option-with-values/index.html) have changed.
12+
- If multiple `--` tokens are present on the command line, all subsequent occurrences after the first are now parsed as positional arguments. Previously, subsequent `--` tokens were skipped.
13+
- The `PlaintextHelpFormatter` has been replaced with `CliktHelpFormatter`, which is more customizable. See [the docs](https://ajalt.github.io/clikt/documenting/) for more info, or the [new sample](samples/ansicolors/README.md) for an example of customizing help output to use ANSI colors.
14+
- Some of the properties and constructor parameters for [`OptionWithValues`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.options/-option-with-values/index.html) and [`ProcessedArgument`](https://ajalt.github.io/clikt/api/clikt/com.github.ajalt.clikt.parameters.arguments/-processed-argument/index.html) have changed.
2315
- The `OptionDelegate` interface has changed, and `GroupableOption` and `ParameterHolder` interfaces have been added to work with option groups.
16+
- [Parameter validation](https://ajalt.github.io/clikt/parameters/#parameter-validation) now occurs after all parameter delegates have set their values, so the lambdas passed to `validate` may reference other parameters.
2417

2518
## [1.7.0] - 2019-03-23
2619
### Added

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ subprojects {
3030
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
3131

3232
testImplementation 'junit:junit:4.12'
33-
testImplementation 'io.kotlintest:kotlintest-assertions:3.1.10'
33+
testImplementation 'io.kotlintest:kotlintest-assertions:3.3.2'
3434
testImplementation 'com.github.stefanbirkner:system-rules:1.18.0'
3535
testImplementation 'com.google.jimfs:jimfs:1.1'
3636
}

clikt/src/main/kotlin/com/github/ajalt/clikt/parameters/arguments/Argument.kt

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,18 @@ interface Argument {
5050
/**
5151
* Called after this command's argv is parsed to transform and store the argument's value.
5252
*
53+
* You cannot refer to other parameter values during this call, since they might not have been
54+
* finalized yet.
55+
*
5356
* @param context The context for this parse
5457
* @param values A possibly empty list of values provided to this argument.
5558
*/
5659
fun finalize(context: Context, values: List<String>)
60+
61+
/**
62+
* Called after all of a command's parameters have been [finalize]d to perform validation of the final value.
63+
*/
64+
fun postValidate(context: Context)
5765
}
5866

5967
/** An argument that functions as a property delegate */
@@ -94,6 +102,7 @@ typealias ArgValidator<AllT> = ArgumentTransformContext.(AllT) -> Unit
94102
*
95103
* @property transformValue Called in [finalize] to transform each value provided to the argument.
96104
* @property transformAll Called in [finalize] to transform the list of values to the final type.
105+
* @property transformValidator Called after all paramters have been [finalize]d to validate the result of [transformAll]
97106
*/
98107
class ProcessedArgument<AllT, ValueT>(
99108
name: String,
@@ -103,7 +112,8 @@ class ProcessedArgument<AllT, ValueT>(
103112
override val helpTags: Map<String, String>,
104113
override val completionCandidates: CompletionCandidates,
105114
val transformValue: ArgValueTransformer<ValueT>,
106-
val transformAll: ArgCallsTransformer<AllT, ValueT>
115+
val transformAll: ArgCallsTransformer<AllT, ValueT>,
116+
val transformValidator: ArgValidator<AllT>
107117
) : ArgumentDelegate<AllT> {
108118
init {
109119
require(nvalues != 0) { "Arguments cannot have nvalues == 0" }
@@ -130,18 +140,36 @@ class ProcessedArgument<AllT, ValueT>(
130140
value = transformAll(ctx, values.map { transformValue(ctx, it) })
131141
}
132142

143+
override fun postValidate(context: Context) {
144+
transformValidator(ArgumentTransformContext(this, context), value)
145+
}
146+
133147
/** Create a new argument that is a copy of this one with different transforms. */
134148
fun <AllT, ValueT> copy(
135149
transformValue: ArgValueTransformer<ValueT>,
136150
transformAll: ArgCallsTransformer<AllT, ValueT>,
151+
validator: ArgValidator<AllT>,
137152
name: String = this.name,
138153
nvalues: Int = this.nvalues,
139154
required: Boolean = this.required,
140155
help: String = this.help,
141156
helpTags: Map<String, String> = this.helpTags,
142157
completionCandidates: CompletionCandidates = this.completionCandidates
143158
): ProcessedArgument<AllT, ValueT> {
144-
return ProcessedArgument(name, nvalues, required, help, helpTags, completionCandidates, transformValue, transformAll)
159+
return ProcessedArgument(name, nvalues, required, help, helpTags, completionCandidates, transformValue, transformAll, validator)
160+
}
161+
162+
/** Create a new argument that is a copy of this one with the same transforms. */
163+
fun copy(
164+
validator: ArgValidator<AllT> = this.transformValidator,
165+
name: String = this.name,
166+
nvalues: Int = this.nvalues,
167+
required: Boolean = this.required,
168+
help: String = this.help,
169+
helpTags: Map<String, String> = this.helpTags,
170+
completionCandidates: CompletionCandidates = this.completionCandidates
171+
): ProcessedArgument<AllT, ValueT> {
172+
return ProcessedArgument(name, nvalues, required, help, helpTags, completionCandidates, transformValue, transformAll, validator)
145173
}
146174
}
147175

@@ -150,6 +178,9 @@ internal typealias RawArgument = ProcessedArgument<String, String>
150178
@PublishedApi
151179
internal fun <T : Any> defaultAllProcessor(): ArgCallsTransformer<T, T> = { it.single() }
152180

181+
@PublishedApi
182+
internal fun <T> defaultValidator(): ArgValidator<T> = {}
183+
153184
/**
154185
* Create a property delegate argument.
155186
*
@@ -162,8 +193,22 @@ internal fun <T : Any> defaultAllProcessor(): ArgCallsTransformer<T, T> = { it.s
162193
* @param helpTags Extra information about this option to pass to the help formatter
163194
*/
164195
@Suppress("unused")
165-
fun CliktCommand.argument(name: String = "", help: String = "", helpTags: Map<String, String> = emptyMap()): RawArgument {
166-
return ProcessedArgument(name, 1, true, help, helpTags, CompletionCandidates.None, { it }, defaultAllProcessor())
196+
fun CliktCommand.argument(
197+
name: String = "",
198+
help: String = "",
199+
helpTags: Map<String, String> = emptyMap()
200+
): RawArgument {
201+
return ProcessedArgument(
202+
name = name,
203+
nvalues = 1,
204+
required = true,
205+
help = help,
206+
helpTags = helpTags,
207+
completionCandidates = CompletionCandidates.None,
208+
transformValue = { it },
209+
transformAll = defaultAllProcessor(),
210+
transformValidator = defaultValidator()
211+
)
167212
}
168213

169214
/**
@@ -182,7 +227,7 @@ fun <AllInT, ValueT, AllOutT> ProcessedArgument<AllInT, ValueT>.transformAll(
182227
nvalues: Int? = null,
183228
required: Boolean? = null,
184229
transform: ArgCallsTransformer<AllOutT, ValueT>): ProcessedArgument<AllOutT, ValueT> {
185-
return copy(transformValue, transform,
230+
return copy(transformValue, transform, defaultValidator(),
186231
nvalues = nvalues ?: this.nvalues,
187232
required = required ?: this.required)
188233
}
@@ -316,7 +361,7 @@ inline fun <T : Any> RawArgument.convert(
316361
fail(err.message ?: "")
317362
}
318363
}
319-
return copy(conv, defaultAllProcessor(), completionCandidates = completionCandidates)
364+
return copy(conv, defaultAllProcessor(), defaultValidator(), completionCandidates = completionCandidates)
320365
}
321366

322367
/**
@@ -335,9 +380,7 @@ inline fun <T : Any> RawArgument.convert(
335380
*/
336381
fun <AllT : Any, ValueT> ProcessedArgument<AllT, ValueT>.validate(validator: ArgValidator<AllT>)
337382
: ArgumentDelegate<AllT> {
338-
return copy(transformValue, {
339-
transformAll(it).also { validator(ArgumentTransformContext(argument, context), it) }
340-
})
383+
return copy(validator)
341384
}
342385

343386
/**
@@ -357,7 +400,5 @@ fun <AllT : Any, ValueT> ProcessedArgument<AllT, ValueT>.validate(validator: Arg
357400
@JvmName("nullableValidate")
358401
fun <AllT : Any, ValueT> ProcessedArgument<AllT?, ValueT>.validate(validator: ArgValidator<AllT>)
359402
: ArgumentDelegate<AllT?> {
360-
return copy(transformValue, {
361-
transformAll(it).also { if (it != null) validator(ArgumentTransformContext(argument, context), it) }
362-
})
403+
return copy({ if (it != null) validator(it) })
363404
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import com.github.ajalt.clikt.core.MissingParameter
88
import com.github.ajalt.clikt.parameters.internal.NullableLateinit
99
import com.github.ajalt.clikt.parameters.options.Option
1010
import com.github.ajalt.clikt.parameters.options.RawOption
11-
import com.github.ajalt.clikt.parameters.options.option
12-
import com.github.ajalt.clikt.parameters.options.required
1311
import com.github.ajalt.clikt.parsers.OptionParser
1412
import kotlin.properties.ReadOnlyProperty
1513
import kotlin.reflect.KProperty
@@ -22,6 +20,7 @@ class ChoiceGroup<GroupT : OptionGroup, OutT>(
2220
override val groupName: String? = null
2321
override val groupHelp: String? = null
2422
private var value: OutT by NullableLateinit("Cannot read from option delegate before parsing command line")
23+
private var chosenGroup: OptionGroup? = null
2524

2625
override fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty<CliktCommand, OutT> {
2726
option.provideDelegate(thisRef, prop) // infer the option name and register it
@@ -52,8 +51,13 @@ class ChoiceGroup<GroupT : OptionGroup, OutT>(
5251
context
5352
)
5453
group.finalize(context, invocationsByOption.filterKeys { it in group.options })
54+
chosenGroup = group
5555
value = transform(group)
5656
}
57+
58+
override fun postValidate(context: Context) {
59+
chosenGroup?.options?.forEach { it.postValidate(context) }
60+
}
5761
}
5862

5963
/**
@@ -68,10 +72,7 @@ class ChoiceGroup<GroupT : OptionGroup, OutT>(
6872
* @see com.github.ajalt.clikt.parameters.types.choice
6973
*/
7074
fun <T : OptionGroup> RawOption.groupChoice(choices: Map<String, T>): ChoiceGroup<T, T?> {
71-
return ChoiceGroup(
72-
copy(transformValue, transformEach, transformAll, completionCandidates = Fixed(choices.keys)),
73-
choices
74-
) { it }
75+
return ChoiceGroup(copy(completionCandidates = Fixed(choices.keys)), choices) { it }
7576
}
7677

7778
/**

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class CoOccurringOptionGroup<GroupT : OptionGroup, OutT>(
1616
override val groupName: String? get() = group.groupName
1717
override val groupHelp: String? get() = group.groupHelp
1818
private var value: OutT by NullableLateinit("Cannot read from option delegate before parsing command line")
19+
private var occurred = false
1920

2021
override fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty<CliktCommand, OutT> {
2122
thisRef.registerOptionGroup(this)
@@ -30,10 +31,14 @@ class CoOccurringOptionGroup<GroupT : OptionGroup, OutT>(
3031
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): OutT = value
3132

3233
override fun finalize(context: Context, invocationsByOption: Map<Option, List<OptionParser.Invocation>>) {
33-
val occurred = invocationsByOption.isNotEmpty()
34+
occurred = invocationsByOption.isNotEmpty()
3435
if (occurred) group.finalize(context, invocationsByOption)
3536
value = transform(occurred, group, context)
3637
}
38+
39+
override fun postValidate(context: Context) {
40+
if (occurred) group.postValidate(context)
41+
}
3742
}
3843

3944
/**

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ class MutuallyExclusiveOptions<OptT : Any, OutT>(
1919

2020
private var value: OutT by NullableLateinit("Cannot read from group delegate before parsing command line")
2121

22+
override operator fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty<CliktCommand, OutT> {
23+
thisRef.registerOptionGroup(this)
24+
25+
for (option in options) {
26+
require(option.names.isNotEmpty()) { "must specify names for all options in a group" }
27+
option.parameterGroup = this
28+
option.groupName = groupName
29+
thisRef.registerOption(option)
30+
}
31+
32+
return this
33+
}
34+
2235
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): OutT = value
2336

2437
override fun finalize(context: Context, invocationsByOption: Map<Option, List<OptionParser.Invocation>>) {
@@ -27,21 +40,20 @@ class MutuallyExclusiveOptions<OptT : Any, OutT>(
2740
option.finalize(context, invocations)
2841
}
2942

43+
for (option in options) {
44+
if (option !in invocationsByOption) {
45+
option.finalize(context, emptyList())
46+
}
47+
}
48+
3049
val values = options.mapNotNull { it.value }
3150
value = transformAll(values)
3251
}
3352

34-
override operator fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty<CliktCommand, OutT> {
35-
thisRef.registerOptionGroup(this)
36-
53+
override fun postValidate(context: Context) {
3754
for (option in options) {
38-
require(option.names.isNotEmpty()) { "must specify names for all options in a group" }
39-
option.parameterGroup = this
40-
option.groupName = groupName
41-
thisRef.registerOption(option)
55+
option.postValidate(context)
4256
}
43-
44-
return this
4557
}
4658

4759
fun <T> copy(transformAll: (List<OptT>) -> T) = MutuallyExclusiveOptions(options, groupName, groupHelp, transformAll)

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,6 @@ import kotlin.properties.ReadOnlyProperty
1111
import kotlin.reflect.KProperty
1212

1313
interface ParameterGroup {
14-
/**
15-
* Called after this command's argv is parsed and all options are validated to validate the group constraints.
16-
*
17-
* @param context The context for this parse
18-
* @param invocationsByOption The invocations of options in this group.
19-
*/
20-
fun finalize(context: Context, invocationsByOption: Map<Option, List<OptionParser.Invocation>>)
21-
2214
/**
2315
* The name of the group, or null if parameters in the group should not be separated from other
2416
* parameters in the help output.
@@ -37,6 +29,19 @@ interface ParameterGroup {
3729
val h = groupHelp
3830
return if (n == null || h == null) null else HelpFormatter.ParameterHelp.Group(n, h)
3931
}
32+
33+
/**
34+
* Called after this command's argv is parsed and all options are validated to validate the group constraints.
35+
*
36+
* @param context The context for this parse
37+
* @param invocationsByOption The invocations of options in this group.
38+
*/
39+
fun finalize(context: Context, invocationsByOption: Map<Option, List<OptionParser.Invocation>>)
40+
41+
/**
42+
* Called after all of a command's parameters have been [finalize]d to perform validation of the final values.
43+
*/
44+
fun postValidate(context: Context)
4045
}
4146

4247
interface ParameterGroupDelegate<out T> : ParameterGroup, ReadOnlyProperty<CliktCommand, T> {
@@ -92,6 +97,8 @@ open class OptionGroup(
9297
// Finalize options not provided on the command line so that they can apply default values etc.
9398
options.forEach { o -> if (o !in invocationsByOption) o.finalize(context, emptyList()) }
9499
}
100+
101+
override fun postValidate(context: Context) = options.forEach { it.postValidate(context) }
95102
}
96103

97104
operator fun <T : OptionGroup> T.provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty<CliktCommand, T> {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class EagerOption(
2828
override val secondaryNames: Set<String> get() = emptySet()
2929
override val parser: OptionParser = FlagOptionParser
3030
override val metavar: String? get() = null
31+
override fun postValidate(context: Context) {}
3132
override fun finalize(context: Context, invocations: List<OptionParser.Invocation>) {
3233
this.callback(OptionTransformContext(this, context))
3334
}

0 commit comments

Comments
 (0)