CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-github-ajalt--clikt-jvm

Multiplatform command line interface parsing for Kotlin

Pending
Overview
Eval results
Files

configuration-sources.mddocs/

Configuration Sources

Load parameter values from external sources including environment variables, configuration files, and custom value providers with priority chaining.

Capabilities

Value Source Interface

Base interface for loading parameter values from external sources.

/**
 * Interface for loading parameter values from external sources
 */
interface ValueSource {
    /**
     * Represents a parameter invocation with its values
     */
    data class Invocation(val values: List<String>)
    
    /**
     * Get parameter values from this source
     * @param context Current parsing context
     * @param option Option to get values for
     * @return List of invocations with values
     */
    fun getValues(context: Context, option: Option): List<Invocation>
}

Map Value Source

Load parameter values from a map (useful for testing and simple configuration).

/**
 * Value source that loads values from a map
 * @param map Map of parameter names to values
 * @param getKey Function to get map key from option names
 */
class MapValueSource(
    private val map: Map<String, String>,
    private val getKey: (Option) -> String = { option ->
        option.valueSourceKey ?: option.names.maxByOrNull { it.length } ?: ""
    }
) : ValueSource {
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation>
}

Usage Examples:

class MyCommand : CliktCommand() {
    private val host by option("--host", help = "Server host", valueSourceKey = "host")
    private val port by option("--port", help = "Server port", valueSourceKey = "port").int()
    private val debug by option("--debug", help = "Debug mode", valueSourceKey = "debug").flag()
    
    override fun run() {
        echo("Host: $host")
        echo("Port: $port")
        echo("Debug: $debug")
    }
}

// Using map value source
fun main() {
    val config = mapOf(
        "host" to "example.com",
        "port" to "8080",
        "debug" to "true"
    )
    
    MyCommand()
        .context {
            valueSource = MapValueSource(config)
        }
        .main()
}

// Custom key mapping
val customKeySource = MapValueSource(
    map = mapOf(
        "server_host" to "localhost",
        "server_port" to "3000"
    ),
    getKey = { option ->
        when (option.valueSourceKey) {
            "host" -> "server_host"
            "port" -> "server_port"
            else -> option.valueSourceKey ?: ""
        }
    }
)

Chained Value Source

Chain multiple value sources with priority ordering.

/**
 * Value source that chains multiple sources in priority order
 * Sources are checked in order, first match wins
 * @param sources List of value sources in priority order
 */
class ChainedValueSource(private val sources: List<ValueSource>) : ValueSource {
    constructor(vararg sources: ValueSource) : this(sources.toList())
    
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation>
}

Usage Examples:

class MyCommand : CliktCommand() {
    private val apiKey by option("--api-key", help = "API key", 
        valueSourceKey = "api_key", envvar = "API_KEY")
    private val timeout by option("--timeout", help = "Timeout seconds",
        valueSourceKey = "timeout").int().default(30)
    
    override fun run() {
        echo("API Key: ${apiKey?.take(8)}...")
        echo("Timeout: $timeout")
    }
}

fun main() {
    // Priority: command line > environment > config file > defaults
    val configFile = mapOf(
        "api_key" to "default-key-from-config",
        "timeout" to "60"
    )
    
    val environmentVars = mapOf(
        "API_KEY" to "env-api-key",
        "TIMEOUT" to "45"
    )
    
    val chainedSource = ChainedValueSource(
        MapValueSource(environmentVars), // Higher priority
        MapValueSource(configFile)       // Lower priority
    )
    
    MyCommand()
        .context {
            valueSource = chainedSource
        }
        .main()
}

// Complex chaining example
class DatabaseCommand : CliktCommand() {
    private val dbUrl by option("--db-url", valueSourceKey = "database.url", envvar = "DATABASE_URL")
    private val dbUser by option("--db-user", valueSourceKey = "database.user", envvar = "DATABASE_USER")
    private val dbPassword by option("--db-password", valueSourceKey = "database.password", envvar = "DATABASE_PASSWORD")
    
    override fun run() {
        echo("Connecting to database...")
        echo("URL: $dbUrl")
        echo("User: $dbUser")
    }
}

fun createConfiguredCommand(): DatabaseCommand {
    // Load from multiple sources
    val prodConfig = mapOf(
        "database.url" to "postgres://prod-server:5432/myapp",
        "database.user" to "prod_user"
    )
    
    val devConfig = mapOf(
        "database.url" to "postgres://localhost:5432/myapp_dev",
        "database.user" to "dev_user",
        "database.password" to "dev_password"
    )
    
    val secrets = mapOf(
        "database.password" to "super-secret-password"
    )
    
    val valueSource = ChainedValueSource(
        MapValueSource(secrets),    // Highest priority (secrets)
        MapValueSource(prodConfig), // Production overrides
        MapValueSource(devConfig)   // Development defaults
    )
    
    return DatabaseCommand().context {
        this.valueSource = valueSource
    }
}

Properties Value Source (JVM Platform)

Load parameter values from Java properties files (available on JVM platform only).

/**
 * Value source that loads values from Java properties files
 * Available on JVM platform only
 * @param properties Properties object
 * @param getKey Function to get property key from option
 */
class PropertiesValueSource(
    private val properties: Properties,
    private val getKey: (Option) -> String = { option ->
        option.valueSourceKey ?: option.names.maxByOrNull { it.length }?.removePrefix("--") ?: ""
    }
) : ValueSource {
    companion object {
        /**
         * Create from properties file
         * @param file Properties file to load
         */
        fun fromFile(file: File): PropertiesValueSource
        
        /**
         * Create from properties file path
         * @param path Path to properties file
         */
        fun fromFile(path: String): PropertiesValueSource
        
        /**
         * Create from input stream
         * @param inputStream Stream containing properties data
         */
        fun fromInputStream(inputStream: InputStream): PropertiesValueSource
    }
    
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation>
}

Usage Examples:

// app.properties file:
// server.host=localhost
// server.port=8080
// database.url=jdbc:postgresql://localhost:5432/myapp
// logging.level=INFO

class MyJvmCommand : CliktCommand() {
    private val host by option("--host", valueSourceKey = "server.host")
    private val port by option("--port", valueSourceKey = "server.port").int()
    private val dbUrl by option("--db-url", valueSourceKey = "database.url")
    private val logLevel by option("--log-level", valueSourceKey = "logging.level")
    
    override fun run() {
        echo("Server: $host:$port")
        echo("Database: $dbUrl")
        echo("Log level: $logLevel")
    }
}

fun main() {
    val propsSource = PropertiesValueSource.fromFile("app.properties")
    
    MyJvmCommand()
        .context {
            valueSource = propsSource
        }
        .main()
}

// Multiple properties files with chaining
fun createProductionCommand(): MyJvmCommand {
    val defaultProps = PropertiesValueSource.fromFile("defaults.properties")
    val envProps = PropertiesValueSource.fromFile("production.properties")
    val localProps = PropertiesValueSource.fromFile("local.properties")
    
    val chainedSource = ChainedValueSource(
        localProps,   // Highest priority (local overrides)
        envProps,     // Environment-specific config
        defaultProps  // Lowest priority (defaults)
    )
    
    return MyJvmCommand().context {
        valueSource = chainedSource
    }
}

// Properties with custom key mapping
class DatabaseCommand : CliktCommand() {
    private val host by option("--db-host", help = "Database host")
    private val port by option("--db-port", help = "Database port").int()
    private val name by option("--db-name", help = "Database name")
    
    override fun run() {
        echo("Connecting to $name at $host:$port")
    }
}

val dbPropsSource = PropertiesValueSource(
    Properties().apply {
        setProperty("db_host", "prod-db-server")
        setProperty("db_port", "5432")
        setProperty("db_name", "production")
    },
    getKey = { option ->
        when (option.names.first()) {
            "--db-host" -> "db_host"
            "--db-port" -> "db_port"
            "--db-name" -> "db_name"
            else -> ""
        }
    }
)

Custom Value Sources

Create custom value sources for specialized configuration needs.

/**
 * Custom value source implementation
 */
abstract class CustomValueSource : ValueSource {
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation>
}

Usage Examples:

// JSON configuration file value source
class JsonValueSource(private val jsonFile: File) : ValueSource {
    private val config: Map<String, Any> by lazy {
        // Parse JSON file (using your preferred JSON library)
        parseJsonFile(jsonFile)
    }
    
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation> {
        val key = option.valueSourceKey ?: return emptyList()
        val value = getNestedValue(config, key) ?: return emptyList()
        return listOf(ValueSource.Invocation(listOf(value.toString())))
    }
    
    private fun getNestedValue(map: Map<String, Any>, key: String): Any? {
        val parts = key.split(".")
        var current: Any? = map
        
        for (part in parts) {
            current = (current as? Map<*, *>)?.get(part)
            if (current == null) break
        }
        
        return current
    }
}

// YAML configuration value source
class YamlValueSource(private val yamlFile: File) : ValueSource {
    private val config: Map<String, Any> by lazy {
        parseYamlFile(yamlFile)
    }
    
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation> {
        val key = option.valueSourceKey ?: return emptyList()
        val value = getNestedValue(config, key)
        
        return when (value) {
            is List<*> -> listOf(ValueSource.Invocation(value.map { it.toString() }))
            null -> emptyList()
            else -> listOf(ValueSource.Invocation(listOf(value.toString())))
        }
    }
}

// Database configuration value source
class DatabaseConfigSource(private val connectionString: String) : ValueSource {
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation> {
        val key = option.valueSourceKey ?: return emptyList()
        
        // Query database for configuration value
        val value = queryDatabase(connectionString, key)
        return if (value != null) {
            listOf(ValueSource.Invocation(listOf(value)))
        } else {
            emptyList()
        }
    }
    
    private fun queryDatabase(connection: String, key: String): String? {
        // Implementation would connect to database and query config table
        // Return null if key not found
        return null
    }
}

// HTTP API configuration source
class HttpConfigSource(private val apiUrl: String, private val apiKey: String) : ValueSource {
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation> {
        val key = option.valueSourceKey ?: return emptyList()
        
        try {
            val value = fetchConfigValue(apiUrl, key, apiKey)
            return if (value != null) {
                listOf(ValueSource.Invocation(listOf(value)))
            } else {
                emptyList()
            }
        } catch (e: Exception) {
            // Log error and return empty result
            return emptyList()
        }
    }
    
    private fun fetchConfigValue(url: String, key: String, token: String): String? {
        // Implementation would make HTTP request to fetch config
        return null
    }
}

Environment Variable Integration

Combine value sources with environment variable support.

/**
 * Environment variable value source
 */
class EnvironmentValueSource : ValueSource {
    override fun getValues(context: Context, option: Option): List<ValueSource.Invocation>
}

Usage Examples:

class MyCommand : CliktCommand() {
    // Options with environment variable support
    private val apiKey by option("--api-key", 
        help = "API key", 
        envvar = "API_KEY",
        valueSourceKey = "api.key")
        
    private val dbUrl by option("--database-url",
        help = "Database URL",
        envvar = "DATABASE_URL", 
        valueSourceKey = "database.url")
    
    override fun run() {
        echo("API Key: ${apiKey?.take(8)}...")
        echo("Database URL: $dbUrl")
    }
}

// Priority chain: CLI args > env vars > config file > defaults
fun createConfiguredApp(): MyCommand {
    val configFileSource = JsonValueSource(File("config.json"))
    val envSource = EnvironmentValueSource()
    
    val chainedSource = ChainedValueSource(
        envSource,        // Environment variables (higher priority)
        configFileSource  // Config file (lower priority)
    )
    
    return MyCommand().context {
        valueSource = chainedSource
    }
}

// Complex configuration hierarchy
class WebServerCommand : CliktCommand() {
    private val port by option("--port", valueSourceKey = "server.port", envvar = "PORT").int()
    private val host by option("--host", valueSourceKey = "server.host", envvar = "HOST")
    private val ssl by option("--ssl", valueSourceKey = "server.ssl.enabled", envvar = "SSL_ENABLED").flag()
    private val certFile by option("--cert", valueSourceKey = "server.ssl.cert", envvar = "SSL_CERT")
    
    override fun run() {
        echo("Starting server on $host:$port")
        if (ssl) {
            echo("SSL enabled with cert: $certFile")
        }
    }
}

fun createWebServer(): WebServerCommand {
    // Multiple configuration sources
    val sources = ChainedValueSource(
        EnvironmentValueSource(),                           // Environment variables
        PropertiesValueSource.fromFile("production.properties"), // Production config
        PropertiesValueSource.fromFile("application.properties"), // Default config
        MapValueSource(mapOf(                               // Fallback defaults
            "server.port" to "8080",
            "server.host" to "0.0.0.0",
            "server.ssl.enabled" to "false"
        ))
    )
    
    return WebServerCommand().context {
        valueSource = sources
    }
}

Experimental API

/**
 * Annotation marking experimental value source APIs
 */
@RequiresOptIn("Value source API is experimental and may change")
annotation class ExperimentalValueSourceApi

Install with Tessl CLI

npx tessl i tessl/maven-com-github-ajalt--clikt-jvm

docs

arguments.md

configuration-sources.md

core-commands.md

exceptions.md

index.md

options.md

parameter-groups.md

parameter-types.md

shell-completion.md

testing-utilities.md

tile.json