← writing

Building a CLI tool in Go: lessons from logpipe

02 Sept 2024

logpipe started as a weekend script to stop copy-pasting log processing commands. A year later, a few hundred people use it. Here's what I learned.

Go is the right language for CLIs

Not because of performance — for most CLI tools that doesn't matter. Because of the binary. One file, zero dependencies, works on any platform. No "install Node first", no virtualenv, no version managers. You ship a binary and it runs.

goreleaser handles cross-compilation and GitHub release artifacts. Setup once, never think about it again.

cobra vs hand-rolled

I started with cobra. It's the standard choice, handles subcommands, generates help text, has a huge ecosystem. But for a tool with a few commands, it felt like a lot of setup for boilerplate I didn't need.

I switched to urfave/cli. Lighter, more idiomatic, easier to test. The right tradeoff for logpipe's scope.

If I were building something complex — multiple nested subcommands, plugin system, config management — cobra is still the answer.

Config file headaches

The hardest part wasn't the logging logic. It was config discovery: where does the config file live? What's the merge order between config file, env vars, and CLI flags?

The answer I landed on (and I think it's the right one):

  1. CLI flags win, always
  2. Env vars second
  3. Config file last

Document it clearly. Users get confused when flags don't override config.

Testing CLI tools

Two things that made testing easier:

  1. Keep business logic in packages, not in main or command handlers. The command handler just parses args and calls the function. The function is tested normally.

  2. For integration tests, build the binary and shell out. It's slower but tests the real thing. I run these in CI, not in development.

The maintenance reality

Open source at small scale is mostly issue triage. Most issues are: user misunderstood the config, user hit an edge case in config parsing, user wants a feature that exists but is undocumented.

Good docs reduce issues more than good code does. I spent a weekend rewriting the README and cut new issues by half.