CLI Design
This page is about building delightful command line apps, which is a deep topic. Colors and visual design is carved out separately, this page focuses on the basic principles of cli design - stdout/stderr, exit code, args, flags, subcommands, etc. We are having a bit of a CLI renaissance with the rise of LLMs. This is also powered by backlash against the plodding, buggy nature of web pages. You can contribute to the renaissance by building delightful apps.
I wanted to call out the two main cli guides that made an impact on me:
- 12 Factor Cli Apps. Essential reading. Written by jdx, who went on to create mise.
- CLIG.dev A bit overbearing, perhaps due to excessive use of Apple Garamond. Love the philosophical tone, though.
Let's stick to the important stuff:
Standard Flags
| flag | behavior |
|---|---|
| -h, --help | show help, then exit 0 |
| -f, --force | yes, do the dangerous thing |
| -v, --version | show version, then exit 0 |
| -q, --quiet | no output |
| --dry-run | don't do anything, just preview |
| --json | output JSON only (for LLMs) |
I do not recommend -v for verbose, instead I like -v for version and use an ENV var like $MYAPP_DEBUG=1 for debug output.
CLI Golden Rules
-
stdoutis for output,stderris only for errors. Exit 0 on success, 1 on failure. -
When running --help or "naked" (no args), print help on stdout and exit 0. Let me pipe it into my pager! Try not to commingle stdout and stderr, this is difficult to untangle.
$ myapp # print help on stdout, exit 0 $ myapp --help # print help on stdout, exit 0 $ myapp cmd --help # print help for this subcmd, exit 0 $ myapp --bogus # print error on stderr, exit 1 $ myapp bogus # print error on stderr, exit 1 $ myapp cmd --bogus # print error on stderr, exit 1 -
Fail fast, fail loud - Don't try to recover from strange errors. Humans and LLMs are both perfectly capable of fixing things, and will probably do a better job than some lightly tested limb of your cli. Has your LSP silently stopped working in vscode? Has prettier failed to format? Give me errors right away, don't silently fail!
-
Don't prompt - Except maybe for destructive actions, support
--forceto skip. -
Use XDG - I may uninstall your app if it writes straight into $HOME.
-
Errors - Guys, it's not that complicated. Make sure your error includes enough detail! Here is a handy chart that illustrates the problem. Some libraries and languages make this quite difficult, unfortunately. I am sending Zig the stink eye right now.
nope yup file not found /tmp/something.txt not found invalid arg --bogus is not a valid flag missing required arg curl: no URL specified -
Flags > env > config file - If your app is successful, users will want to configure it in a more permanent fashion. Some flags will be very popular. Users will setup aliases. They will want ENV variables. They will want config files. You can add support for all this stuff, but remember the priority of each mechanism.
-
ENV is great for spelunking - I love it when apps & libs support special environment variables that modify behavior.
--helpis the public face of your app, but it doesn't have to end there. I love love love when I find a MYAPP_DEBUG=1 flag. As a cli author I hesitate to add cli flags that increase verbosity, turn the cache off, disable retries, etc. Each flag comes with a small burden for testing, --help text, man page, README, and completions. Secret ENV flags mostly sidestep that burden. Add 'em!
Example - gum
Gum is a fun little app from our friends at Charm. Like all charm apps, it is written in golang and uses kong for args. A fine example of what you get out-of-the-box with a proper library. This is why I highlight Kong and other stellar libraries on my recommendations page.
1. Naked gum
Nice help message, and it goes to stdout. Only quibble is that naked gum exits with a non-zero code. I understand that this is technically an error ("the user didn't pass a subcommand") but this is unfriendly. In my terminal this shows a nasty red ERROR indicator. Why make me feel bad just for entering your command naked? This reminds me of web forms that display an error as soon as you focus the input, then the error disappears once the field is valid. Meanies.
$ gum
Usage: gum <command> [flags]
A tool for glamorous shell scripts.
Flags:
-h, --help Show context-sensitive help.
-v, --version Print the version number
Commands:
choose Choose an option from a list of choices
confirm Ask a user to confirm an action
...
2. gum --help
Same as above, but exit code zero. Sweet relief.
$ gum --help
Usage: gum <command> [flags]
A tool for glamorous shell scripts.
Flags:
-h, --help Show context-sensitive help.
-v, --version Print the version number
...
3. gum subcommand --help
Same as above, but the help text is specific to the subcommand. Great.
$ gum choose --help
Usage: gum choose [<options> ...] [flags]
Choose an option from a list of choices
Arguments:
[<options> ...] Options to choose from.
Flags:
-h, --help Show context-sensitive help.
-v, --version Print the version number
--ordered Maintain the order of the selected options ($GUM_CHOOSE_ORDERED)
--height=10 Height of the list ($GUM_CHOOSE_HEIGHT)
...
4. gum errors
These show help again, a useful error message, and exit non-zero. Perfect!
$ gum bogus
$ gum --bogus
$ gum choose --bogus
...
[various surly error messages]