How to Build Great CLI’s in Golang, One Snake at a Time

Steve Domino
6 min readMay 27, 2016

--

Image by d3ya

During my time at Nanobox, I’ve had the wonderful opportunity to do tons of development in Go, which has rapidly become my favorite language to develop in.

At one point I was tasked with building our CLI tool and found two really great projects, Cobra and Viper, which make building CLI’s easy.

Individually they are both great projects, but with their powers combined they are… no, not Captain Planet… but just as awesome!

Snakes. Why’d it have to be snakes?

That quote actually has nothing to do with the projects, as their names are a G.I. Joe reference… I just like Indiana Jones.

Before I get into some code I want to give a quick history of how I ended up finding and using these projects.

Cobra

Cobra is both a library for creating powerful modern CLI applications as well as a program to generate applications and command files.

Before Cobra, I was searching for a good CLI framework to use (hoping I wouldn’t have to write one myself) and options were limited. I found two that showed the most promise here, and here. However, after playing around with them for a bit, I didn’t really like what the implementation looked like for either. Neither of them “felt” quite like what I wanted from a CLI framework.

As I set out to write my own framework, I ended up following a pattern that I saw in Heroku’s CLI that I thought was pretty simple. It worked for awhile but in the end I needed more.

Just as I was trying to figure out how I was going to implement some upcoming requirements to the CLI, I stumbled upon this great presentation by Steve Francia in which he mentions Cobra. I checked it out and was hooked immediately; I have been using Cobra now for probably 8 months and it has yet to disappoint.

Viper

Go configuration with fangs! Viper is a complete configuration solution for go applications including 12 factor apps. It is designed to work within an application, and can handle all types of configuration needs and formats.

Fast forward a few months after I started using Cobra in a whole bunch of Nanopack projects, I found Viper. At first I wasn’t immediately enchanted with it like I was with Cobra, and didn’t really think to start using it right away. Plus, we already had a similar pattern for handling config with a separate package that we would import everywhere.

It wasn’t until one of my co-workers also found Viper, and said he was going to try it in place of our config package pattern, that I noticed it integrated with Cobra. At that point I decided to give it a try and have been very pleased.

A little snake goes a long way

There is so much great stuff that you can do with these two projects, but in the interest of time, I’ll show you a simple integration to hopefully get you started.

The CLI example used here will come from a micro-service called hoarder.

To start, I want to show off how the CLI is structured for hoarder (I’m not implying that this is the best way to do it, this is just the way that we’ve decided works for us).

We create a hoarder/commands package which represents the CLI for this project. Inside of that package there is a commands.go which is essentially the main.go for this package.

hoarder/commands/commands.go

The commands.go file is the meat of the CLI, all other files in the package represent actual commands for the CLI.

There is a lot going on here which I’ll go over. Also, I’ve tried to remove any actual code that isn’t important to the actual Cobra/Viper implementation for this article, but you can still find all the source code on github.

HoarderCmd

HoarderCmd is the primary command for our CLI. It has a PersistentPreRun method that will always get called for itself, and any sub-commands, before executing the Run method. For this reason, we’ve decided to parse an optional config file in this portion of the command to ensure we get any changes before attempting to do anything else.

Next, the command will Run. We made an interesting choice here to allow hoarder to run as either a server or a CLI. To run it as a server, the Run method looks for a flag that indicates it will be running as a server and acts accordingly, otherwise it will try and pick up on the desired command and attempt to execute if. If no command is recognized, it will failover to outputting the default help output.

init()

First we attach all the flags that this command will accept, setting default values, and a description.

Next we tell Viper which flags tie into which config options, so that when those get changed Viper will update those values accordingly, when flags are provided, or if a config file is parsed.

A quick note here: any flags that are attached as PersistentFlag’s will get carried over into any sub-commands of a command. For the scope of this article I wont go into all the details of the types of flags we’re using and why, because both Cobra and Viper have pretty good documentation; however if I get a lot of requests, I’ll write another article going into more detail.

Finally we attach sub-commands. Notice that he we have grouped commands and hidden/aliased commands, I’ll go over that in a later section.

hoarder/main.go

Now that we have our primary command setup, all we need to do is fire up Cobra. We do that by simply executing the command.

hoarder/commands/show.go

Next I want to quickly go over a few points of one of the sub-commands to HoarderCmd (again trying to have removed any actual project logic).

Hidden/Alias commands

First I want to point out that this command actually defines three commands, two of which are flagged as hidden. The reason for this is simply to provide some additional options when attempting to run this command, however in the help output only the showCmd will display with it’s usage and descriptions.

init()

Again, in this init() we define flags, however these flags will be local to only this command.

show()

If you notice when the showCmd is defined, this is the method that is Run when the command is executed. I’ve removed all actual logic, but left in one thing I wanted to point out.

When executing, the command checks to make sure it has any required values, otherwise it will indicated as such and stop execution.

Sub-commands

Something that I allude to but never really go over is sub-commands (because this project is pretty simple).

Making a sub-command is simple; the same way I’ve created the HoarderCmd and attached commands to it, is the same thing you can do with any other command.

So any command can both be and/or have sub-commands attached to it, and if those commands are “primary sub-commands” you would probably then include another package that represents all of the sub-commands for that sub-command.

Conclusion

I’ve only scratched the surface here of what you can do with Cobra and Viper. This implementation is very small and simple. If you look around some of our source code you’ll find much more extensive examples of how we’ve put these two projects to good use.

I promise you that both of these projects will make your life easier when writing your next CLI. Individually they are very powerful, very flexible and very good at what they do. But together they will help you show your next CLI who is boss (p.s. It’s you)!

--

--

Steve Domino

Husband, father, and tech enthusiast. I’m passionate about finding, using, and creating awesome stuff. Currently creating awesome stuff at https://getdivvy.com/