My Profile Photo

Tom Bell


Seasoned code hacker, and rookie DJ
Northern Quarter, Manchester


My Go CLI Patterns

I stumbled across a post on the Go subreddit asking about the patterns people use for building command-line applications. The post interested me because I’d recently written a number of command-line apps using Go. With writing these I had developed a number of patterns that I reuse when building command-line apps now.

The first thing I noticed about the replies to the Reddit post was that most people were suggesting the original poster should use different packages for building command-line apps. I stand by the philosophy where by you should try to stick to as much of the standard library as possible. After writing a number of my command-line apps, these are the patterns I try to stick by.

Project Structure

I use a typical project structure that the Go community seems to have adopted.

Example from tombell/brewer

brewer
├─ cmd
│  └─ brewer
│     ├─ main.go
│     └─ version.go
├─ brewer.go
├─ formula.go
└─ formula_test.go

I always start by creating the cmd/binaryname directory which will be the main package for the binary being created. Instead this directory I’ll always have a main.go and version.go.

  • main.go will contain the main() function and flags parsing.
  • version.go will contain the Version and GitCommit variables.

The Version and GitCommit variables have the values injected at build time using -ldflags.

-ldflags "-X main.Version=${VERSION} -X main.Commit=${COMMIT}"

The next file that always exists is a Makefile. Using make is so simple, it’s a simple tool, that provides a simple way for running different tasks for your project. I have a typical Makefile that I include in each project, and change accordingly. The three important targets are dev, dist, and test.

The root of the project will be the package for the main “app” code. The package is always named after the project.

Flags

Parsing command-line arguments always felt like this daunting part of writing a command-line app to me. Having come from Ruby and JavaScript there are many packages to help ease parsing command-line arguments.

First coming to Go I looked at a number of flag and command-line app specific packages but they always felt overkill given the simplicity of Go as a language. That was all before I learnt to love to the [flag][flag-pk] package in the standard library. However I must confess I ended up using go-flags for releasekit because I didn’t know how to create flags using the flag package that could be used more than once to give multiple values. I have learnt since that it is possible to do this using the flag package.

When declaring my flags I will declare them as non-exported globals, this just makes using them a bit simpler in the main package. Some people will be against using globals here, but I prefer the simplicity over strict structure.

var (
  version = flag.Bool("version", false, "")

  token = flag.String("token", "", "")
  owner = flag.String("owner", "", "")
  name  = flag.String("name", "homebrew-formulae", "")

  // ...
)

As you might see I give defaults where required, however I omit the description of the flag with an empty string. I adopted this pattern from the command-line apps of Go itself, overriding the usage/help output of the flags with my own.

const helpText = `usage: brewer [options]

Special options:
  --help     show this message, then exit
  --version  show the brewer version number, then exit

GitHub options:
  --token  GitHub API token
  --owner  GitHub repository owner of the Homebrew tap
  --repo   GitHub repository name of the Homebrew tap (defaults to homebrew-formulae)

Formula options:
  --formula   Path to the formula in the git repository
  --tag       New git tag to update in the formula
  --revision  New revision SHA to update in the formula
  --url       New URL to update in the formula
  --sha256    New SHA256 to update in the formula

Commit options:
  --commit-message  Commit message for the formula update
  --commit-author   Commit author for the formula update
  --commit-email    Commit email for the formula update
`

func usage() {
  fmt.Fprintf(os.Stderr, helpText)
  os.Exit(2)
}

Then before I call flag.Parse() I override the flag.Usage function with my own.

flag.Usage = usage
flag.Parse()

I always have a version flag for determining if the version information should be printed. This is the Version and GitCommit variables that are injected at build time.

The next thing I typically do is validate any required flags. I create a validateFlags function that will check if flags are present, and if not print a message and exit. This function is just called after checking for the --version flag.

func validateFlags() {
  if *token == "" {
    exit("must provide --token flag for GitHub API token")
  }

  if *owner == "" {
    exit("must provide --owner flag for GitHub repository owner")
  }

  // ...

Running

Once the flags have been parsed and validated, I pass them off to the main package of the command-line app to run.

I create a single Run function on the main project package, that receives the flags, and begins doing whatever the command-line app is built to do. Depending on the number of flags I need to pass to this function I’ll either pass them as arguments to the argument (I usually hard cap this at 5 arguments), or as a Config structure with fields for each argument.

func Run(config Config) error {
  // ..
}

The Run function will return just an error, which will be nil unless there was an error. This leaves the responsibility of the main package to be to parse and validate command-line flags, and pass them to the project package.

These patterns have served me well for the number of command-line apps I’ve written this year. I’m sure they won’t be set in stone, and they’ll evolve further when I learn new tricks and ways to do what I need.