---
title: "18 — Building CLIs — From flag to Cobra, urfave/cli, and Bubble Tea"
uid: building-clis
tags: ["golang", "coverage", "testing", "urfave-cli", "bubbletea", "roadmap:golang", "flag", "cli", "cobra"]
excerpt: "Single-binary output plus cross-compilation make Go the CLI language; flag covers the basics, and Cobra, urfave/cli, and Bubble Tea cover progressively richer interfaces."
date: 2026-08-13T03:28:09+0000
source: https://www.aveshina.my.id/en/blog/building-clis
---

The use case where Go most obviously shines is also the one that shows off its deployment story. The model that clicked: **Go's fast compile and single statically-linked binary make it the best mainstream language for CLIs, the standard flag package covers simple tools, and three libraries — Cobra, urfave/cli, and Bubble Tea — cover progressively richer command structures and terminal UIs.** [1] The deployment story is what makes it click: one file, no runtime to install, cross-compiled to any OS from one command. That is why tools like gh, kubectl, hugo, and terraform are written in Go.

## Why Go dominates CLIs

Three properties line up perfectly for command-line tools [1]:

- **Single static binary.** go build produces one executable with every dependency linked in. Users install by downloading one file and putting it on PATH — no Python virtualenv, no Node version manager, no JVM.
- **Cross-compilation.** GOOS=linux GOARCH=arm64 go build builds for a Raspberry Pi from a Mac, with no toolchain installed for the target. One command per platform.
- **Fast startup.** A Go binary starts in milliseconds, unlike a JVM or a cold Node script. CLIs feel instant.

Add Go's strong standard library (os, flag, fmt, io) and the result is a language where a useful CLI takes an afternoon.

## flag — the standard-library starting point

For a tool with a few flags and no subcommands, flag is enough [2]:

```
port := flag.Int("port", 8080, "port to listen on")
verbose := flag.Bool("v", false, "verbose output")
flag.Parse()
fmt.Println(*port, *verbose)
```

It auto-generates -h/--help text from the descriptions. The limitation: flag does not do subcommands (myapp serve, myapp build) gracefully, and its help output is plain. The moment a tool needs nested commands or richer UX, the standard library hands off to a framework.

## Cobra — the subcommand standard

Cobra is the dominant CLI framework, used by kubectl, gh, and Hugo [3]. It provides:

- **Nested subcommands** (git remote add, git remote remove) with per-command flags.
- **Automatic help** generation, including suggestions for typos (did you mean "build"?).
- **Shell completion** scripts for bash, zsh, fish, and PowerShell.
- **A code generator** (cobra-cli) that scaffolds the command structure.

```
var rootCmd = &cobra.Command{Use: "myapp"}
var serveCmd = &cobra.Command{
    Use:   "serve",
    Short: "Start the server",
    Run: func(cmd *cobra.Command, args []string) {
        // start server
    },
}
func init() { rootCmd.AddCommand(serveCmd) }
func main() { rootCmd.Execute() }
```

The pattern is a tree of Command structs, each with its own Use, Short, flags, and Run function. Cobra handles parsing, routing, help, and completion. For any CLI that will grow beyond three flags, Cobra is the default choice — it is opinionated, well-documented, and matches what users expect from a modern tool.

## urfave/cli — the lighter alternative

urfave/cli is a simpler alternative for CLIs that want subcommands and flags without Cobra's full machinery [4]. It has an intuitive API, automatic help, bash completion, and environment-variable integration, but a smaller feature surface. The choice between Cobra and urfave/cli is largely taste — Cobra is more popular and has the code generator; urfave/cli is lighter and reads more like plain Go. For a tool whose CLI is a thin wrapper over library functions, urfave/cli is often enough.

## Bubble Tea — terminal UIs, not just lines

Bubble Tea (from the Charm ecosystem) is a different category: a framework for **interactive terminal UIs** based on the Elm Architecture (model-update-view) [5]. Where Cobra prints lines, Bubble Tea builds full-screen interfaces — selectable lists, spinners, text inputs, styled panels, even dashboards.

```
type model struct{ choices []string; cursor int; selected map[int]bool }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { /* handle keys */ }
func (m model) View() string { /* render */ }
```

The component renders to a string, the runtime handles the terminal redrawing, and the model is plain data. Bubble Tea is the choice when a CLI needs real interactivity — a configuration wizard, a live process monitor, a TUI dashboard — beyond what line-oriented output can express. Tools like gh dash and lazygit-style interfaces sit here.

## Testing and coverage for CLIs

A CLI is only as trustworthy as its tests. Go's testing package (covered more in the next notes) gives table-driven tests and httptest for HTTP-based tools, and go test -cover plus go tool cover -html visualize exactly which lines ran [6]:

```
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```

The HTML view color-codes every line: green for covered, red for not. The discipline of running coverage before a release surfaces the error paths nobody tested. For CLIs specifically, integration tests that invoke the compiled binary with exec.Command and assert on its stdout/stderr/exit code catch the wiring bugs that unit tests on the library code miss.

## How I use this

The progression maps cleanly to tool complexity. For a personal script with two flags, flag is enough and adds zero dependencies. For a tool anyone else will use, I default to Cobra from the start — the cost of adopting it early is far lower than retrofitting subcommands onto a flag-based tool, and the auto-generated help and completions are immediate wins. Bubble Tea I reach for only when line output genuinely cannot express the UX — a wizard, a live monitor, a selector. And every CLI gets go test -cover before release, because the gap between "the happy path works" and "the error paths are covered" is where the production bugs hide. The single-binary output is the constant payoff: I build once with GOOS/GOARCH, drop the file in a release, and users install by downloading one asset.

## References

[1] The Go Authors, "Command-line Interfaces (CLIs)," Go Solutions, 2024. [Online]. Available: [https://go.dev/solutions/clis](https://go.dev/solutions/clis)

[2] DigitalOcean, "How To Use the Flag Package in Go," 2024. [Online]. Available: [https://www.digitalocean.com/community/tutorials/how-to-use-the-flag-package-in-go](https://www.digitalocean.com/community/tutorials/how-to-use-the-flag-package-in-go)

[3] The Cobra Authors, "Cobra," cobra.dev, 2024. [Online]. Available: [https://cobra.dev/](https://cobra.dev/)

[4] urfave, "urfave/cli," cli.urfave.org, 2024. [Online]. Available: [https://cli.urfave.org/](https://cli.urfave.org/)

[5] Charm, "charmbracelet/bubbletea," GitHub, 2024. [Online]. Available: [https://github.com/charmbracelet/bubbletea](https://github.com/charmbracelet/bubbletea)

[6] The Go Authors, "Coverage profiling," Go Documentation, 2024. [Online]. Available: [https://go.dev/doc/build-cover](https://go.dev/doc/build-cover)

```quiz
Q: Why is Go particularly well-suited for CLI tools?
- it has the largest package registry for CLI libraries
- it compiles to a single statically linked binary, cross-compiles to any OS with one flag, and starts in milliseconds
correct: 1
explain: One file to download, no runtime to install, instant startup, and builds for any target from any host. That is why kubectl, gh, hugo, and terraform are all Go.

Q: When does the standard `flag` package stop being enough?
- never — it covers every CLI need
- when the tool needs nested subcommands (myapp serve, myapp build), rich help, or shell completion
correct: 1
explain: flag handles flat flags and auto -h help. The moment you need subcommands, typo suggestions, or completion, a framework like Cobra or urfave/cli takes over.

Q: What does Cobra provide beyond the standard flag package?
- only faster flag parsing
- nested subcommands, automatic help with typo suggestions, shell completion, and a code generator for scaffolding
correct: 1
explain: Cobra is a full CLI framework: command trees, per-command flags, generated help, completion scripts, and cobra-cli for scaffolding. It is the default for serious CLIs.

Q: Bubble Tea differs from Cobra/urfave/cli in that it…
- is a faster flag parser
- builds interactive full-screen terminal UIs using the Elm Architecture (model-update-view), not just line output
correct: 1
explain: Bubble Tea is a TUI framework for interactive interfaces — lists, spinners, panels, dashboards. The others are line-oriented command frameworks. They solve different problems.

Q: After `go test -coverprofile=coverage.out ./...`, how do you see which lines were not covered?
- you cannot — cover only reports a percentage
- run `go tool cover -html=coverage.out` for a color-coded HTML view showing covered (green) and uncovered (red) lines
correct: 1
explain: go tool cover -html opens a browser view of every source file with lines color-coded by coverage. It is the standard way to find untested error paths before release.
```
