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

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

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.mpp.isLetterOrDigit
import com.github.ajalt.clikt.mpp.readEnvvar
import com.github.ajalt.clikt.output.HelpFormatter
import com.github.ajalt.clikt.parsers.OptionParser
import com.github.ajalt.clikt.sources.ExperimentalValueSourceApi
import com.github.ajalt.clikt.sources.ValueSource
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. */
    val metavar: String?

    /** The description of this option, usually a single line. */
    val help: String

    /** The parser for this option's values. */
    val parser: OptionParser

    /** The names that can be used to invoke this option. They must start with a punctuation character. */
    val names: Set

    /** Names that can be used for a secondary purpose, like disabling flag options. */
    val secondaryNames: Set

    /** The number of values that must be given to this option. */
    val nvalues: Int

    /** 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

    /** Optional set of strings to use when the user invokes shell autocomplete on a value for this option. */
    val completionCandidates: CompletionCandidates get() = CompletionCandidates.None

    /** Information about this option for the help output. */
    val parameterHelp: HelpFormatter.ParameterHelp.Option?
        get() = when {
            hidden -> null
            else -> HelpFormatter.ParameterHelp.Option(names, secondaryNames, metavar, help, nvalues, helpTags,
                    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)

    /**
     * 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 : GroupableOption, ReadOnlyProperty {
    /**
     * The value for this option.
     *
     * An exception should be thrown if this property is accessed before [finalize] is called.
     */
    val value: T

    /** Implementations must call [ParameterHolder.registerOption] */
    operator fun provideDelegate(thisRef: ParameterHolder, prop: KProperty<*>): ReadOnlyProperty

    override fun getValue(thisRef: ParameterHolder, property: KProperty<*>): T = value
}

internal fun inferOptionNames(names: Set, propertyName: String): Set {
    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.split(Regex("(?<=[a-z])(?=[A-Z])"))
            .joinToString("-", prefix = "--") { it.toLowerCase() }
    return setOf(normalizedName)
}

internal fun inferEnvvar(names: Set, 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"), "_").toUpperCase()
}

/** Split an option token into a pair of prefix to simple name. */
internal fun splitOptionPrefix(name: String): Pair =
        when {
            name.length < 2 || isLetterOrDigit(name[0]) -> "" 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)
        }

internal fun  deprecationTransformer(
        message: String? = "",
        error: Boolean = false,
        transformAll: CallsTransformer
): CallsTransformer = {
    if (it.isNotEmpty()) {
        val msg = when (message) {
            null -> ""
            "" -> "${if (error) "ERROR" else "WARNING"}: option ${option.longestName()} is deprecated"
            else -> message
        }
        if (error) {
            throw CliktError(msg)
        } else if (message != null) {
            message(msg)
        }
    }
    transformAll(it)
}

internal fun Option.longestName(): String? = names.maxBy { it.length }

@OptIn(ExperimentalValueSourceApi::class)
internal sealed class FinalValue {
    data class Parsed(val values: List) : FinalValue()
    data class Sourced(val values: List) : FinalValue()
    data class Envvar(val key: String, val value: String) : FinalValue()
}

internal fun Option.getFinalValue(
        context: Context,
        invocations: List,
        envvar: String?
): FinalValue {
    return when {
        invocations.isNotEmpty() -> FinalValue.Parsed(invocations)
        context.readEnvvarBeforeValueSource -> {
            readEnvVar(context, envvar) ?: readValueSource(context)
        }
        else -> {
            readValueSource(context) ?: readEnvVar(context, envvar)
        }
    } ?: FinalValue.Parsed(emptyList())
}

@OptIn(ExperimentalValueSourceApi::class)
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 readEnvvar(env)?.let { FinalValue.Envvar(env, it) }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy