Supercharge Your DevOps Arsenal:

Building Your First CLI Tool in Go

Joshua Delsman
5 min readOct 11, 2015
The help output from docker-machine, a command-line tool written in Go!

As a dev/ops engineer, roughly eighty-percent of my time is spent deep within a bowels of various flavors of a Unix shell, calling up a plethora of command-line tools over the course of the day: docker, brew, ip6calc, hub, otto…and on and on. Before I discovered Go, my devops tooling was written in mostly Ruby, requiring it’s very own runtime environment! This meant writing/maintaining Chef recipes, Ansible playbooks, or even plain ol’ Bash build scripts (in extreme cases) to install Ruby on a server to even use these tools!

However… I soon discovered how simple it was to write command-line tools using Go! Not only are Go programs compiled to a single, tightly-wound executable binary, but they include all their runtime dependencies!

ZOMG! No more need for those expensive runtime environments!? ( ノ^.^)ノ゚

In the following series of posts, I will outline the process of building a command-line tool. The easiest way to master a programming language is by building something tangible — something useful, or something one is passionate about! I have chosen to rewrite (most of) the awesome Engine Yard Ruby gem in Go. If all goes according to plan, the result will be a binary with zero runtime dependencies, easier installation, and hopefully faster deployments.

BY THE WAY… If you need badass dudes with true ops-chops to manage your infrastructure, I recommend trying Engine Yard!

Sign up for a free trial500 free hours on one or more instances!

Beginning a new project

$ eyNAME:
ey — The Engine Yard command line utility
USAGE:
ey [global options] command [command options] [arguments…]
VERSION:
0.1.0
...

I am assuming you have a fully-functioning local Go environment. Let’s start out by creating a new project in our GOPATH: gitlab.com/voxxit/ey.

NOTE: It’s important to follow the workspace nomenclature, as this is how other Go projects may reference any library or public API you may provide.

Command-line tools usually consist at least one, but usually more than one command — forks of the main program which the user may request by name. It is important to quickly go over a typical Go-based command-line tool’s project structure:

$ tree
.
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── command/
│ ├── deploy.go
│ ├── environments.go
│ ├── init.go
│ ├── login.go
│ ├── logs.go
│ ├── rebuild.go
│ ├── recipes.go
│ ├── rollback.go
│ ├── servers.go
│ ├── ssh.go
│ ├── status.go
│ └── web.go
└── main.go
1 directory, 18 files

main.go —

Executable commands in Go must always use package main. This file should be pretty simple: it should define the tool (name, usage, author, etc.) and then branch out tasks to the command package. This may seem familiar, as this is pretty much the way you’d start out a program in C, too!

command/NAME.go —

For each command, we will create a new file with the command name and the .go suffix. For instance, the command “ey deploy” code would live at command/deploy.go.

Makefile —

After typing “go build” a few hundred times, or once you need to support more than one platform, you’ll probably want a Makefile for your Go command-line utility will be a necessity. There are lots of examples for you to get inspiration from. For our tool, though, we’ll keep it simple: it will build our tool for the Mac, Windows & Linux platforms, and run our tests for us.

Laying out the project

I cannot name a single Go programmer in the know who would deny this: Jeremy Saenz is truly a “code gangsta!” Soon after I wrote my first CLI tool from scratch using only the flag package (part of the Go standard library) I discovered his awesome cli project on GitHub.

By importing this project into our project, much of the necessary framework we’ll need to get going with our CLI tool is already done.

Use “go get” to fetch this package so we can use it:

$ go get github.com/codegangsta/cli

Now, we can start writing the main.go file:

Seem a bit too simple? I’ll explain it step-by-step:

package main

As I mentioned before, all command line executables in Go must start from the main package. The presence of the main package will tell the Go compiler that it should compile an executable program instead of a shared library.

import (
“os”
“github.com/codegangsta/cli”
)

import instructs the compiler to pull a package from your workspace into your program. In this case, I’ve imported the github.com/codegangsta/cli library, as well as the os package from the standard library — used to grab the command-line arguments.

var app *cli.App

By using var outside of the main function, we can create a placeholder for our app variable. This instructs the compiler to allocate enough storage for the variable when it compiles the program.

func main() {
// Create a new app instance
app = cli.NewApp()
// Define the app we’re building
app.Name = “ey”
app.Usage = “The Engine Yard command line utility”
app.Version = “0.1.0”
// Run the app, with the given arguments
app.Run(os.Args)
}

The main function is the starting point of all Go programs. It is the first function which is executed. In our main function, we initialize a new cli.App instance (using our app variable from before) and define a few of the values of the app. Finally, we call our app instance’s public Run method.

Voilà! You should now be able to run “go build”, then run the freshly-compiled binary to see the following:

$ ./ey
NAME:
ey — The Engine Yard command line utility
USAGE:
ey [global options] command [command options] [arguments…]
VERSION:
0.1.0
COMMANDS:
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--help, -h show help
--version, -v print the version

That’s all for now

In part two, get ready to dive deeper! I will go over how to use flags, environment variables, configure Bash autocompletion, and much more to round out our command-line tool. In part three, I plan to show you how to build tests in Go to ensure our tool is production-quality and ready to add to our devops tool belt in no time at all!

Experienced Go developer?
Share a pro-tip with us — add it to the responses below!

Thanks for reading!

Discuss on Hacker News or reddit

--

--

Joshua Delsman

Ten-year Ruby on Rails vet, devops extraordinaire, & Golang guru-in-training