Pages

▼

Building CLI Tools

🧑🏻‍🎓 AL Academy Masterclass

Building CLI Tools

A command-line tool is a product with users. Treat it like one and it will outlive everything you build around it.


The terminal is having a quiet renaissance. After a decade in which every new tool arrived as a web dashboard, developers keep gravitating back to the command line: gh, aws, kubectl, rg, fzf, an entire generation of fast, focused utilities. The reason isn't nostalgia. It's that a good CLI fits the way technical people actually work, and a great one is one of the most durable, leverage-rich things you can ship. The web app you build today may be rewritten in three years. The CLI that does one thing well may still be in someone's Makefile a decade from now.

The catch is that "good" and "great" are not accidents. They come from treating the tool as a product with users, not as a script that happens to take arguments.

The script that became a product

Almost every CLI starts as a script. You automate something tedious, hardcode a path or two, and move on. Then you run it a second time. Then a colleague asks for it. Then you find yourself editing the source to change a value that should have been a flag. Somewhere in there, the script crossed a line: it acquired users, and it became a product. The trouble is that nothing about the file changed to mark the moment, so most tools never get the design they now deserve.

Recognizing that transition is the single most useful habit in CLI development. The instant a tool has users - even just you, six months from now - it earns the same questions you'd ask of any product. What is the common case, and is it the default? What happens when the input is wrong? Can another program consume the output? Can someone install it without reading a wiki?

Stand on the shoulders of Unix

You don't have to invent the answers. The Unix philosophy, codified by McIlroy and elaborated in Eric Raymond's The Art of Unix Programming, gives you a design language that's been stress-tested for fifty years: do one thing well, write programs that work together, and treat text as a universal interface.

That last point is deceptively powerful. When your tool reads from stdin, writes data to stdout, sends its chatter to stderr, and returns a meaningful exit code, it instantly composes with every other well-behaved tool in existence. You write mytool, and suddenly mytool list --json | jq '.[] | select(.active)' | wc -l works, even though you never wrote jq or wc. Composability is not a feature you add; it's a contract you honor. Break it - by printing a progress bar to stdout, say - and you quietly corrupt every pipeline your tool touches.

Human-first, not just machine-friendly

The classic Unix tools were composable to a fault and friendly to nobody. Their error messages were koans. Their help was a man page or silence. The modern correction, captured beautifully in the community-written Command Line Interface Guidelines at clig.dev, is that a CLI has human users too, and human-centered design matters as much as pipe-friendliness.

In practice this means a handful of concrete behaviors. Errors should name what went wrong, where, and how to fix it - "config file 'app.toml' not found; run 'mytool init' or pass --config" beats "Error: invalid input" every time. Help text should lead with a real example, because people copy examples and read prose only when they're stuck. Color should guide the eye but vanish the moment output isn't a terminal or NO_COLOR is set, so it never poisons a log file. Destructive actions should ask first, and offer --dry-run so users can look before they leap. And the tool should say just enough: quiet when piped, talkative when asked, never both.

None of this is hard. It's a series of small respects paid to the person on the other side of the prompt.

Serve the machine on purpose

The flip side of human-first is remembering that half your users aren't human. The flag that most often turns a personal script into team infrastructure is --json. Default to pretty tables and color for people; add a mode that emits clean, structured data and nothing else for programs. With that one option, your tool stops being a destination and becomes a backend other code can build on. That is the difference between a utility and a platform.

The last mile is the tool

A CLI that only runs as python script.py in one folder is a prototype, no matter how good its internals. The thing that makes it real is the last mile: a pyproject.toml with a console entry point so it installs as a named command, a pipx install story so it lives in its own clean environment, a correct --version, tab completion, and a few tests that pin down exit codes and output shape. In Python this is a remarkably short list - a dozen lines of packaging config and a handful of CliRunner tests - and yet it's exactly the part most projects skip. It's also exactly the part users feel.

So build the small tool. Pick a chore you do every week, design its interface on paper, honor the stream-and-exit-code contract, write the errors you'd want to receive, add --json, and package it so a teammate can install it in one line. You'll spend an afternoon and ship something people use for years. Few things in software offer that ratio.

This article accompanies the free Building CLI Tools masterclass at AL Academy. Workshop, PDF handbook and curated resources: alouatiq.com/academy.
clipythondeveloper-toolsargparseclick

No comments:

Post a Comment