Multiplatform command line interface parsing for Kotlin
—
Organize related parameters into groups with mutual exclusion, co-occurrence, and choice-based selection patterns.
Group related options together for better help organization and validation.
/**
* Basic option group for organizing related parameters
* @param name Group name for help display
* @param help Help text for the group
*/
open class OptionGroup(val name: String? = null, val help: String? = null) {
/** Access to parent command */
protected val command: CliktCommand get() = TODO()
/** Create option in this group */
fun option(
vararg names: String,
help: String = "",
metavar: String? = null,
hidden: Boolean = false,
helpTags: Map<String, String> = emptyMap(),
envvar: String? = null,
valueSourceKey: String? = null,
completionCandidates: CompletionCandidates? = null
): RawOption
}Usage Examples:
class DatabaseOptions : OptionGroup(name = "Database Options", help = "Database connection settings") {
val host by option("--db-host", help = "Database host").default("localhost")
val port by option("--db-port", help = "Database port").int().default(5432)
val username by option("--db-user", help = "Database username").required()
val password by option("--db-password", help = "Database password").required()
}
class LoggingOptions : OptionGroup(name = "Logging Options") {
val level by option("--log-level", help = "Logging level")
.choice("debug", "info", "warn", "error").default("info")
val file by option("--log-file", help = "Log file path")
}
class MyCommand : CliktCommand() {
private val database by DatabaseOptions()
private val logging by LoggingOptions()
override fun run() {
echo("Connecting to ${database.host}:${database.port}")
echo("Log level: ${logging.level}")
}
}Create groups where only one option can be specified at a time.
/**
* Create mutually exclusive option group
* @param options Options that are mutually exclusive
*/
fun ParameterHolder.mutuallyExclusiveOptions(
vararg options: OptionDelegate<*>
): MutuallyExclusiveOptions
/**
* Mutually exclusive option group
*/
class MutuallyExclusiveOptions : ParameterGroup {
val option: Option?
}Usage Examples:
class MyCommand : CliktCommand() {
// Individual options
private val verbose by option("-v", "--verbose", help = "Verbose output").flag()
private val quiet by option("-q", "--quiet", help = "Quiet output").flag()
private val debug by option("-d", "--debug", help = "Debug output").flag()
// Make them mutually exclusive
private val outputMode by mutuallyExclusiveOptions(verbose, quiet, debug)
override fun run() {
when {
verbose -> echo("Verbose mode enabled")
quiet -> echo("Quiet mode enabled")
debug -> echo("Debug mode enabled")
else -> echo("Default output mode")
}
}
}
// Alternative approach with enum
class MyCommand2 : CliktCommand() {
enum class OutputMode { VERBOSE, QUIET, DEBUG }
private val verbose by option("-v", "--verbose", help = "Verbose output")
.flag().convert { OutputMode.VERBOSE }
private val quiet by option("-q", "--quiet", help = "Quiet output")
.flag().convert { OutputMode.QUIET }
private val debug by option("-d", "--debug", help = "Debug output")
.flag().convert { OutputMode.DEBUG }
private val outputMode by mutuallyExclusiveOptions(verbose, quiet, debug)
}Create groups where all options must be specified together.
/**
* Mark option group as co-occurring (all options required together)
*/
fun <T : OptionGroup> T.cooccurring(): ParameterGroupDelegate<T?>Usage Examples:
class AuthOptions : OptionGroup(name = "Authentication") {
val username by option("--username", help = "Username").required()
val password by option("--password", help = "Password").required()
}
class TlsOptions : OptionGroup(name = "TLS Configuration") {
val certFile by option("--cert", help = "Certificate file").required()
val keyFile by option("--key", help = "Private key file").required()
val caFile by option("--ca", help = "CA certificate file")
}
class MyCommand : CliktCommand() {
// Either provide both username and password, or neither
private val auth by AuthOptions().cooccurring()
// Either provide cert and key (and optionally CA), or none
private val tls by TlsOptions().cooccurring()
override fun run() {
auth?.let { authOptions ->
echo("Authenticating as ${authOptions.username}")
} ?: echo("No authentication")
tls?.let { tlsOptions ->
echo("Using TLS with cert: ${tlsOptions.certFile}")
} ?: echo("No TLS")
}
}Create groups where the user selects one of several predefined option sets.
/**
* Create choice group with predefined option sets
* @param choices Map of choice names to values
*/
fun <T : Any> CliktCommand.groupChoice(
vararg choices: Pair<String, T>
): ParameterGroupDelegate<T?>Usage Examples:
// Define different deployment configurations
sealed class DeploymentConfig {
data class Development(val debugPort: Int = 8000) : DeploymentConfig()
data class Staging(val replicas: Int = 2) : DeploymentConfig()
data class Production(val replicas: Int = 5, val healthCheck: Boolean = true) : DeploymentConfig()
}
class MyCommand : CliktCommand() {
private val deployment by groupChoice(
"--dev" to DeploymentConfig.Development(),
"--staging" to DeploymentConfig.Staging(),
"--prod" to DeploymentConfig.Production()
)
override fun run() {
when (val config = deployment) {
is DeploymentConfig.Development -> {
echo("Development deployment with debug port ${config.debugPort}")
}
is DeploymentConfig.Staging -> {
echo("Staging deployment with ${config.replicas} replicas")
}
is DeploymentConfig.Production -> {
echo("Production deployment with ${config.replicas} replicas, health check: ${config.healthCheck}")
}
null -> {
echo("No deployment configuration specified")
}
}
}
}
// More complex choice groups with actual option groups
class DatabaseGroup : OptionGroup() {
val host by option("--db-host").default("localhost")
val port by option("--db-port").int().default(5432)
}
class FileGroup : OptionGroup() {
val path by option("--file-path").required()
val format by option("--file-format").choice("json", "xml").default("json")
}
class MyCommand2 : CliktCommand() {
private val source by groupChoice(
"--database" to DatabaseGroup(),
"--file" to FileGroup()
)
override fun run() {
when (val config = source) {
is DatabaseGroup -> {
echo("Using database at ${config.host}:${config.port}")
}
is FileGroup -> {
echo("Using file ${config.path} with format ${config.format}")
}
null -> {
echo("No data source specified")
}
}
}
}Combine multiple group types for complex parameter relationships.
/**
* Parameter group interface
*/
interface ParameterGroup {
val groupName: String?
val groupHelp: String?
}
/**
* Parameter group delegate interface
*/
interface ParameterGroupDelegate<out T> {
operator fun getValue(thisRef: ParameterHolder, property: KProperty<*>): T
}Usage Examples:
// Complex nested group structure
class ServerConfig : OptionGroup(name = "Server Configuration") {
val host by option("--host").default("0.0.0.0")
val port by option("--port").int().default(8080)
}
class DatabaseConfig : OptionGroup(name = "Database Configuration") {
val url by option("--db-url").required()
val poolSize by option("--db-pool-size").int().default(10)
}
class CacheConfig : OptionGroup(name = "Cache Configuration") {
val enabled by option("--cache-enabled").flag()
val ttl by option("--cache-ttl").int().default(3600)
}
class MyCommand : CliktCommand() {
// Required server config
private val server by ServerConfig()
// Either database or cache can be optional, but not both
private val database by DatabaseConfig().cooccurring()
private val cache by CacheConfig().cooccurring()
override fun run() {
require(database != null || cache != null) {
"Either database or cache configuration must be provided"
}
echo("Server: ${server.host}:${server.port}")
database?.let { db ->
echo("Database: ${db.url} (pool size: ${db.poolSize})")
}
cache?.let { c ->
if (c.enabled) {
echo("Cache enabled with TTL: ${c.ttl}s")
} else {
echo("Cache disabled")
}
}
}
}
// Validation across groups
class MyAdvancedCommand : CliktCommand() {
private val inputFile by option("--input").file()
private val outputFile by option("--output").file()
class ProcessingOptions : OptionGroup(name = "Processing") {
val threads by option("--threads").int().default(1)
val batchSize by option("--batch-size").int().default(100)
}
private val processing by ProcessingOptions()
override fun run() {
// Cross-group validation
if (inputFile == outputFile) {
echo("Warning: Input and output files are the same", err = true)
}
if (processing.threads > Runtime.getRuntime().availableProcessors()) {
echo("Warning: Thread count exceeds available processors", err = true)
}
echo("Processing with ${processing.threads} threads, batch size ${processing.batchSize}")
}
}/**
* Exception thrown when mutually exclusive options are used together
*/
class MutuallyExclusiveGroupException(val names: List<String>) : UsageError()
/**
* Custom group validation
*/
abstract class ParameterGroup {
/** Validate group after all parameters are parsed */
protected open fun validate() {}
}Usage Examples:
class CustomValidationGroup : OptionGroup(name = "Custom Validation") {
val minValue by option("--min").int()
val maxValue by option("--max").int()
// Custom validation logic
override fun validate() {
val min = minValue
val max = maxValue
if (min != null && max != null && min > max) {
throw UsageError("Minimum value ($min) cannot be greater than maximum value ($max)")
}
}
}
class MyCommand : CliktCommand() {
private val range by CustomValidationGroup()
override fun run() {
echo("Range: ${range.minValue} to ${range.maxValue}")
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-github-ajalt--clikt-jvm