
commonMain.com.github.ajalt.clikt.parameters.arguments.Argument.kt Maven / Gradle / Ivy
package com.github.ajalt.clikt.parameters.arguments
import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.core.BadParameterValue
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.Context
import com.github.ajalt.clikt.core.UsageError
import com.github.ajalt.clikt.output.HelpFormatter.ParameterHelp
import com.github.ajalt.clikt.parameters.arguments.defaultAllProcessor
import com.github.ajalt.clikt.parameters.arguments.defaultValidator
import com.github.ajalt.clikt.parameters.internal.NullableLateinit
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.int
import kotlin.js.JsName
import kotlin.jvm.JvmName
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* A positional parameter to a command.
*
* Arguments can take any number of values.
*/
interface Argument {
/** The metavar for this argument. */
val name: String
/**
* The number of values that this argument takes.
*
* Negative [nvalues] indicates a variable number of values. Cannot be 0.
*/
val nvalues: Int
/** If true, an error will be thrown if this argument is not given on the command line. */
val required: Boolean
/**
* The description of this argument.
*
* It's usually better to leave this null and describe options in the usage line of the command instead.
*/
val help: String
/** Extra information about this argument to pass to the help formatter. */
val helpTags: Map
/** Optional set of strings to use when the user invokes shell autocomplete on a value for this argument. */
val completionCandidates: CompletionCandidates get() = CompletionCandidates.None
/** Information about this argument for the help output. */
val parameterHelp: ParameterHelp.Argument?
/**
* Called after this command's argv is parsed to transform and store the argument'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 values A possibly empty list of values provided to this argument.
*/
fun finalize(context: Context, values: List)
/**
* Called after all of a command's parameters have been [finalize]d to perform validation of the final value.
*/
fun postValidate(context: Context)
}
/** An argument that functions as a property delegate */
interface ArgumentDelegate : Argument, ReadOnlyProperty {
/** Implementations must call [CliktCommand.registerArgument] */
operator fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>): ReadOnlyProperty
}
/**
* A receiver for argument transformers.
*
* @property argument The argument that was invoked
*/
class ArgumentTransformContext(val argument: Argument, val context: Context) : Argument by argument {
/** Throw an exception indicating that usage was incorrect. */
fun fail(message: String): Nothing = throw BadParameterValue(message, argument)
/** Issue a message that can be shown to the user */
fun message(message: String) = context.command.issueMessage(message)
/** If [value] is false, call [fail] with the output of [lazyMessage] */
inline fun require(value: Boolean, lazyMessage: () -> String = { "invalid value" }) {
if (!value) fail(lazyMessage())
}
}
/** A callback that transforms a single value from a string to the value type */
typealias ArgValueTransformer = ArgValueConverter
/** A callback that transforms a single value from one type to another */
typealias ArgValueConverter = ArgumentTransformContext.(InT) -> ValueT
/** A callback that transforms all the values into the final argument type */
typealias ArgCallsTransformer = ArgumentTransformContext.(List) -> AllT
/** A callback validates the final argument type */
typealias ArgValidator = ArgumentTransformContext.(AllT) -> Unit
/**
* An [Argument] delegate implementation that transforms its values .
*
* @property transformValue Called in [finalize] to transform each value provided to the argument.
* @property transformAll Called in [finalize] to transform the list of values to the final type.
* @property transformValidator Called after all parameters have been [finalize]d to validate the result of [transformAll]
*/
class ProcessedArgument(
name: String,
override val nvalues: Int,
override val required: Boolean,
override val help: String,
override val helpTags: Map,
val completionCandidatesWithDefault: ValueWithDefault,
val transformValue: ArgValueTransformer,
val transformAll: ArgCallsTransformer,
val transformValidator: ArgValidator
) : ArgumentDelegate {
init {
require(nvalues != 0) { "Arguments cannot have nvalues == 0" }
}
override var name: String = name
private set
internal var value: AllT by NullableLateinit("Cannot read from argument delegate before parsing command line")
private set
override val completionCandidates: CompletionCandidates
get() = completionCandidatesWithDefault.value
override val parameterHelp
get() = ParameterHelp.Argument(name, help, required && nvalues == 1 || nvalues > 1, nvalues < 0, helpTags)
override fun getValue(thisRef: CliktCommand, property: KProperty<*>): AllT = value
override operator fun provideDelegate(thisRef: CliktCommand, prop: KProperty<*>):
ReadOnlyProperty {
if (name.isBlank()) name = prop.name.toUpperCase().replace("-", "_")
thisRef.registerArgument(this)
return this
}
override fun finalize(context: Context, values: List) {
val ctx = ArgumentTransformContext(this, context)
value = transformAll(ctx, values.map { transformValue(ctx, it) })
}
override fun postValidate(context: Context) {
transformValidator(ArgumentTransformContext(this, context), value)
}
/** Create a new argument that is a copy of this one with different transforms. */
fun copy(
transformValue: ArgValueTransformer,
transformAll: ArgCallsTransformer,
validator: ArgValidator,
name: String = this.name,
nvalues: Int = this.nvalues,
required: Boolean = this.required,
help: String = this.help,
helpTags: Map = this.helpTags,
completionCandidatesWithDefault: ValueWithDefault = this.completionCandidatesWithDefault
): ProcessedArgument {
return ProcessedArgument(name, nvalues, required, help, helpTags, completionCandidatesWithDefault, transformValue, transformAll, validator)
}
/** Create a new argument that is a copy of this one with the same transforms. */
fun copy(
validator: ArgValidator = this.transformValidator,
name: String = this.name,
nvalues: Int = this.nvalues,
required: Boolean = this.required,
help: String = this.help,
helpTags: Map = this.helpTags,
completionCandidatesWithDefault: ValueWithDefault = this.completionCandidatesWithDefault
): ProcessedArgument {
return ProcessedArgument(name, nvalues, required, help, helpTags, completionCandidatesWithDefault, transformValue, transformAll, validator)
}
}
internal typealias RawArgument = ProcessedArgument
@PublishedApi
internal fun defaultAllProcessor(): ArgCallsTransformer = { it.single() }
@PublishedApi
internal fun defaultValidator(): ArgValidator = {}
/**
* Create a property delegate argument.
*
* The order that these delegates are created is the order that arguments must appear. By default, the
* argument takes one value and throws an error if no value is given. The behavior can be changed with
* functions like [int] and [optional].
*
* @param name The metavar for this argument. If not given, the name is inferred form the property name.
* @param help The description of this argument for help output.
* @param helpTags Extra information about this option to pass to the help formatter
*/
@Suppress("unused")
fun CliktCommand.argument(
name: String = "",
help: String = "",
helpTags: Map = emptyMap(),
completionCandidates: CompletionCandidates? = null
): RawArgument {
return ProcessedArgument(
name = name,
nvalues = 1,
required = true,
help = help,
helpTags = helpTags,
completionCandidatesWithDefault = ValueWithDefault(completionCandidates, CompletionCandidates.None),
transformValue = { it },
transformAll = defaultAllProcessor(),
transformValidator = defaultValidator()
)
}
/**
* Transform all values to the final argument type.
*
* The input is a list of values, one for each value on the command line. The values in the
* list are the output of calls to [convert]. The input list will have a size of [nvalues] if
* [nvalues] is > 0.
*
* Used to implement functions like [pair] and [multiple].
*
* ## Example
*
* ```
* val entries by argument().transformAll { it.joinToString() }
* ```
*
* @param nvalues The number of values required by this argument. A negative [nvalues] indicates a
* variable number of values.
* @param required If true, an error with be thrown if no values are provided to this argument.
*/
fun ProcessedArgument.transformAll(
nvalues: Int? = null,
required: Boolean? = null,
transform: ArgCallsTransformer): ProcessedArgument {
return copy(transformValue, transform, defaultValidator(),
nvalues = nvalues ?: this.nvalues,
required = required ?: this.required)
}
/**
* Return null instead of throwing an error if no value is given.
*
* This must be called after all other transforms.
*
* ### Example:
*
* ```
* val arg: Int? by argument().int().optional()
* ```
*/
fun ProcessedArgument.optional(): ProcessedArgument {
return transformAll(required = false) { if (it.isEmpty()) null else transformAll(it) }
}
/**
* Accept any number of values to this argument.
*
* Only one argument in a command may use this function, and the command may not have subcommands. This must
* be called after all other transforms.
*
* ### Example:
*
* ```
* val arg: List by argument().int().multiple()
* ```
*/
fun ProcessedArgument.multiple(required: Boolean = false): ProcessedArgument, T> {
return transformAll(nvalues = -1, required = required) { it }
}
/**
* Only store unique values for this argument
*
* ### Example:
*
* ```
* val arg: Set by argument().int().multiple().unique()
* ```
*/
fun ProcessedArgument, T>.unique(): ProcessedArgument, T> {
return transformAll(nvalues = -1) { it.toSet() }
}
/**
* Require exactly two values to this argument, and store them in a [Pair].
*
* This must be called after converting the value type, and before other transforms.
*
* ### Example:
*
* ```
* val arg: Pair by argument().int().pair()
* ```
*/
fun ProcessedArgument.pair(): ProcessedArgument, T> {
return transformAll(nvalues = 2) { it[0] to it[1] }
}
/**
* Require exactly three values to this argument, and store them in a [Triple]
*
* This must be called after converting the value type, and before other transforms.
*
* ### Example:
*
* ```
* val arg: Triple by argument().int().triple()
* ```
*/
fun ProcessedArgument.triple(): ProcessedArgument, T> {
return transformAll(nvalues = 3) { Triple(it[0], it[1], it[2]) }
}
/**
* If the argument is not given, use [value] instead of throwing an error.
*
* This must be applied after all other transforms.
*
* ### Example:
*
* ```
* val arg: Pair by argument().int().pair().default(1 to 2)
* ```
*/
fun ProcessedArgument.default(value: T): ArgumentDelegate {
return transformAll(required = false) { it.firstOrNull() ?: value }
}
/**
* If the argument is not given, call [value] and use its return value instead of throwing an error.
*
* This must be applied after all other transforms. If the argument is given on the command line, [value] will
* not be called.
*
* ### Example:
*
* ```
* val arg: Pair by argument().int().pair().defaultLazy { expensiveOperation() }
* ```
*/
inline fun ProcessedArgument.defaultLazy(crossinline value: () -> T): ArgumentDelegate {
return transformAll(required = false) { it.firstOrNull() ?: value() }
}
/**
* Convert the argument's values.
*
* The [conversion] is called once for each value given. If any errors are thrown, they are caught and a
* [BadParameterValue] is thrown with the error message. You can call `fail` to throw a [BadParameterValue]
* manually.
*
* You can call `convert` more than once to wrap the result of the previous `convert`, but it cannot
* be called after [transformAll] (e.g. [multiple]) or [transformValues] (e.g. [pair]).
*
* ## Example
*
* ```
* val bd: BigDecimal by argument().convert { it.toBigDecimal() }
* val fileText: ByteArray by argument().file().convert { it.readBytes() }
* ```
*
* @param completionCandidates candidates to use when completing this argument in shell autocomplete,
* if no candidates are specified in [argument]
*/
inline fun ProcessedArgument.convert(
completionCandidates: CompletionCandidates = completionCandidatesWithDefault.default,
crossinline conversion: ArgValueConverter
): ProcessedArgument {
val conv: ArgValueTransformer = {
try {
conversion(transformValue(it))
} catch (err: UsageError) {
err.argument = argument
throw err
} catch (err: Exception) {
fail(err.message ?: "")
}
}
return copy(conv, defaultAllProcessor(), defaultValidator(),
completionCandidatesWithDefault = completionCandidatesWithDefault.copy(default = completionCandidates)
)
}
@Deprecated(
"Cannot wrap an argument that isn't converted",
replaceWith = ReplaceWith("this.convert(wrapper)"),
level = DeprecationLevel.ERROR
)
@JvmName("rawWrapValue")
@JsName("rawWrapValue")
@Suppress("UNUSED_PARAMETER")
fun RawArgument.wrapValue(wrapper: (String) -> Any): RawArgument = this
/**
* Wrap the argument's values after a conversion is applied.
*
* This can only be called on an argument after [convert] or a conversion function like [int].
*
* If you just want to perform checks on the value without converting it to another type, use
* [validate] instead.
*/
@Deprecated("Use `convert` instead", ReplaceWith("this.convert(wrapper)"))
inline fun ProcessedArgument.wrapValue(
crossinline wrapper: (T1) -> T2
): ProcessedArgument {
val conv: ArgValueTransformer = {
try {
wrapper(transformValue(it))
} catch (err: UsageError) {
err.argument = argument
throw err
} catch (err: Exception) {
fail(err.message ?: "")
}
}
return copy(conv, defaultAllProcessor(), defaultValidator())
}
/**
* Check the final argument value and raise an error if it's not valid.
*
* The [validator] is called with the final argument type (the output of [transformAll]), and should call
* `fail` if the value is not valid.
*
* You can also call `require` to fail automatically if an expression is false.
*
* ### Example:
*
* ```
* val opt by argument().int().validate { require(it % 2 == 0) { "value must be even" } }
* ```
*/
fun ProcessedArgument.validate(validator: ArgValidator)
: ArgumentDelegate {
return copy(validator)
}
/**
* Check the final argument value and raise an error if it's not valid.
*
* The [validator] is called with the final argument type (the output of [transformAll]), and should call
* `fail` if the value is not valid. It is not called if the delegate value is null.
*
* You can also call `require` to fail automatically if an expression is false.
*
* ### Example:
*
* ```
* val opt by argument().int().validate { require(it % 2 == 0) { "value must be even" } }
* ```
*/
@JvmName("nullableValidate")
@JsName("nullableValidate")
fun ProcessedArgument.validate(validator: ArgValidator)
: ArgumentDelegate {
return copy({ if (it != null) validator(it) })
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy