4 minute read

You probably use a CLI on a daily basis. Depending on the solution the CLI works, the number of available options can be overwhelming. Because no visual recognition is available, we depend on the help to find the correct arguments. An appealing extension to CLIs is to provide users with shell completion. This simplifies the life of the user by displaying the expected arguments of the command without interruptions. This post explains how to get a shell completion CLI in Go using the Cobra library.

The Cobra library is a solution that well deserves your time if you are developing using Go. This post assumes you are familiar with this library and its main concepts (Commands, Flags, etc.). Shell completion is not provided by the Go program itself. Actually, this is done with a shell script generated by Cobra that captures users interaction with our CLI.

You can find the source code for these examples and more in the Build Systems with Go repo in the 15_cli folder.

The following CLI is a Cobra naive example that uses commands to print a message (hello or bye) in the console.

package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
)

var RootCmd = &cobra.Command{
	Use: "say",
	Long: "Root command",
}

var HelloCmd = &cobra.Command{
	Use: "hello",
	Short: "Say hello",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Hello!!!")
	},
}

var ByeCmd = &cobra.Command{
	Use: "bye",
	Short: "Say goodbye",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Bye!!!")
	},
}

func init() {
	RootCmd.AddCommand(HelloCmd,ByeCmd)
}

func main() {
	if err := RootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Create a shell completion script

Cobra generates shell completion scripts for Bash, Zsh, Fish and PowerShell. The Command type offers methods with a signature like GetXXXCompletions to generate the corresponding completion script for the shell XXX. Normally, a completion command is added to the CLI to permit users to generate the corresponding completion script for their shells. When the script is loaded into the shell, pressing the tab key twice displays the valid commands and the help. The example below shows a possible implementation of a completion command using the root command (check the documentation for additional details).

package cmd

import (
    "github.com/spf13/cobra"
    "os"
)

var CompletionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Generate completion script",
    Long: "To load completions",
    DisableFlagsInUseLine: true,
    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)
        case "powershell":
            cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        }
    },
}

If we add this command to our previous code using

RootCmd.AddCommand(HelloCmd,ByeCmd,CompletionCommand)

we can generate and load the shell completion script for Bash as follows.

>>> ./say completion bash > /tmp/completion
>>> source /tmp/completion

Now we have that pressing the tab key twice displays the following completions:

>>> ./say [tab][tab]
bye         -- Say goodbye
completion  -- Generate completion script
hello       -- Say hello
help        -- Help about any command

Command arguments can be displayed for additional help. A list of valid arguments can be provided with the ValidArgs field of the Command type. Our completion command has already filled this field showing the following list.

>> ./say completion [tab][tab]
bash        fish        powershell  zsh

Get command arguments at runtime

In some scenarios, the arguments of a command can only be determined at runtime. For example, assume we have an application that queries the information of a certain user in a database using her identifier. The user id is only a valid argument if it exists in the database. For these scenarios, the list of valid arguments can be defined using a function in the field ValidArgsFunction. The following example emulates the availability of different users with a random selector in the UserGet function. The ShellCompDirective is a binary flag used to modify the shell behaviour. Check the documentation for more information about this flag and what it does.

package main

import (
	"fmt"
	"github.com/juanmanuel-tirado/savetheworldwithgo/15_cli/cobra/advanced/example_05/cmd"
	"github.com/spf13/cobra"
	"os"
	"math/rand"
	"time"
)

var RootCmd = &cobra.Command{
	Use: "db",
	Long: "Root command",
}

var GetCmd = &cobra.Command{
	Use: "get",
	Short: "Get user data",
	Args: cobra.ExactValidArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Printf("Get user %s!!!\n",args[0])
	},
	ValidArgsFunction: UserGet,
}

func UserGet (cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
	rand.Seed(time.Now().UnixNano())
	if rand.Int() % 2 == 0 {
		return []string{"John", "Mary"}, cobra.ShellCompDirectiveNoFileComp
	}
	return []string{"Ernest", "Rick", "Mary"}, cobra.ShellCompDirectiveNoFileComp
}

func init() {
	RootCmd.AddCommand(GetCmd, cmd.CompletionCmd)
}

func main() {
	if err := RootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

After generating and loading the shell completion script, the completion dynamically suggests user ids with the UserGet function as shown below.

>>> ./db get [tab][tab]
John  Mary
>>> ./db [tab][tab]
completion  -- Generate completion script
get         -- Get user data
help        -- Help about any command
>>> ./db get [tab][tab]
Ernest  Mary    Rick

As you can see generating a shell completion script is pretty straight forward using Cobra. I hope you consider adding shell completion in your next project.

Thanks for reading.