TL;DR: Half the users of any CLI tool aren’t people, they’re other programs: scripts, CI pipelines, automation. Pretty human output is actively hostile to those users, so they end up grep-ing your prose and breaking the day you reword a line. go-tool-base gives every command a --output json flag backed by one standard Response envelope (status, command, data). Every built-in command uses the same shape, so a consumer learns it once and trusts it everywhere.
Your tool has two audiences and only serves one
Run a CLI command and look at what comes back. Colour. Aligned columns. A friendly summary sentence. It’s designed for a person reading a terminal, and for a person reading a terminal it’s great.
Now picture the other half of your users. A deploy script that needs to know which version is installed. A CI job that runs doctor and wants to fail the build on a specific check. A bit of automation gluing your tool to three others. None of them have eyes. They have parsers.
What do they do with your beautiful human output? They butcher it. They grep for a keyword, awk out the third field, sed off a prefix. It works, in the demo. Then someone rewords a status line, or adds a column, or the colour codes shift, and every script downstream breaks at once. Silently, too, because a broken grep returns nothing rather than an error. You changed a sentence and took out somebody’s pipeline.
The human-readable output was never the contract. It just got used as one, because it was the only output there was.
Give the machines their own channel
The fix is not to make the human output more parseable. That’s a trap. You’d be constraining prose meant for people to satisfy programs, and serving neither well. The fix is to give programs their own output format, declared and stable, separate from the prose.
So every command built with go-tool-base gets a --output flag. Leave it alone and you get the friendly human rendering. Pass --output json and you get something a parser can actually rely on.
And not just some JSON. JSON with a fixed shape.
One envelope, every command
The temptation with JSON output is to let each command emit whatever structure suits it. Don’t. A consumer scripting against five of your commands then has to learn five shapes, and “where’s the actual payload” is a different answer every time.
go-tool-base wraps every command’s JSON in one standard Response envelope:
{
"status": "success",
"command": "deploy",
"data": {
"environment": "production",
"version": "1.4.0",
"replicas": 3
}
}
status says how it went. command says what produced it. data holds the command-specific payload, and only the payload. Every built-in command (version, doctor, update, init) emits exactly this shape. So does every command you write, because pkg/output hands you the envelope rather than letting you freelance:
format, _ := cmd.Flags().GetString("output")
w := output.NewWriter(os.Stdout, output.Format(format))
return w.Write(output.Response{
Status: output.StatusSuccess,
Command: "deploy",
Data: result,
})
The consumer-side payoff is the whole point. A script can check .status without touching .data. It can pull .data.version and know the field is there because it’s typed, not scraped. It learns the envelope once and every command in your tool, and every tool built on the framework, honours it. The contract is explicit, versioned, and the same everywhere, which is precisely what the abused human output never was.
The human output gets to relax
There’s a quiet second benefit. Once programs have their own reliable channel, the human output is freed. It no longer has to stay parseable-by-accident. You can reword a status line, add colour, restructure a table, make it genuinely nicer to read, and not break a single script, because no script is reading it any more. They’re all on --output json, where the contract lives.
Two audiences, two formats, each one actually suited to its reader. That’s the deal a CLI tool should be offering, and most don’t.
The gist
A CLI tool that only emits human-readable output is only half-built, because half its users are programs that end up grep-ing prose and shattering the moment that prose changes. go-tool-base gives every command a --output json flag and one standard Response envelope (status, command, data) used identically by every built-in command and by anything you write through pkg/output. Machines get a stable, explicit, learn-it-once contract; humans get output that’s now free to be properly readable because nothing fragile depends on its wording.
If your tool will ever be called by another program (it will), give that program a front door. Don’t make it climb in the window.