tomfeigin
Posted on August 7, 2023
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:
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)
}
}
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)
}
We now need to tie it all together in our main.go
file:
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:
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:
./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!
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
},
}
When we use the auto-completion of the mix
subcommand by pressing tab
twice, the terminal will show:
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.
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:
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.
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.
Posted on August 7, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.