-
Notifications
You must be signed in to change notification settings - Fork 121
/
Option.kt
188 lines (159 loc) · 7.39 KB
/
Option.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
package com.github.ajalt.clikt.parameters.options
import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.GroupableOption
import com.github.ajalt.clikt.core.ParameterHolder
import com.github.ajalt.clikt.core.StaticallyGroupedOption
import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.parsers.OptionInvocation
import com.github.ajalt.clikt.sources.ValueSource
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* An optional command line parameter that takes a fixed number of values.
*
* Options can take any fixed number of values, including 0.
*/
interface Option {
/** A name representing the values for this option that can be displayed to the user. */
fun metavar(context: Context): String?
/** The description of this option, usually a single line. */
fun optionHelp(context: Context): String
/** The names that can be used to invoke this option. They must start with a punctuation character. */
val names: Set<String>
/** Names that can be used for a secondary purpose, like disabling flag options. */
val secondaryNames: Set<String>
/** The min and max number of values that must be given to this option. */
val nvalues: IntRange
/** If true, this option should not appear in help output. */
val hidden: Boolean
/** Extra information about this option to pass to the help formatter. */
val helpTags: Map<String, String>
/** Optional set of strings to use when the user invokes shell autocomplete on a value for this option. */
val completionCandidates: CompletionCandidates get() = CompletionCandidates.None
/** Optional explicit key to use when looking this option up from a [ValueSource] */
val valueSourceKey: String?
/** If true, this option can be specified without a name e.g. `-2` instead of `-o2` */
val acceptsNumberValueWithoutName: Boolean get() = false
/** If true, the presence of this option on the command line will halt parsing immediately */
val eager: Boolean get() = false
/** If false, invocations must be of the form `--foo=1` or `-f1`. If true, the forms `--foo 1` and `-f 1` are also accepted. */
val acceptsUnattachedValue: Boolean get() = true
/** Information about this option for the help output. */
fun parameterHelp(context: Context): HelpFormatter.ParameterHelp.Option? = when {
hidden -> null
else -> HelpFormatter.ParameterHelp.Option(
names,
secondaryNames,
metavar(context),
optionHelp(context),
nvalues,
helpTags,
acceptsNumberValueWithoutName,
acceptsUnattachedValue,
groupName = (this as? StaticallyGroupedOption)?.groupName
?: (this as? GroupableOption)?.parameterGroup?.groupName,
)
}
/**
* Called after this command's argv is parsed to transform and store the option's value.
*
* You cannot refer to other parameter values during this call, since they might not have been
* finalized yet.
*
* @param context The context for this parse
* @param invocations A possibly empty list of invocations of this option.
*/
fun finalize(context: Context, invocations: List<OptionInvocation>)
/**
* Called after all of a command's parameters have been [finalize]d to perform validation of the final value.
*/
fun postValidate(context: Context)
}
/** An option that functions as a property delegate */
interface OptionDelegate<T> :
GroupableOption,
ReadOnlyProperty<ParameterHolder, T>,
PropertyDelegateProvider<ParameterHolder, ReadOnlyProperty<ParameterHolder, T>> {
/**
* The value for this option.
*
* @throws IllegalStateException if this property is accessed before [finalize] is called.
*/
val value: T
/** Implementations must call [ParameterHolder.registerOption] */
override operator fun provideDelegate(
thisRef: ParameterHolder,
property: KProperty<*>,
): ReadOnlyProperty<ParameterHolder, T>
override fun getValue(thisRef: ParameterHolder, property: KProperty<*>): T = value
}
internal fun inferOptionNames(names: Set<String>, propertyName: String): Set<String> {
if (names.isNotEmpty()) {
val invalidName = names.find { !it.matches(Regex("""[\-@/+]{1,2}(?:[\w\-_]+|\?)""")) }
require(invalidName == null) { "Invalid option name \"$invalidName\"" }
return names
}
val normalizedName = "--" + propertyName.replace(Regex("""[a-z][A-Z]""")) {
"${it.value[0]}-${it.value[1]}"
}.lowercase()
return setOf(normalizedName)
}
internal fun inferEnvvar(names: Set<String>, envvar: String?, autoEnvvarPrefix: String?): String? {
if (envvar != null) return envvar
if (names.isEmpty() || autoEnvvarPrefix == null) return null
val name = splitOptionPrefix(names.maxBy { it.length }).second
if (name.isEmpty()) return null
return autoEnvvarPrefix + "_" + name.replace(Regex("\\W"), "_").uppercase()
}
/** Split an option token into a pair of prefix to simple name. */
internal fun splitOptionPrefix(name: String): Pair<String, String> =
when {
name.length < 2 || name[0] !in "-@/+" -> "" to name
name.length > 2 && name[0] == name[1] -> name.slice(0..1) to name.substring(2)
else -> name.substring(0, 1) to name.substring(1)
}
@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 {
return when {
// We don't look at envvars or the value source for eager options
eager || invocations.isNotEmpty() -> FinalValue.Parsed(invocations)
context.readEnvvarBeforeValueSource -> {
readEnvVar(context, envvar) ?: readValueSource(context)
}
else -> {
readValueSource(context) ?: readEnvVar(context, envvar)
}
} ?: FinalValue.Parsed(emptyList())
}
// This is a pretty ugly hack: option groups need to enforce their constraints, including on options
// from envvars/value sources, but not including default values. Unfortunately, we don't know
// whether an option's value is from a default or envvar. So we do some ugly casts and read the
// final value again to check for values from other sources.
internal fun Option.hasEnvvarOrSourcedValue(
context: Context,
invocations: List<OptionInvocation>,
): Boolean {
val envvar = (this as? OptionWithValues<*, *, *>)?.envvar
val final = this.getFinalValue(context, invocations, envvar)
return final !is FinalValue.Parsed
}
private fun Option.readValueSource(context: Context): FinalValue? {
return context.valueSource?.getValues(context, this)?.ifEmpty { null }
?.let { FinalValue.Sourced(it) }
}
private fun Option.readEnvVar(context: Context, envvar: String?): FinalValue? {
val env = inferEnvvar(names, envvar, context.autoEnvvarPrefix) ?: return null
return context.readEnvvar(env)?.let { FinalValue.Envvar(env, it) }
}