The difference between an aggravating CLI tool and a great one can often be made by a few simple changes.

I have built many CLI tools over the years, including pipx which has nearly a half a million downloads. Here I try to capture the important things in a CLI tool that make it pleasant to use. These are conventions and expectations I’ve come to adopt. Without further ado, here is the checklist.

Use an argument parsing library

Don’t try to manually parse command line arguments. Use a library. Libraries will also autogenerate help text.

Set the Exit Code Correctly

On POSIX systems the standard exit code returned to the parent process (the shell in this case) is 0 for success and 1 to 255 for anything else. People calling your tool will want to script around its success or failure. In bash and zsh, the exit code can be obtained with $?. For errors, I generally just set 1, but depending on the context you may give special meaning to other error code values.

Show help text with --help

This is a common expectation of users. The tool should autogenerate help text.

Show version with --version

This is a common convention. Don’t use -v since this can be confused with verbose.

Allow for optional verbosity (and use a logger)

Whether it’s with a --verbose flag, or more fine-grained logging levels like ERROR, INFO, or DEBUG, allow users to get more detailed information about what’s going on. You should NOT accomplish this by doing if (verbose) { print(verboseMessage) }.

Instead, use a logger, and set the desired logging level. That way, you can make logging calls everywhere, and set the logging level once.

If the logging level only shows errors, the info or debug log calls will be no-ops. Otherwise, they’ll print something (to stderr, of course).

Use stderr and stdout intentionally. They have different purposes.

stdout is meant to be machine parsable, and only show output intended by the invocation of the CLI tool. stderr is for diagnostic messages and logging. By intentionally separating streams, you can print out as much diagnostic information as you want, yet machine parsable output like json can still be parsed from stdout.

The streams are distinct, and have distinct purposes. Read more.

Don’t show end users a stack trace. Ever.

The CLI is an abstraction on top of whatever language it is written in. The only thing the user should be concerned with is the API the CLI exposes: the help text and actionable error messages.

Errors should be oriented around help text. A stack trace showing an error is a confusing experience for the user. Instead, wrap in a try/catch, and display an actionable error message.

Make error messages as helpful as possible

Construct the message with the user in mind. Instead of saying “found invalid character at position 0”, which leaves the user working more to decipher what that means, say something like “Could not parse input from file.txt. Confirm it is valid JSON then try again.” It’s also often helpful to follow up with the underlying error message as well, in this case “found invalid character at position 0”.

Use Color Sparingly

Users may have different needs for contrast. Some users have a white background, some have a dark one, so setting the color might wash out the text. Also, some users may be visually impaired and need special settings. Using too many colors ends up being counterproductive by making the tool look too jarring or amateur. The ony rule that is generally okay is errors in red, warnings in yellow, and output in the default color.

Signal “end of options” with --

This is a bit of a corner case, but I’ve needed it a few times. The -- delimeter is used to signal anything after that argument is to be used literally, not as a flag. For example, in

grep -- -c file.txt

means the literal string -c is searched for in the file. Without the delimeter, it would be an error as the -c would be interpreted as a flag, not an argument. Read More.

Be as quiet as possible

Have you noticed how little output is generally given for built in commands like ls or rm? They get straight to the point. They don’t say “looking for files in directory… found 5 files… here are the files…”. If users want to see that, they can use verbose flags.