Multiplatform command line interface parsing for Kotlin
—
Generate shell completion scripts for Bash and Fish, with support for dynamic completion candidates including files, hostnames, and custom completions.
Define what values should be suggested for parameters during shell completion.
/**
* Sealed class representing different types of completion candidates
*/
sealed class CompletionCandidates {
/** No completion candidates */
object None : CompletionCandidates()
/** File and directory path completion */
object Path : CompletionCandidates()
/** Hostname completion */
object Hostname : CompletionCandidates()
/** Username completion */
object Username : CompletionCandidates()
/** Fixed set of completion candidates */
data class Fixed(val candidates: Set<String>) : CompletionCandidates()
/** Custom completion generator */
data class Custom(val generator: (ShellType) -> String?) : CompletionCandidates()
}
/**
* Shell types supported for completion
*/
enum class ShellType { BASH, FISH, ZSH }Usage Examples:
class MyCommand : CliktCommand() {
// File path completion
private val configFile by option("--config", help = "Configuration file",
completionCandidates = CompletionCandidates.Path)
// Hostname completion
private val server by option("--server", help = "Server hostname",
completionCandidates = CompletionCandidates.Hostname)
// Fixed choices completion
private val format by option("--format", help = "Output format",
completionCandidates = CompletionCandidates.Fixed(setOf("json", "xml", "yaml")))
// Custom completion
private val service by option("--service", help = "Service name",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> "_custom_services_bash"
ShellType.FISH -> "__custom_services_fish"
ShellType.ZSH -> "_custom_services_zsh"
}
})
// Argument with path completion
private val inputFile by argument(name = "FILE", help = "Input file",
completionCandidates = CompletionCandidates.Path)
override fun run() {
echo("Config: $configFile")
echo("Server: $server")
echo("Format: $format")
echo("Service: $service")
echo("Input: $inputFile")
}
}Generate shell completion scripts for different shell types.
/**
* Base interface for completion script generators
*/
interface CompletionGenerator {
/**
* Generate completion script for the given command
* @param commandName Name of the command
* @param command Root command
* @return Generated completion script
*/
fun generateScript(commandName: String, command: CliktCommand): String
}
/**
* Bash completion script generator
*/
class BashCompletionGenerator : CompletionGenerator {
override fun generateScript(commandName: String, command: CliktCommand): String
}
/**
* Fish completion script generator
*/
class FishCompletionGenerator : CompletionGenerator {
override fun generateScript(commandName: String, command: CliktCommand): String
}Usage Examples:
class MyCommand : CliktCommand() {
private val generateCompletion by option("--generate-completion",
help = "Generate shell completion script")
.choice("bash", "fish")
override fun run() {
if (generateCompletion != null) {
val generator = when (generateCompletion) {
"bash" -> BashCompletionGenerator()
"fish" -> FishCompletionGenerator()
else -> error("Unsupported shell: $generateCompletion")
}
val script = generator.generateScript("mycommand", this)
echo(script)
return
}
// Normal command execution
echo("Running normal command...")
}
}
// Standalone completion script generation
fun main() {
val command = MyComplexCommand()
// Generate Bash completion
val bashScript = BashCompletionGenerator().generateScript("myapp", command)
File("completion/myapp-completion.bash").writeText(bashScript)
// Generate Fish completion
val fishScript = FishCompletionGenerator().generateScript("myapp", command)
File("completion/myapp.fish").writeText(fishScript)
// Run the command normally
command.main()
}Add completion generation options to your command.
/**
* Add completion generation option to command
* @param option Option names for completion generation
* @param help Help text for the option
*/
fun CliktCommand.completionOption(
vararg option: String = arrayOf("--generate-completion"),
help: String = "Generate shell completion script and exit"
): UnitUsage Examples:
class MyCommand : CliktCommand() {
init {
// Add standard completion option
completionOption()
}
// Your regular options and arguments
private val input by option("--input", help = "Input file",
completionCandidates = CompletionCandidates.Path)
private val verbose by option("-v", "--verbose", help = "Verbose output").flag()
override fun run() {
echo("Input: $input")
if (verbose) echo("Verbose mode enabled")
}
}
// Custom completion option names
class MyCommand2 : CliktCommand() {
init {
completionOption("--completion", "--comp", help = "Generate completion script")
}
override fun run() {
echo("Running command...")
}
}Create dynamic completion that depends on runtime context or previous arguments.
/**
* Custom completion function type
*/
typealias CompletionFunction = (context: CompletionContext) -> List<String>
/**
* Context provided to custom completion functions
*/
data class CompletionContext(
val command: CliktCommand,
val currentWord: String,
val previousWords: List<String>
)Usage Examples:
class DatabaseCommand : CliktCommand() {
// Database names depend on the connection
private val database by option("--database", help = "Database name",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> """
COMPREPLY=($(compgen -W "$(myapp list-databases 2>/dev/null || echo '')" -- "${"$"}{COMP_WORDS[COMP_CWORD]}"))
""".trimIndent()
ShellType.FISH -> """
myapp list-databases 2>/dev/null
""".trimIndent()
else -> null
}
})
// Table names depend on selected database
private val table by option("--table", help = "Table name",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> """
local db_option=""
for ((i=1; i<COMP_CWORD; i++)); do
if [[ ${"$"}{COMP_WORDS[i]} == "--database" ]]; then
db_option="--database ${"$"}{COMP_WORDS[i+1]}"
break
fi
done
COMPREPLY=($(compgen -W "$(myapp list-tables ${"$"}db_option 2>/dev/null || echo '')" -- "${"$"}{COMP_WORDS[COMP_CWORD]}"))
""".trimIndent()
ShellType.FISH -> """
set -l db_option ""
set -l prev_was_db_flag false
for word in (commandline -opc)[2..-1]
if test "$prev_was_db_flag" = true
set db_option "--database $word"
set prev_was_db_flag false
else if test "$word" = "--database"
set prev_was_db_flag true
end
end
myapp list-tables $db_option 2>/dev/null
""".trimIndent()
else -> null
}
})
override fun run() {
echo("Database: $database")
echo("Table: $table")
}
}
// Environment-aware completion
class DeployCommand : CliktCommand() {
private val environment by option("--env", help = "Environment",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> """
COMPREPLY=($(compgen -W "dev staging prod" -- "${"$"}{COMP_WORDS[COMP_CWORD]}"))
""".trimIndent()
ShellType.FISH -> "echo -e 'dev\nstaging\nprod'"
else -> null
}
})
private val service by option("--service", help = "Service name",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> """
local env=""
for ((i=1; i<COMP_CWORD; i++)); do
if [[ ${"$"}{COMP_WORDS[i]} == "--env" ]]; then
env=${"$"}{COMP_WORDS[i+1]}
break
fi
done
if [[ -n "${"$"}env" ]]; then
COMPREPLY=($(compgen -W "$(kubectl get services -n ${"$"}env --no-headers -o custom-columns=':metadata.name' 2>/dev/null || echo '')" -- "${"$"}{COMP_WORDS[COMP_CWORD]}"))
fi
""".trimIndent()
ShellType.FISH -> """
set -l env ""
set -l prev_was_env_flag false
for word in (commandline -opc)[2..-1]
if test "$prev_was_env_flag" = true
set env $word
set prev_was_env_flag false
else if test "$word" = "--env"
set prev_was_env_flag true
end
end
if test -n "$env"
kubectl get services -n $env --no-headers -o custom-columns=':metadata.name' 2>/dev/null
end
""".trimIndent()
else -> null
}
})
override fun run() {
echo("Deploying service $service to $environment")
}
}Instructions for installing and using completion scripts.
Bash Completion Installation:
# Generate completion script
mycommand --generate-completion bash > mycommand-completion.bash
# Install system-wide (Linux)
sudo cp mycommand-completion.bash /etc/bash_completion.d/
# Install user-specific
mkdir -p ~/.local/share/bash-completion/completions
cp mycommand-completion.bash ~/.local/share/bash-completion/completions/mycommand
# Or source directly in ~/.bashrc
echo "source /path/to/mycommand-completion.bash" >> ~/.bashrcFish Completion Installation:
# Generate completion script
mycommand --generate-completion fish > mycommand.fish
# Install user-specific
mkdir -p ~/.config/fish/completions
cp mycommand.fish ~/.config/fish/completions/
# Fish will automatically load completions from this directoryAdvanced Completion Patterns:
class GitLikeCommand : CliktCommand() {
// Subcommand completion
init {
subcommands(
CommitCommand(),
PushCommand(),
PullCommand(),
BranchCommand()
)
}
override fun run() = Unit
}
class CommitCommand : CliktCommand(name = "commit") {
private val files by argument(help = "Files to commit",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> """
COMPREPLY=($(compgen -W "$(git diff --name-only --cached 2>/dev/null || echo '')" -- "${"$"}{COMP_WORDS[COMP_CWORD]}"))
""".trimIndent()
ShellType.FISH -> "git diff --name-only --cached 2>/dev/null"
else -> null
}
}).multiple()
override fun run() {
echo("Committing files: ${files.joinToString(", ")}")
}
}
class BranchCommand : CliktCommand(name = "branch") {
private val branchName by argument(help = "Branch name",
completionCandidates = CompletionCandidates.Custom { shell ->
when (shell) {
ShellType.BASH -> """
COMPREPLY=($(compgen -W "$(git branch --format='%(refname:short)' 2>/dev/null || echo '')" -- "${"$"}{COMP_WORDS[COMP_CWORD]}"))
""".trimIndent()
ShellType.FISH -> "git branch --format='%(refname:short)' 2>/dev/null"
else -> null
}
}).optional()
override fun run() {
if (branchName != null) {
echo("Switching to branch: $branchName")
} else {
echo("Listing branches...")
}
}
}class MyCommand : CliktCommand() {
private val debugCompletion by option("--debug-completion",
help = "Debug completion generation").flag(default = false)
override fun run() {
if (debugCompletion) {
echo("Completion debugging enabled")
// Add debug logging for completion
}
}
}Install with Tessl CLI
npx tessl i tessl/maven-com-github-ajalt--clikt-jvm