35.6 Generating Shell Completions with cobra
Right, so you’ve built a CLI tool. It’s a beautiful, gleaming piece of engineering. It has flags, subcommands, and it prints helpful messages. But you know what separates the tools you use from the tools you tolerate? A complete lack of friction. Every time a user has to hit TAB and nothing happens, a little part of their soul leaves their body. We’re not here to murder souls. We’re here to build muscle memory. That’s where shell completions come in, and cobra makes generating them almost criminally easy.
Think of it this way: you, the author, have already defined the entire structure of your command in code—what commands exist, what flags they take, and what those flags mean. Shell completion is just the process of exporting that structure into a format your shell (bash, zsh, fish, powershell) can understand. cobra handles this translation for you, so you don’t have to manually write and maintain incredibly finicky shell scripts. It’s one of those features that makes you look like a CLI wizard for very little actual work.
The One-Liner to Generate Them All
The absolute simplest way to get started is to just add a command to your root command. This is the “good enough for government work” approach, and it’s often all you need. You just hook up the completion generation command to your root cobra.Command.
var rootCmd = &cobra.Command{
Use: "myapp",
Short: "My brilliant application",
// ... all your other setup
}
func init() {
// This is the magic line. It adds the 'completion' subcommand.
rootCmd.AddCommand(completionCommand)
}
// completionCommand is defined by cobra for us.
var completionCommand = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
Long: `Output the shell completion code for the specified shell.
Once generated, you can source it or add it to your shell's completion dir.`,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true) // 'true' enables descriptions
case "powershell":
cmd.Root().GenPowerShellCompletion(os.Stdout)
}
},
}
Now your users can run myapp completion bash and it will spit out a beautiful, complex bash script. They can then source this script directly or, more permanently, save it to a file and install it to their system’s completion directory (e.g., /usr/share/bash-completion/completions/ on many Linux distros). The same goes for all the other shells.
The Slightly More Elegant Installation Method
Telling your users to “just run this and pipe it to some obscure file” is functional, but we can do better. cobra provides a hidden, extra-smart method for some shells that handles the installation itself. Let’s upgrade our completion command to use this method for bash and zsh, which is what the cobra CLI tool itself uses.
// Replacing the Run function in the completionCommand from above:
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
// GenBashCompletionV2 is the newer version, and 'true' means include descriptions.
// The 'false' at the end tells it NOT to output the "help" text about installing.
// We're handling that ourselves in the Long description.
cmd.Root().GenBashCompletionV2(os.Stdout, true)
case "zsh":
// This one does the installation logic for the user. If the COMP_LINE and COMP_POINT
// environment variables are set (meaning the completion is being sourced by the shell),
// it runs the completion logic. Otherwise, it outputs the script. It's very clever.
err := cmd.Root().GenZshCompletion(os.Stdout)
if err != nil {
log.Fatal(err)
}
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
cmd.Root().GenPowerShellCompletion(os.Stdout)
}
},
The zsh one is particularly neat because the generated script is smart enough to act as both the installer and the runtime completer. It’s a two-in-one deal.
Integrating Directly with Your Root Command
Sometimes you want completion to be available immediately when someone sources your script, without them having to run a subcommand first. This is where you hook into the cobra.Command’s built-in completion mechanics. You do this by setting the ValidArgsFunction or Args fields on your commands. This is how you get dynamic completion, like suggesting available git branches for a checkout command.
Here’s a practical example. Let’s say you have a command connect that takes a hostname as an argument, and you have a function that can fetch a list of available hosts.
var connectCmd = &cobra.Command{
Use: "connect [host]",
Short: "Connect to a host",
Args: cobra.ExactArgs(1),
// This is the key field. It points to a function that returns completions.
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// You'd call your real function here. Let's fake it for the example.
hosts := getAvailableHosts() // []string{"db01", "web02", "cache03"}
// Use the 'toComplete' string to filter the list for the shell.
// The ShellCompDirective tells cobra (and the shell) how to behave.
// NoFileComp tells the shell not to fall back to filename completion.
return hosts, cobra.ShellCompDirectiveNoFileComp
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Connecting to %s...\n", args[0])
},
}
func getAvailableHosts() []string {
// In reality, you'd fetch this from an API, a config file, etc.
return []string{"db01", "web02", "cache03"}
}
Now, when a user types myapp connect db<TAB>, your getAvailableHosts() function is called, and it returns ["db01", "web02", "cache03"]. Cobra filters this to just "db01" (since it starts with ‘db’) and hands that perfect suggestion back to the shell. Magic.
The Rough Edges and Pitfalls
It’s not all rainbows and butterflies. Here’s what to watch for:
- Performance of
ValidArgsFunction: That function gets called on every tab press. IfgetAvailableHosts()is a slow database query or HTTP request, your completion will feel horribly laggy. Cache the results aggressively or make sure the source is lightning fast. - The Hidden
--Pitfall: This is a shell convention, but it bites everyone. The double dash--signifies “end of flags.” So if you’re completing an argument that starts with a dash, the shell might get confused. Cobra handles this correctly, but it’s good to know why sometimes a flag name won’t complete as an argument. - Installation Paths Vary Wildly: The correct path to save the completion script (
/usr/share/bash-completion/completions/myapp) is different on macOS (often/usr/local/etc/bash_completion.d/), and it depends on whether the user hasbash-completioninstalled. Your documentation should provide the common options but be clear that the user might need to adjust for their OS. Thecompletioncommand outputs the script; it’s the user’s job to put it where their shell expects it. - Sourcing vs. Installing: Remember,
source <(myapp completion bash)is a temporary test. It only lasts for the current shell session. To make it permanent, the script must be saved to a file. Don’t let your users think the sourcing method is the final solution.
The bottom line? Adding shell completion is a disproportionately high reward for the effort required. It’s a hallmark of a polished, professional-grade tool. With cobra, you have no excuse not to include it. Go make your users’ tab key the most used key on their keyboard.