go |cli |ux

Auto-Completion and Cocktail mixing with Golang’s Cobra CLI

tomfeigin

tomfeigin

Posted on August 7, 2023

Auto-Completion and Cocktail mixing with Golang’s Cobra CLI

At Raftt 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. We start by walking through the creation of a tiny 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

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 🐍

Cobra is a library for creating powerful modern CLI applications. 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, Github CLI, Helm, and many more.

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:

// 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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)
}
Enter fullscreen mode Exit fullscreen mode

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

package main
    
import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}
Enter fullscreen mode Exit fullscreen mode

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:

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,
    ...
}
Enter fullscreen mode Exit fullscreen mode

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:

./mixologist completion zsh > /tmp/completion
source /tmp/completion
Enter fullscreen mode Exit fullscreen mode

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

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 package for some help:

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
    },
}
Enter fullscreen mode Exit fullscreen mode

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

custom-auto-completing-igredients-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

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 ⛵

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, 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

If you are interested in how Raftt can help you be develop effectively on local or remote Kubernetes clusters check out our tutorials here.

💖 💪 🙅 🚩
tomfeigin
tomfeigin

Posted on August 7, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related