Build Command Line Interfaces (CLI) using Go

A quick introduction on how to develop Command Line Interfaces (CLI) using Go

Bijesh O S
The Startup
10 min readMay 6, 2020

--

Photo by Yingchih on Unsplash

Introduction

When we think of designing ways to interact with users, Graphical User Interface (GUI) is the de-facto choice. In cases where our area of focus is cloud/infrastructure tools and users are programmers themselves, Command Line Interface (CLI) becomes a preferred choice. Though CLIs are text based, its simplicity and automation abilities make them a popular option among power-users. Ability to chain with other command line tools improves overall usability as well.

Most of the popular cloud platforms, such as AWS, Azure and Google Cloud, provide CLIs to interact with their underlying services. Some of the well known cloud-native softwares such as Kubernetes, Docker, Hugo, Istio, and Helm, provide easy to use CLIs as well.

Designing Command Line Interface is often an under appreciated process. Ever wondered how we can create intuitive and easy to use CLIs for our tools? No worries 😃. In this article, let’s explore how to develop such a CLI using Go.

Why use Go?

A lot has been written about the pros and cons about Go. Let’s pick top 3 advantages relevant to our context.

  • Output of the build process is a single self contained binary; in other words, no separate dependencies/runtimes/libraries are needed to execute the binary. This is a huge gain since dependency and runtime management is a headache in most cases.
  • We can build binaries for multiple Operating Systems (Mac, Linux or Windows) and architectures (amd64, arm, x386 etc ) from a single machine. This simplifies the build process significantly.
  • Time needed to compile a Go application is quite less compared to its peers. That’s a productivity boost when we have a relatively large code base.

Libraries

Though we can start from scratch, it is always wise to take a look around and see if any frameworks/libraries are available to get us off the ground quickly. Though there are a number of libraries available in the Go ecosystem to help in this scenario, two popular libraries stand out: Cobra and Viper.

Cobra

Created by Steve Francia (Go Product Lead at Google), Cobra is a library to build modern CLI applications. It can also be used as a program to generate CLI applications with necessary scaffolding and command files.

CLIs for many of the widely popular projects are built using Cobra. Some of the examples include: Kubernetes, GitHub CLI, Moby (formerly Docker), Docker (distribution), etcd, Istio, Helm, Hugo, OpenShift and CockroachDB. The list goes on.

To know more about Cobra, click here.

Viper

Sharing the same creator as that of Cobra, Viper is a configuration solution for Go applications. It is designed to work within an application and can handle many types of configuration needs and formats. Some of the use-cases covered include setting defaults, reading from JSON/YAML file, reading environment variables and command line flags.

To know more about Viper, click here.

What are we building?

For the purpose of this demonstration, let’s imagine that we have created a deployment automation tool for a 3-tier application, which has Web, API and Database artifacts to deploy. We need to create a CLI to act as an entry point for our automation actions such as deploy, undeploy and status check. In this article, we’ll see the steps to create such a CLI application, with placeholders left for actual integration.

Now, on to the naming ceremony. What do we call the tool? Hmm.. let’s call the tool deployer (quite an innovative name, isn’t it? Just kidding 😃 ).

Next step is to design the command structure. As wise folks say, a well designed command structure should read like a sentence so that it is easy to remember. Two of the popular CLIs which follow this philosophy are Kubernetes CLI (kubectl) and Docker CLI.

Some examples are as follows:

$ kubectl create -f my-config.yaml
$ kubectl get pod example-pod1
$ kubectl delete -f my-config.yaml
$ docker start
$ docker build /path

(Check out Kubernetes’ CLI (kubectl) here or Docker’s CLI here).

We’ll take cues from these commands and try to build something similar. Here is how our command structure would look like:

  • A top-level/root command, called deployer
  • Four sub-commands to deployer : deploy, undeploy, status and version
  • Three sub-commands to deploy, undeploy and status: web, api and database
  • A global flag which is valid for all commands and sub commands: config

If we take the following Kubernetes example,

$ kubectl create -f my-config.yaml

kubectl is the top-level/root command, create is the sub-command, -f is the flag and my-config.yaml is the value of the flag.

Diagrammatically, our command structure would looks like the following:

Command structure

(As you would’ve imagined, this command structure is not perfect. That’s OK. Purpose here is to use a structure that covers a few of the most common scenarios)

Prerequisites

Before we get into discussion on coding, let’s ensure that prerequisites for our development environment is met.

  • Ensure that latest version of Go is installed in your machine (v1.14.2 at the time of this writing). If you are yet to set up Go workspace in your machine, follow the steps from here to do the installation.
  • Rest of this article expects you to have some basic understanding about Go language. If you need to refresh a bit, click here to get started. (Even if you don’t have Go experience, I still encourage you to read through to know what lies ahead)

Project

Instead of starting everything from scratch, we’ll refer to the completed project and do a walk through of the steps involved in creating it. The completed project is available in my GitHub repo. Clone the repo as follows:

$ git clone https://github.com/bijeshos/go-cli-examples.git

The code for our skeleton CLI application can be found in the sub-directory named deployer. Let’s explore important building blocks.

The project structure would look like the following:

Project structure

What does each file represent? Let’s find out.

  • main.go is the entry point to the program. Code related to individual commands are kept in separate files, including root command. These command files are kept in a directory named cmd, which is also defined as a package.
  • go.mod has details about go module and go.sum is an auto generated file containing the expected cryptographic hashes of the content of specific module versions.
  • cmd/doc.go contains package declaration details.
  • cmd/root.go has code for the root command (deployer)
  • cmd/deploy.go, cmd/undeploy.go, cmd/status.go and cmd/version.go contains code for respective commands.

Please note that this is one of the ways such a project can be structured. It is not necessarily the only way.

Exploring the code

Main building blocks in a Cobra applications are Commands, Args and Flags.

To quote from Cobra documentation:

Commands represents the action to be performed, Args represents the things and Flags are modifiers to those actions.

Entry point

The main file usually does not contain much code. It’s purpose is to initialize Cobra. In our case, main.go is as follows:

main.go | Import statements and main function

Root command : deployer

The root command details are kept in cmd/root.go

After declaring cmd as the package, relevant import statements are added as follows:

root.go | Package declaration and import statements

First two import statements are for formatting and Operating System functionalities. go-homedir is used to work with home directory. Last two statements are for Cobra and Viper respectively.

root.go | Root command declaration and Execute() function

The root command is declared using &cobra.Command{} . It accepts three parameters: Use, Short and Long. Use specifies the name of the command; remaining two are for short description and long description.

The Execute() function simply invokes root command’s Execute() function.

Initialization and error handling is done as follows.

root.go | Initialization and error handling

During the init() phase, we’ll pass a function which will process the config file (initConfig()). This function is used to set the config file. Config file can be accepted as a flag or assigned to a default config file location. Viper functions are used for such configuration related activities.

root.go | Initialization and error handling : config assignment

That completes our root.go. Completed code for root command can be found here

Sub-command : version

Now, let’s look at how we can add a sub-command, version. Respective code is in cmd/version.go.

Import statements are straight forward: just fmt and cobra.

version.go | Import statements

This sub-command is also created using &cobra.Command{}. Unlike the root command, in addition to Use (name), Short and Long arguments, we’ll also specify a Run parameter, which takes a function. Whatever action we expect this sub-command to perform, we can add here. At the moment, let’s just print the version number of the tool.

Once version command is declared, we need to attach version as a sub-command to root command. It can be done by invoking root command’s AddCommand() function. This step will add version as a sub-command to deployer. More commands and sub-commands can be chained in this fashion. We’ll see this approach being used later as well.

version.go | Declaration and Initialization

Sub-command: deploy

Now, let’s explore the next sub-command: deploy.

Import statements are similar to that of version.go. The deploy command is also declared similar to the way version command was declared, except the Run arguments. Run argument is not provided since we don’t want deploy command to do anything own. ‘deploy’ should be executed together with it’s sub-commands: web, api or database.

cmd/deploy.go | Declaration

The three sub-commands for deploy (web, api, database) are declared as follows:

cmd/deploy.go | Declaration of deploy: web sub-command
cmd/deploy.go | Declaration of deploy: api sub-command
cmd/deploy.go | Declaration of deploy: database sub-command

Note that, we’ve provided ‘Run’ argument for each of these sub-commands. At the moment, this will be just a placeholder. Code to invoke actual automation end points can be added here.

Now, it’s time to chain these commands together in the init function.

cmd/deploy.go | Initialization and command chaining

That completes deploy.go. The completed code can be found at here

Same approach is used to define undeploy and status sub-commands as well. Completed code for the those two sub-commands can be found here and here respectively.

Building the project

Once we complete coding for all relevant commands, it’s time to build and generate binaries.

On Linux, go to a terminal and execute the following:

$ go build -o build/deployer

This will create an executable called deployer in build sub directory.

On Windows, it can be done by going to command prompt and executing the following:

$ go build -o build/deployer.exe

This will generate an executable called deployer.exe in build sub directory.

If you would like to generate binaries for multiple platforms & architectures, you could do as follows (On Linux, Windows or Mac).

For Linux 64 bit target system:

$ env GOOS=linux GOARCH=amd64 go build -o build/deployer

For Windows 64 bit target system:

$ env GOOS=windows GOARCH=amd64 go build -o build/deployer.exe

Execution

Since we’ve got the the binary generated now, execution is straight forward.

Following is how commands can be executed on Linux terminal.

Invoking root command
Getting ‘help’ on root command

If you’ve noticed, when we run the root command (deployer), nothing much happens except a suggestion on usage. The same would appear if we were to execute deploy help. You may remember that we have not added any code to do so. This is handled by Cobra behind the scenes. Cobra identifies the command structure and displays details accordingly. It automatically lists various sub-commands, flags and aliases defined. This feature provides a good command line user experience, without adding much code to get that behaviour.

We can also get help on the sub-commands as follows:

Getting help on ‘deploy’ sub-command
Getting help on ‘deploy web’ sub-command

When we need to execute actual commands, we could do as follows:

Executing ‘version’ and various ‘deploy’ sub-commands
Executing various ‘status’ sub-commands
Executing various ‘undeploy’ sub-commands

As you can see, relevant placeholder statement are executed.

Next steps

At the moment, CLI skeleton structure is ready along with relevant placeholders. The next step would be to replace the placeholders and plugin actual deployment automation end points’ invocations. Once that is done, you are all set to give it your users.

Conclusion

Let’s summarize what we have done so far.

  • We discussed about Cobra and Viper at a high level
  • How the project is organized and a quick walk through of important building blocks
  • How the binaries for various platforms and architectures can be generated
  • How the binary can be executed
  • Some of the benefits of provided by Cobra

That’s all for now.

Thank you for reading so far. Hope the time you spent was worth it.

If you have any suggestions, kindly share the same as a comment below. Your feedback is really valuable.

Till we meet next time, happy coding. Thank you.

References

If you would like to explore more, please refer to the official repositories of Cobra and Viper.

Cobra: https://github.com/spf13/cobra

Viper: https://github.com/spf13/viper

To know more on Go, please refer to the official Go website.

Go: https://golang.org/

--

--