Auto-Completion and Cocktail mixing with Golang’s Cobra CLI
Tags: go, cli, ux
Published: 2023-08-07 12:47:15

At [Raftt](http://raftt.io) we build a dev tool which enables effortless development on Kubernetes envs. Development environments created with Raftt can be interacted with and controlled via a CLI program which we built with the Go programming language. Recently we added the ability to auto-complete the names of Kubernetes resources in the dev env. The auto-completion feature improved our UX and made many users happy that they can leverage the terminal to auto-complete resource names instead of having to type them out.

In this post we explain how to implement custom auto-completion ability for a CLI tool written in Go and using [Cobra](https://cobra.dev/). We start by walking through the creation of a tiny [mixologist](https://github.com/rafttio/mixologist) app using Cobra. The mixologist app is a CLI tool written in Go using the Cobra framework that can make cocktails from a list of ingredients and uses custom auto-completion to improve the user experience. For reference, the code for the CLI application can be found here: https://github.com/rafttio/mixologist

Here is an example of auto completion of the `mixologist` CLI:

![Auto-completion of the mixologist app](https://uploads-ssl.webflow.com/627bf36ecef36c976239c7b6/64bfc61cb0740c324e3c27e1_mixologist-example-gif.gif)

The post will then cover how to enable basic auto-completion of a specific subcommand, and finally, how to implement custom auto-completion. The post concludes with a brief discussion of how this feature is used in the Raftt CLI. **‍**

### Create a CLI utility with Cobra

This paragraph discusses how to enable auto-completion in a CLI tool written in Go using the Cobra framework. It walks through the process of creating a small application using Cobra, adding a subcommand, and implementing basic auto-completion for that subcommand. It then explains how to implement custom auto-completion in order to improve the user experience.

#### What is Cobra :snake:

> Cobra is a library for creating powerful modern CLI applications. [https://github.com/spf13/cobra](https://github.com/spf13/cobra)

You probably already know about Cobra if you ever wrote a CLI tool in Go, but for the few who don't, this article contains a short introduction on how to use it.

For CLI utilities written in Go, Cobra is the go-to command line wrapper, used by the likes of [Kubernetes](https://kubernetes.io/), [Github CLI](https://cli.github.com/), [Helm](https://helm.sh/), [and many more](https://github.com/spf13/cobra/blob/main/site/content/projects_using_cobra.md).

In the next section we will build a small application in Go using the `cobra` framework.

#### The mixologist app

In this introduction we will create a `mixologist` app. The app will act as a `mixologist` which can make a cocktail from a list of ingredients.

First create the root command for our app:
```golang
// cmd/root.govar
rootCmd = &cobra.Command{
    Use: "mixologist",
    Short: "Mixologist is your personal bartender.",
    Long: `Mixologist acts as a bartender who specializes in cocktail making.`,
    RunE: func(cmd *cobra.Command, args []string) error {
        return cmd.Help()
    },
}
func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}
```

The `rootCmd` is the entry-point of our `mixologist` app, it prints the help and exits. As mentioned before, we want our `mixologist` to mix cocktails, to do that we will add the sub command `mix`. The mix subcommand will receive at least 2 arguments, controlled by the `Args` field of the `cobra.Command`.

```golang
// cmd/mix.go
import (
    "github.com/spf13/cobra"
    "golang.org/x/exp/slices"
)
var mixCmd = &cobra.Command{
    Use:   "mix",
    Short: "make a cocktail.",
    Long:  `Mix some ingredients together to make a cocktail`,
    Args:  cobra.MinimumNArgs(2),
    RunE: func(cmd *cobra.Command, args []string) error {
        // Psuedo code
        if slices.IndexFunc(
            args, func(s string) bool { return s == "Vodka" }) > -1 &&
            slices.IndexFunc(
                args, func(s string) bool { return s == "Orange Juice" }) > -1 {
            fmt.Println("You can make a screwdriver cocktail!")
            return nil
        }
        return fmt.Errorf("i can't make a cocktail from %v", args)
    },
}
func init() {
    rootCmd.AddCommand(mixCmd)
}
```    

We now need to tie it all together in our `main.go` file:

```golang
package main
    ‍
import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}
```

By executing the `mixologist` with the correct ingredients, it can make us a Vodka Screwdriver! But if we get the ingredients wrong we will get an error.

In the next paragraph we will demonstrate how we can leverage `cobra` to have auto-completion in our terminal and make it easier for our users to enjoy our app.

#### Basic Auto-completion

Users of the `mixologist` app may find it difficult to guess the available ingredients. We should make it easier to use the app by making our CLI utility auto complete an ingredient if it is available in our bar.

To implement basic auto completion we will set the `ValidArgs` field of the command like so:

```golang
var availableIngredients = []string{
    "Vodka",
    "Gin",
    "Orange Juice",
    "Triple Sec",
    "Tequila",
    "simple syrup",
    "white rum",
    "Kahlua coffee liqueur",
}

var mixCmd = &cobra.Command{
    Use:   "mix",
    Short: "make a cocktail.",
    ValidArgs: availableIngredients,
    ...
}
```
    
Now we need to install the auto-completion in our shell, cobra can generate auto completion for `bash`, `zsh` and `fish` shells out of the box. After choosing the shell we can do:

```bash
./mixologist completion zsh > /tmp/completion
source /tmp/completion
```

And then when we type `mixologist [tab][tab]` we will get the list of ingredients auto completed!

![Auto-completion of ingredients for the mix subcommand](https://uploads-ssl.webflow.com/627bf36ecef36c976239c7b6/64bfc8f7772b2c24b04534b2_mixologist-basic-autocomplete.gif)

That is really cool but it still isn't optimal, the command can auto-complete ingredients which when mixed together doesn't make any cocktail. We want to auto-complete the next ingredient only if it can actually combine with all previously mentioned ingredients. To achieve that optimal user experience we need to implement custom auto-completion, we will see how to do that in the next paragraph.

### Custom Auto completion

Now our `mixologist` app auto-completes the ingredient list in when pressing tab on the `mix` subcommand, but we want to make it smarter.

Our opinionated bartender believes that the only thing that can be paired with Vodka is Orange Juice, so we want our auto-completion to suggest only Orange Juice if the previously mentioned ingredient was Vodka. We also don't want to repeat the same ingredient twice so we won't end up mixing Kahlua with Kahlua 4 times.

To achieve that we need to edit the `mix` subcommand and use the awesome [`lo`](https://github.com/samber/lo) package for some help:

```golang
import "github.com/samber/lo"
var availableIngredients = []string{...}
var mixCmd = &cobra.Command{
    Use:   "mix",
    Short: "make a cocktail.",
    ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
        if len(args) == 1 && args[0] == "Vodka" {
            return []string{"Orange Juice"}, cobra.ShellCompDirectiveNoFileComp
        }
        _, unusedIngredients := lo.Difference(args, availableIngredients)
        return unusedIngredients, cobra.ShellCompDirectiveNoFileComp
    },
}
```    

When we use the auto-completion of the `mix` subcommand by pressing `tab` twice, the terminal will show:

![custom-auto-completing-igredients-gif](https://uploads-ssl.webflow.com/627bf36ecef36c976239c7b6/64bfc9fefacc755c5e72e0e8_mixologist-custom.gif)

Notice that selected ingredients do not appear as an option for subsequent auto-completions anymore.

If we give `Vokda` as the first argument and then press `tab` twice, the terminal will auto complete `Orange Juice` immediately.

![custom-auto-comleting-ingredients-gif-2](https://uploads-ssl.webflow.com/627bf36ecef36c976239c7b6/64bfca25fa988372a146d730_mixologist-custom-2.gif)

The new auto-completion improved the UX of our `mixologist` app and made our users much happier 🙂.

Now that we know how to implement custom auto-completion using the `cobra` framework, we will discuss in short how we use this feature in the `raftt` CLI.

### Auto completion in raftt :sailboat:

In the Raftt CLI, we have implemented custom auto-completion using the Cobra framework. This feature allows users to easily and quickly input valid Kubernetes resource names when interacting with resources in the Raftt dev environment. With custom auto-completion, users can select only valid resource names, improving the overall user experience and preventing frustrating typos.

If you are following along our tutorial [here](https://docs.raftt.io/basics/tutorials/connect_mode), you can try out our auto-complete for yourself when you convert the `frontend` or `recommendations` services to dev mode. Write `raftt dev [tab][tab]` and see it list the options 🙂. The tightly integrated auto completion provided us with blazing fast and effortless UX without having to worry about typos.

The step where we installed the completion by running the `source` command above is performed for you if you have installed Raftt using `brew` or `snap`. Otherwise, add `eval $(raftt completion <SHELL>)` to your shell's `rc` file.

#### Performance

Lastly we should briefly touch the efficiency of the auto-completion feature, if the auto-completion logic is complicated or depends on remote services, consecutive completions will be slow and result in a bad user experience. For example when auto-completing a long list of Kubernetes resources by repeatedly pressing the `[tab][tab]` combination. To mitigate this issue, we can cache the results of the complicated auto-completion logic.

Caching auto-complete results introduces two new problems:

1.  Where are the results stored? The CLI itself only runs for a second to generate the completion, and does not stick around. We could potentially put them on disk, but that means dealing with multiple accessors at once, locking, etc. Raftt has a daemon that it starts up, and it was natural to cache the results there.
    
2.  Out of date information. If our cache is too long, we might be serving incorrect information. For example, if the Kubernetes resources have since been deleted. We settled on 3 seconds as a reasonable duration.
    

In conclusion, implementing custom auto-completion for a CLI tool using Cobra can greatly improve the user experience and make the tool more efficient to use. By following the steps outlined in this post, you can add this feature to your own Go-based CLI tool. With auto-completion, users can more easily navigate and interact with your tool, reducing errors and improving the overall experience. Thank you for auto-completing this post 🙂

The code for the mixologist app can be found here: [https://github.com/rafttio/mixologist](https://github.com/rafttio/mixologist)

If you are interested in how Raftt can help you be develop effectively on local or remote Kubernetes clusters check out our tutorials [here](https://docs.raftt.io/basics/tutorials).