Multiplatform command line interface parsing for Kotlin
—
Load parameter values from external sources including environment variables, configuration files, and custom value providers with priority chaining.
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>
}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 ?: ""
}
}
)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
}
}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 -> ""
}
}
)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
}
}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
}
}/**
* Annotation marking experimental value source APIs
*/
@RequiresOptIn("Value source API is experimental and may change")
annotation class ExperimentalValueSourceApiInstall with Tessl CLI
npx tessl i tessl/maven-com-github-ajalt--clikt-jvm