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

shell-completion.mddocs/

Shell Completion

Generate shell completion scripts for Bash and Fish, with support for dynamic completion candidates including files, hostnames, and custom completions.

Capabilities

Completion Candidates

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")
    }
}

Completion Generators

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()
}

Built-in Completion Options

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"
): Unit

Usage 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...")
    }
}

Dynamic Completion

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")
    }
}

Installation and Usage

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" >> ~/.bashrc

Fish 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 directory

Advanced 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...")
        }
    }
}

Debugging Completion

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

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