All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.github.ajalt.clikt.parameters.options.OptionWithValues.kt Maven / Gradle / Ivy

@file:JvmMultifileClass
@file:JvmName("OptionWithValuesKt")

package com.github.ajalt.clikt.parameters.options

import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parameters.groups.ParameterGroup
import com.github.ajalt.clikt.parameters.groups.mutuallyExclusiveOptions
import com.github.ajalt.clikt.parameters.internal.NullableLateinit
import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parsers.OptionParser.Invocation
import com.github.ajalt.clikt.parsers.OptionWithValuesParser
import com.github.ajalt.clikt.sources.ExperimentalValueSourceApi
import kotlin.js.JsName
import kotlin.jvm.JvmMultifileClass
import kotlin.jvm.JvmName
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

/**
 * A receiver for options transformers.
 *
 * @property name The name that was used to invoke this option.
 * @property option The option that was invoked
 */
class OptionCallTransformContext(
        val name: String,
        val option: Option,
        val context: Context
) : Option by option {
    /** Throw an exception indicating that an invalid value was provided. */
    fun fail(message: String): Nothing = throw BadParameterValue(message, name)

    /** 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 receiver for options transformers.
 *
 * @property option The option that was invoked
 */
class OptionTransformContext(val option: Option, val context: Context) : Option by option {
    /** Throw an exception indicating that usage was incorrect. */
    fun fail(message: String): Nothing = throw UsageError(message, option)

    /** 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 ValueTransformer = ValueConverter

/** A block that converts a single value from one type to another */
typealias ValueConverter = OptionCallTransformContext.(InT) -> ValueT

/**
 * A callback that transforms all the values for a call to the call type.
 *
 * The input list will always have a size equal to `nvalues`
 */
typealias ArgsTransformer = OptionCallTransformContext.(List) -> EachT

/**
 * A callback that transforms all of the calls to the final option type.
 *
 * The input list will have a size equal to the number of times the option appears on the command line.
 */
typealias CallsTransformer = OptionTransformContext.(List) -> AllT

/** A callback validates the final option type */
typealias OptionValidator = OptionTransformContext.(AllT) -> Unit

/**
 * An [Option] that takes one or more values.
 *
 * @property metavarWithDefault The metavar to use. Specified at option creation.
 * @property envvar The environment variable name to use.
 * @property envvarSplit The pattern to split envvar values on. If the envvar splits into multiple values,
 *   each one will be treated like a separate invocation of the option.
 * @property valueSplit The pattern to split values from the command line on. By default, values are
 *   split on whitespace.
 * @property transformValue Called in [finalize] to transform each value provided to each invocation.
 * @property transformEach Called in [finalize] to transform each invocation.
 * @property transformAll Called in [finalize] to transform all invocations into the final value.
 * @property transformValidator Called after all parameters have been [finalized][finalize] to validate the output of [transformAll]
 */
// `AllT` is deliberately not an out parameter. If it was, it would allow undesirable combinations such as
// default("").int()
@OptIn(ExperimentalValueSourceApi::class)
class OptionWithValues(
        names: Set,
        val metavarWithDefault: ValueWithDefault,
        override val nvalues: Int,
        override val help: String,
        override val hidden: Boolean,
        override val helpTags: Map,
        val envvar: String?,
        val envvarSplit: ValueWithDefault,
        val valueSplit: Regex?,
        override val parser: OptionWithValuesParser,
        val completionCandidatesWithDefault: ValueWithDefault,
        val transformValue: ValueTransformer,
        val transformEach: ArgsTransformer,
        val transformAll: CallsTransformer,
        val transformValidator: OptionValidator
) : OptionDelegate, GroupableOption {
    override var parameterGroup: ParameterGroup? = null
    override var groupName: String? = null
    override val metavar: String? get() = metavarWithDefault.value
    override var value: AllT by NullableLateinit("Cannot read from option delegate before parsing command line")
        private set
    override val secondaryNames: Set get() = emptySet()
    override var names: Set = names
        private set
    override val completionCandidates: CompletionCandidates
        get() = completionCandidatesWithDefault.value

    override fun finalize(context: Context, invocations: List) {
        val inv = when (val v = getFinalValue(context, invocations, envvar)) {
            is FinalValue.Parsed -> {
                when (valueSplit) {
                    null -> invocations
                    else -> invocations.map { inv -> inv.copy(values = inv.values.flatMap { it.split(valueSplit) }) }
                }
            }
            is FinalValue.Sourced -> {
                if (v.values.any { it.values.size != nvalues }) throw IncorrectOptionValueCount(this, longestName()!!)
                v.values.map { Invocation("", it.values) }
            }
            is FinalValue.Envvar -> {
                v.value.split(envvarSplit.value).map { Invocation(v.key, listOf(it)) }
            }
        }

        value = transformAll(OptionTransformContext(this, context), inv.map {
            val tc = OptionCallTransformContext(it.name, this, context)
            transformEach(tc, it.values.map { v -> transformValue(tc, v) })
        })
    }

    override operator fun provideDelegate(thisRef: ParameterHolder, prop: KProperty<*>): ReadOnlyProperty {
        require(secondaryNames.isEmpty()) {
            "Secondary option names are only allowed on flag options."
        }
        names = inferOptionNames(names, prop.name)
        thisRef.registerOption(this)
        return this
    }

    override fun postValidate(context: Context) {
        transformValidator(OptionTransformContext(this, context), value)
    }

    /** Create a new option that is a copy of this one with different transforms. */
    fun  copy(
            transformValue: ValueTransformer,
            transformEach: ArgsTransformer,
            transformAll: CallsTransformer,
            validator: OptionValidator,
            names: Set = this.names,
            metavarWithDefault: ValueWithDefault = this.metavarWithDefault,
            nvalues: Int = this.nvalues,
            help: String = this.help,
            hidden: Boolean = this.hidden,
            helpTags: Map = this.helpTags,
            envvar: String? = this.envvar,
            envvarSplit: ValueWithDefault = this.envvarSplit,
            valueSplit: Regex? = this.valueSplit,
            parser: OptionWithValuesParser = this.parser,
            completionCandidatesWithDefault: ValueWithDefault = this.completionCandidatesWithDefault
    ): OptionWithValues {
        return OptionWithValues(names, metavarWithDefault, nvalues, help, hidden,
                helpTags, envvar, envvarSplit, valueSplit, parser, completionCandidatesWithDefault,
                transformValue, transformEach, transformAll, validator)
    }

    /** Create a new option that is a copy of this one with the same transforms. */
    fun copy(
            validator: OptionValidator = this.transformValidator,
            names: Set = this.names,
            metavarWithDefault: ValueWithDefault = this.metavarWithDefault,
            nvalues: Int = this.nvalues,
            help: String = this.help,
            hidden: Boolean = this.hidden,
            helpTags: Map = this.helpTags,
            envvar: String? = this.envvar,
            envvarSplit: ValueWithDefault = this.envvarSplit,
            valueSplit: Regex? = this.valueSplit,
            parser: OptionWithValuesParser = this.parser,
            completionCandidatesWithDefault: ValueWithDefault = this.completionCandidatesWithDefault
    ): OptionWithValues {
        return OptionWithValues(names, metavarWithDefault, nvalues, help, hidden,
                helpTags, envvar, envvarSplit, valueSplit, parser, completionCandidatesWithDefault,
                transformValue, transformEach, transformAll, validator)
    }
}

typealias NullableOption = OptionWithValues
typealias RawOption = NullableOption

@PublishedApi
internal fun  defaultEachProcessor(): ArgsTransformer = { it.single() }

@PublishedApi
internal fun  defaultAllProcessor(): CallsTransformer = { it.lastOrNull() }

@PublishedApi
internal fun  defaultValidator(): OptionValidator = { }

/**
 * Create a property delegate option.
 *
 * By default, the property will return null if the option does not appear on the command line. If the option
 * is invoked multiple times, the value from the last invocation will be used The option can be modified with
 * functions like [int], [pair], and [multiple].
 *
 * @param names The names that can be used to invoke this option. They must start with a punctuation character.
 *   If not given, a name is inferred from the property name.
 * @param help The description of this option, usually a single line.
 * @param metavar A name representing the values for this option that can be displayed to the user.
 *   Automatically inferred from the type.
 * @param hidden Hide this option from help outputs.
 * @param envvar The environment variable that will be used for the value if one is not given on the command
 *   line.
 * @param envvarSplit The pattern to split the value of the [envvar] on. Defaults to whitespace,
 *   although some conversions like `file` change the default.
 * @param helpTags Extra information about this option to pass to the help formatter
 */
@Suppress("unused")
fun ParameterHolder.option(
        vararg names: String,
        help: String = "",
        metavar: String? = null,
        hidden: Boolean = false,
        envvar: String? = null,
        envvarSplit: Regex? = null,
        helpTags: Map = emptyMap(),
        completionCandidates: CompletionCandidates? = null
): RawOption = OptionWithValues(
        names = names.toSet(),
        metavarWithDefault = ValueWithDefault(metavar, "TEXT"),
        nvalues = 1,
        help = help,
        hidden = hidden,
        helpTags = helpTags,
        envvar = envvar,
        envvarSplit = ValueWithDefault(envvarSplit, Regex("\\s+")),
        valueSplit = null,
        parser = OptionWithValuesParser,
        completionCandidatesWithDefault = ValueWithDefault(completionCandidates, CompletionCandidates.None),
        transformValue = { it },
        transformEach = defaultEachProcessor(),
        transformAll = defaultAllProcessor(),
        transformValidator = defaultValidator()
)

/**
 * Check the final option value and raise an error if it's not valid.
 *
 * The [validator] is called with the final option 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 option().int().validate { require(it % 2 == 0) { "value must be even" } }
 * ```
 */
fun  OptionWithValues.validate(
        validator: OptionValidator
): OptionDelegate {
    return copy(transformValue, transformEach, transformAll, validator)
}

/**
 * Check the final option value and raise an error if it's not valid.
 *
 * The [validator] is called with the final option 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, or `warn` to show
 * the user a warning message without aborting.
 *
 * ### Example:
 *
 * ```
 * val opt by option().int().validate { require(it % 2 == 0) { "value must be even" } }
 * ```
 */
@JvmName("nullableValidate")
@JsName("nullableValidate")
fun  OptionWithValues.validate(
        validator: OptionValidator
): OptionDelegate {
    return copy(transformValue, transformEach, transformAll, { if (it != null) validator(it) })
}

/**
 * Mark this option as deprecated in the help output.
 *
 * By default, a tag is added to the help message and a warning is printed if the option is used.
 *
 * This should be called after any conversion and validation.
 *
 * ### Example:
 *
 * ```
 * val opt by option().int().validate { require(it % 2 == 0) { "value must be even" } }
 *    .deprecated("WARNING: --opt is deprecated, use --new-opt instead")
 * ```
 *
 * @param message The message to show in the warning or error. If null, no warning is issued.
 * @param tagName The tag to add to the help message
 * @param tagValue An extra message to add to the tag
 * @param error If true, when the option is invoked, a [CliktError] is raised immediately instead of issuing a warning.
 */
fun  OptionWithValues.deprecated(
        message: String? = "",
        tagName: String? = "deprecated",
        tagValue: String = "",
        error: Boolean = false
): OptionDelegate {
    val helpTags = if (tagName.isNullOrBlank()) helpTags else helpTags + mapOf(tagName to tagValue)
    return copy(transformValue, transformEach, deprecationTransformer(message, error, transformAll), transformValidator, helpTags = helpTags)
}

@Deprecated(
        "Cannot wrap an option that isn't converted",
        replaceWith = ReplaceWith("this.convert(wrapper)"),
        level = DeprecationLevel.ERROR
)
@JvmName("rawWrapValue")
@JsName("rawWrapValue")
@Suppress("UNUSED_PARAMETER")
fun RawOption.wrapValue(wrapper: (String) -> Any): RawOption = this

/**
 * Wrap the option's values after a conversion is applied.
 *
 * This can be useful if you want to use different option types wrapped in a sealed class for
 * [mutuallyExclusiveOptions].
 *
 * This can only be called on an option 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.
 *
 * ## Example
 *
 * ```
 * sealed class GroupTypes {
 *   data class FileType(val file: File) : GroupTypes()
 *   data class StringType(val string: String) : GroupTypes()
 * }
 *
 * val group by mutuallyExclusiveOptions(
 *   option("-f").file().wrapValue(::FileType),
 *   option("-s").convert { StringType(it) }
 * )
 * ```
 */
@Deprecated("Use `convert` instead", ReplaceWith("this.convert(wrapper)"))
inline fun  NullableOption.wrapValue(
        crossinline wrapper: (T1) -> T2
): NullableOption = convert { wrapper(it) }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy