TS-16: Command Line Interfaces (CLIs)

This technical standard covers the design of command line interfaces (CLIs). The focus here is on designing CLI utilities for Unix-based environments, but the guidelines are intended to be applicable to the CLIs of programs running in all kinds of environments – Windows, Node.js, Python, Java, etc.

These technical standards build on the Command Line Interface Guidelines, an excellent open source guide to writing better command line programs. Other references are listed at the end of this document.

CLIs are a type of UI, and therefore this technical standard can be seen as an extension of TS-15: User Interfaces. Terminal user interfaces (TUIs), which are full-screen terminal programs like Vim and Emacs, are not addressed by this standard. This standard is all about designing interactive command-oriented CLI utilities.

See also TS-62: Make for guidance on Makefiles, which are commonly used to expose a project’s scripts as a discoverable, CLI-like set of targets.

Principles

Making good quality CLI programs requires giving attention to all the little design details that affect the user experience. It’s about keeping the user informed at all times about what’s happening. It’s about explaining clearly why errors happened and how they can be resolved. It’s about good validation, sensible defaults, reasonable timeouts, and so on.

This section sets out some design principles that will help you to think through all the little details of your CLI programs.

Composability

Design CLI programs to be composable. This means that each program and subcommand should be designed do a small amount of work independently, but that it should be possible to combine multiple simple programs and commands to compose more complex operations. Your CLI programs might even be used by others in ways you did not anticipate.

This design principle is a key tenet of the UNIX philosophy. At a time of large-scale automation, in the form of CI/CD pipelines and container orchestration, this principle is as important today as it has ever been.

Composable CLIs are achieved by following standard conventions for inputs and outputs for the program’s runtime environment. In Unix-like environments, this means using standard in/out/err, signals, exit codes, arguments and flags, environment variables and other long-established conventions for input and output.

Design CLIs as conversations

It is tempting to think about CLIs as programmatic interfaces, used in scripting and automation, rather than human interfaces. But we should design CLIs primarily with human users in mind. Even if a program is intended for use in automation, its client programs will still be developed by real people. Those people, the developers of the client programs, are the users.

The user experience of a CLI should be like having a conversation with the computer.

In CLIs, users provide input through a sequence of commands, each of which performs a specific task within a larger operation. For example, you git add files before you git commit them. The output of one command should guide the user to their next command. When the user inputs invalid data or uses an unsupported subcommand, the program should provide helpful suggestions or error messages to guide the user.

If the user enters an invalid command name, the program SHOULD suggest valid commands with similar spellings or semantics. For example, if the user inputs upgrade, the output might suggest the user try update or install instead. If the user inputs ststus, the output might suggest they meant status.

But you MUST NOT run the alternative operation without the user’s explicit approval, unless it is a documented alias. Making assumptions about a user’s intention can be dangerous, especially where operations may result in modified state.

Make CLIs discoverable

CLIs should be self-documenting. Good help tests, with lots of examples, and suggestions on what commands the user might like to run next, and how they can fix errors – all of these things help users to discover a program for themselves.

Communicate state changes

If the program changes state, inform the user. This is especially important when a program enters intermediate state, while waiting for further user input, or when a failure mode puts the system into an invalid state.

Make it easy for the user to inspect the current state of CLI programs. The Git operation git status is a good model for this.

Control output

An essential design choice for CLI programs is the amount of output they produce by default. The optimum balance between too much and too little output will vary depending on the use cases of the program.

Users should be able to control the amount of output, for example by using --quiet and/or --verbose flags. It may also be desirable to allow users to control output formats, too. Flags such as --plain and --json are commonly used for this purpose.

Be responsive

CLIs should respond quickly to user input. This means providing feedback in under 100ms and keeping the user informed of the progress of long operations, eg. using progress bars.

If you need to make a network request, try to do it in a non-blocking way. If that is not possible or desirable, print something before you initialize the request, so the UI doesn’t hang and look broken if the network request times out. Set sensible defaults for network timeouts, and allow the user to override the default configuration.

A responsive program feels robust and dependable.

Parallelism

Do stuff in parallel where you can, to speed up operations. But only do this if it can be done reliably. It is more important to be responsive and robust, than to be fast.

Crash-only

Design your CLI as a "crash-only" program. This means that the program should exit immediately on failure or user interruption (Ctrl+C).

Besides being safer, this also makes CLI programs feel more responsive and robust.

Idempotency

Make requests idempotent where possible. This means that if a command fails, the user should be able to retry the command – simply by pressing Up and Enter – without causing unintended side effects.

Defensive programming

In designing a CLI, think through all the ways that users could misuse it. Ask yourself, how does the program perform when:

  • it is used in a script;
  • when the user has a bad network connection;
  • when the user runs multiple instances of it at once;
  • when it runs in an unsupported environment.

Plan and test for these use case scenarios. Fail gracefully whenever the program cannot handle the user’s input or is otherwise unable to guarantee correct operation for whatever reason.

Future proofing

In software of any kind, it is important that interfaces do not change erratically. This is especially important in CLI programs, which are commonly dependencies of other programs.

Subcommands, arguments, flags, configuration files, environment variables – these are all interfaces, and once your program starts using them you are committing to keeping them stable.

Major versions SHOULD endure with non-breaking changes for as long as possible – preferably indefinitely, for the whole lifespan of the program.

We do not break userspace.

– Linus Torvalds

Keep changes additive where you can. For example, rather than modifying the behavior of a flag (which would be backwards-incompatible), prefer instead to add a new flag. To avoid bloating the interface, the old flag can be marked as deprecated – but not removed until the next major version bump.

Warn your users about deprecated operations. This gives them a chance to update their clients before they are broken by the next major release of your program.

Avoid creating time-bombs. Think about the future – how might your program work 5, 10, 20 years from now? Can you guarantee it will still work in the same way? If you cannot guarantee this – due to dependencies on external components or services, for example – be sure to clearly document the reasons why.

Naming

When naming CLI commands, subcommands, and options, follow these guidelines:

  • Make the name memorable and easy to type.
  • Keep it short, but not too generic – to avoid conflicts.
  • Reserve the shortest, most generic names for standard tools.
  • Avoid superfluous words such as "tool", "util", and "kit".
  • Don’t name commands after any standard, protocol, or file format, such as "openssl" or "ffmpeg".

Follow the naming conventions of the target runtime system. For Unix-based systems, the convention is to use only lower case ASCII letters and delimit words with single dashes. The user SHOULD NOT need to press their Shift key to type your commands: VirtualBox and easy_install break this rule. Numbers MAY be included but they SHOULD NOT be the first character of a command name.

Don’t pollute the global namespace with dozens of commands. If your package consists of a suite of utilities, consider implementing them as subcommands of a single program namespace. Git does this brilliantly: git [subcmd].

Subcommands can be generic words like "update" and "status". But globally-scoped program names MUST NOT be – to avoid conflicts with other programs. Both ImageMagick and Windows used the command convert — oops!

The very shortest, most generic names should be reserved for standard system tools, or things people use all the time like cd and ls. The more niche the command, the longer its name should be.

Distribution

CLI programs MUST be easy to uninstall.

Where possible, CLI programs SHOULD be distributed as either binaries or via the platform’s native package management system. Binaries and native packages can be easily removed.

Options

There are several mechanisms for passing options to CLI programs. By convention, the order of precedence, from highest to lowest, is as follows:

  • Arguments and flags
  • Local configuration files
  • Environment variables
  • User-level configuration files
  • System-wide configuration files

Different classes of options are suited to different mechanisms of input.

Defaults

It is important for CLI programs to have good defaults. Making things configurable is good. But most users crave convenience above all.

The most commonly-used options SHOULD form the basis of a command’s default configuration.

Arguments and flags

Arguments and flags are options that are inputted directly to commands. These options MUST override options configured via all other input mechanisms – environment variables and all types of configuration files.

Not all input options need to be implemented as arguments and flags. Only the options that are the most likely to vary between invocations should be implemented as arguments and flags.

Arguments and flags are distinct from one another:

  • Arguments or args are positioned parameters to a command. For example, the file paths you provide to cp are arguments. Their order is significant: cp foo bar is not equivalent to cp bar foo. (Arguments look similar to, but are distinct from, subcommands. A command cannot have both arguments and subcommands.)
  • Flags are named parameters. Unlike arguments, the order of flags should not affect program behavior. In Unix-like systems, flags take one of these two formats:
  • --<name>, where <name> is a word, eg. --recursive, or hyphenated phrase, eg. --no-recursive. * -<x>, where <x> is a single-letter abbreviation, eg. -r

As a general rule, arguments SHOULD be used for required options and flags for optional ones. But there are plenty of exceptions to this rule. In some CLIs, it will make perfect sense to have optional arguments and mandatory flags.

Where an input option could be implemented as either an argument or a flag, prefer the flag. Flags take more typing, but they make for a clearer self-documenting API, they are more flexible because they can be applied in any order, and they are more future-proof because we can more easily add new flags in a backwards compatible manner.

Arguments SHOULD be used only for tightly-scoped, single-purpose operations that are likely to be frequently run by your users, and that will unlikely ever change in the lifetime of the tool. Good examples include cp [source] [destination] and rm [file1] [file2] …​.

Good examples of use cases for flags include enabling "safe mode" or "dry run" operations, and "forcing" destructive and dangerous operations to complete without user confirmation (to support non-interactive environments). In fact, these are the sorts of options that SHOULD only be inputted via flags, and not via other mechanisms such as configuration files.

All flags MUST be implemented using the long-form notation, eg. --help. Err on the side of clarity over brevity every time. Short-form flags MAY be implemented as shorthand aliases of the long-form versions. It is RECOMMENDED to reserve short-form aliases for a subset of flags that are the most useful for human users, such as -h for --help. There should not be too many short-form flags; there is a finite number of one-letter flags you can add to a program, so be wary of polluting this particular namespace.

Normally, flags behave as boolean toggles. Alternatively, flags may take an input value. In Unix, the syntax is --flag value or --flag=value. For flags that accept a value, try to design the flag so that the value is optional – have a sensible default. For some use cases, you might consider allowing a special word like "none" to refer to no value at all. For example, ssh -F accepts an optional filename (an alternative path to ssh_config) but ssh -F none runs SSH with no config file at all.

Follow existing patterns and choose standard and conventional names for flags. Here’s a list of common flags used in Unix programs:

Flag

Abbr.

Description

Examples

--all

-a

All

ps, fetchmail

--debug

-d

Show debugging output

--force

-f

Force a destructive or dangerous operation without confirmation

rm

--json

Display JSON output

--help

-h

Show help text

--no-input

Non-interactive mode

--output

-o

Output file

sort, gcc

--port

-p

Port

psql, ssh

--quiet

-q

Quiet mode (display less output or none at all)

--user

-u

User

ps, ssh

--version

Version

--verbose

Verbose output

Note that -v is varyingly used as an abbreviation for "version" and "verbose". To avoid confusion, it is best to not use this short-form flag at all. Ideally, when this flagged is used, suggest what other flags the user can try instead.

$ my-tool -v
The flag "-v" is not recognized. Did you mean "--version"?

Inputting secrets via arguments and flags

In most environments, programs SHOULD NOT read secrets from arguments or flags such as --password. In Unix, secrets passed as arguments and flags will leak into the output of the ps command, and potentially the shell history too.

Best practice is for CLI programs to accept secrets only via files or stdin.

So, a --password-file flag is better than --password. A --password-file flag, in which only a local file path is passed to the program, is more secure. Passwords are not leaked into the command history, and the user has control over the permissions on the file that stores the password.

(Note, in Unix shells it’s possible to pass a file’s contents into an argument, eg --password $(< password.txt), but this has the same security concerns as typing the password directly into the command.)

It is also safe to prompt users for passwords and other secrets, so programs capture them from stdin. But to support non-interactive environments you SHOULD provide an alternative means for secrets to be inputted non-interactively.

Configuration files

Configuration files can exist at three levels in the user’s filesystem:

  • Localized (directory-scoped)
  • The user level
  • The system level (aka. global)

Configuration files have precedence in that order, from highest to lowest. Configuration files under the user’s home directory take priority over system-wide configurations, and local configuration files – ie. configurations set within the scope of the execution of an instance of the software program – take precedence over both system-wide and user-level configs.

User-level and system-wide configuration

Options that are very likely to stay consistent from one invocation to the next, on the same computer, SHOULD be configurable via centralized user-level or system-wide configuration files.

User-level configurations, which are normally stored in the user’s home directory, tend to be used for things like specifying how color should be used in output, providing non-default paths to dependencies, and configuring HTTP proxy servers to route all requests through – things that are preferences or requirements of individual users.

For both system-wide and user-level configurations on Unix-like systems, it is RECOMMENDED to follow the XDG Base Directory specification. It specifies the location of base directories where config files may be located. A goal of this standard is to limit the proliferation of dotfiles in a user’s home directory by supporting a general-purpose ~/.config folder. The XDG Base Directory specification is supported by a wide range of programs including yarn, fish, wireshark, emacs, neovim, and tmux.

Local configuration

Configurations that are scoped to the execution of a program within a particular directory are called local configurations. Local configuration files include Makefile, package.json, docker-compose.yml, and .env files. For example, the existence of a Makefile in a project directory controls the behavior of make within that directory.

Local configuration files are commonly used to control the behavior of tools used to automate software development processes within the context of a project directory. These files are typically committed to version control systems, so the configurations can be shared and installed consistently in multiple environments.

Typically, local configuration files override environment variables, user-level configurations, and system-wide (global) configurations. The purpose of local configuration files is to be able to share the same configuration of a tool among multiple users and environments, so the tool behaves consistently in different contexts.

External configurations

If your program needs to modify a configuration file that does not belong to your program, it MUST ask for the user’s consent before doing this. But prefer instead to create a new config file (eg. /etc/cron.d/my-app) rather than appending to an existing ones (eg. /etc/crontab).

Environment variables

Environment variables SHOULD be used to vary behavior of commands based on the context in which they are run. In the case of Unix CLI programs, the environment is the terminal session.

For maximum portability, environment variable identifiers MUST contain only upper case ASCII letters, with underscores delimiting words. Numbers MAY be included, but identifiers MUST start with a letter.

It is RECOMMENDED to repurpose standard, general-purpose environment variables where appropriate. The following are useful to know about:

Name

Description

NO_COLOR

Disable colorful output

DEBUG

Enable more verbose output

EDITOR

The user’s preferred program for editing files or inputting multiple lines of text

HTTP(S)_PROXY, ALL_PROXY, NO_PROXY

Check these when performing network operations

SHELL

Open interactive sessions in the user’s preferred shell

TMPDIR

Use this directory to store temporary files

HOME

The user’s home directory, used to locate user-level configuration files

PAGER

The default tool to enable paged output

LINES, COLUMNS

Use these to adjust output based on screen size

For environment variables that are specific to your program, it is RECOMMENDED to prefix the env vars with a unique identifier for your program, to avoid conflicts with other applications. In particular, avoid clashes with POSIX-standard environment variables, which are listed here.

Environment variables have a disadvantage of being limited to one data type: string. Multi-line string values are possible, but programmers should aim for configurations to be composed of online single-line string values. Multi-line strings create interoperability problems (eg. with some .env file parsers) and usability issues (eg. with some env commands).

.env files

Environment files, which are normally named .env, are a convention for declaring environment variables through local configuration files. This convention is widely supported, with most mainstream programming languages having built-in libraries for reading .env files.

Unlike most other types of local configuration, .env files tend not to be committed to version control.

The purpose of .env files is to give users the convenience of being able to set environment variables on a directory-by-directory basis. Where there are valid use cases for adjusting the configuration of a tool on a directory-by-directory basis (eg. project-specific configurations), you SHOULD consider making the tool recognize .env files.

All options that can be set in a .env file MUST be settable via conventional environment variables, too. When a .env file is loaded, its settings SHOULD override actual environment variables with the same identifiers, but only within the scope of the .env file’s directory path.

You MUST NOT use .env files as a substitute for proper configuration files, which are more versatile (eg. supporting more types) and can be stored more securely (eg. outside of version controlled directories).

Inputting secrets via environment variables

We MUST NOT design CLI programs to read secrets from environment variables. This will encourage users to store sensitive information in environment variables, which are prone to leakage, for the following reasons:

  • Exported environment variables are sent to every process, and from there can easily leak into logs.
  • Shell substitutions like curl -H "Authorization: Bearer $BEARER_TOKEN" will leak into globally-readable process state.
  • Docker container environment variables can be viewed by anyone with Docker daemon access, via docker inspect.
  • Environment variables in systemd units are globally readable via systemctl show.

As explained in the section on arguments and flags, best practice is to accept secrets only via files and stdin.

Note

This rule applies to CLI programs that are widely distributed. Environment variables are actually a great way of injecting secrets into applications at runtime, when those applications are deployed to environments that you control.

Subcommands

If you have several tools that are closely related, you can make them easier to use by combining them under a single command. It means you have a single program that can share configuration, storage, flags and arguments, and help text. Therefore, overall complexity is greatly reduced. This is what Git and many other programs do very well.

Be consistent. Use the same arguments and flags across all the subcommands. Have similar output formatting and error handling. Prompt for input in the same way. And so on. Subcommands SHOULD feel like they’re all parts of a cohesive program, rather than standalone programs under a common namespace.

Avoid ambiguous subcommands, and avoid having two subcommands that have similar names. Avoid, for example, having both update and upgrade subcommands. This is quite confusing. Disambiguate with extra words, eg. update-dependencies, upgrade-latest.

In complex programs with lots of objects, and lots of operations that can be performed on each of those objects, it is a common pattern to use two levels of subcommands, where one is a noun (referring to the object) and one is a verb (referring to the operation). You can use the noun verb or the verb noun pattern. The first seems to be the more common pattern — eg .docker container create — but either is fine as long as you are consistent.

Where subcommands are used in combination with flags, the positioning of flags within the subcommand structure SHOULD NOT be significant. By way of example, the following two commands SHOULD be equivalent. However, it is acknowledged that this is not always possible. You may be constrained by the runtime environment or the capabilities of your argument parser.

$ my-tool --flag subcmd
$ my-tool subcmd --flag

It is not recommended to design in default or "catch-all" subcommands. If you have a subcommand that’s likely to be used the most, you might be tempted to let people omit it entirely for brevity’s sake. For example, say you have a run command that wraps an arbitrary shell command:

$ mycmd run echo "hello world"

You could make it so that, if the first argument to mycmd is not the name of a subcommand, you assume the user means run:

mycmd echo "hello world"

This has a serious drawback. Now you can never add a subcommand named echo — or anything at all — without risking breaking existing usages. If there’s a script out there that uses mycmd echo, it will do something entirely different after that user upgrades to the new version of your tool. Oops. It is for this reason that you should avoid default or "catch-all" subcommands. Instead, require all subcommands to be explicitly invoked.

Also, don’t allow arbitrary abbreviations of subcommands. For example, say your tool has an install subcommand. When you added it, you wanted to save users some typing, so you allowed them to type any non-ambiguous prefix, like mycmd ins, or even just mycmd i. Now you’re stuck. You can’t add any more commands beginning with i. There’s nothing in principle wrong with aliases — saving on typing is good — but they should be explicit and remain stable.

Interactivity

Prompts

Never build CLI commands that require the user be prompted for input. It MUST be possible to run CLIs non-interactively. This means it MUST be possible to input all invocation-specific parameters via arguments or flags.

Prompts and other interactive elements MUST be enabled only if stdin is an interactive terminal (a TTY). In Unix-like systems, this is a pretty reliable way to tell whether you’re piping data into a command or whether it’s being run in a script. If the program is not being run in an interactive terminal, prompts MUST NOT be used. Instead, the program MUST return errors when required input parameters are missing.

# Check if stdin (file descriptor 0) is a TTY.
if [ -t 0 ]; then
  echo "Running in a TTY"
else
  echo "Not running in a TTY (redirected or piped)"
fi

Consider designing in the --no-input flag to disable prompts. This MUST force the program to run in non-interactive mode.

In interactive mode, when a user does not pass a required argument or flag, the program SHOULD prompt for the missing input, if possible. In most use cases, this is a better experience for the user than receiving an error.

In interactive mode, always prompt for confirmation before doing anything dangerous or highly destructive. A common convention is to require the user to type y or yes. In non-interactive environments, confirmation SHOULD be done by passing the --force/-f flag. Consider also offering users a --dry-run mode, which will show them the consequences of their action before they commit to it.

When prompting for passwords, don’t print the password as the user types. In Unix-like systems, this is done by turning off echo in the terminal. Most other systems will have helper functions to support this.

Signals

Let users escape from operations by typing Ctrl+C (the INT signal). Exit as soon as possible, and return a confirmation of the exit before you start clean-up.

Remember to add a timeout to any clean-up operation, so it doesn’t hang forever. If the user types Ctrl+C again during the clean-up operation, cancel the clean-up. Design your programs to start in situations where clean-up of prior operations has not been completed.

Piping

In Unix CLIs, if either the input or output is a file, the CLI SHOULD support use of - to read from stdin or write to stdout. This is known as piping. It lets the output of another command be the input to your command, and vice versa, without needing to redirect data through a temporary file.

For example, tar can extract files from stdin, like this:

$ curl https://example.com/something.tar.gz | tar xvf -

If your command is expecting to have something piped to it when stdin is an interactive terminal, and no input is provided, you have two options:

  • Display help text and quit immediately.
  • Or print an error message to stderr.

But don’t do nothing. Don’t let the operation just hang, like cat does.

Exit codes

In Unix-like environments, exit codes are how scripts determine whether a command succeeded or failed. You MUST report these correctly.

Return a zero exit code on success and any non-zero exit code on failure.

Error codes SHOULD be positive integers of 1 or greater. Reserve 1 for a general, undocumented error. Map other errors codes (of 2 or higher) to the various failure modes of your program, and document these.

Thus, there SHOULD be a unique exit code for each type of failed operation in your program. Each of these failure modes MUST be fastidiously documented. One of the reasons to consult documentation is to fix errors. That is why all types of error from a program MUST be documented.

Error messages SHOULD be written to stderr when a command returns a non-zero exit code. See the section on errors for more guidance on how error messages can be used to support debugging of failure modes.

Output

Standard output

In Unix-like environments, the primary output from most CLI programs goes to stdout. For most commands, this should be a human-readable text stream. In most cases, output should be in a human-readable format by default.

Since stdout is what is piped to the next command, it is RECOMMENDED to support flags that allow the user to toggle the output into alternative machine-readable formats. For example, use --plain to support piping into tools such as grep or awk, and --json for use with jq.

If your program or command is expected to be used mostly in automation scenarios, it may make more sense for the default output format to be machine-readable. If so, you can still have your program automatically switch to a human-readable format by checking if the standard output stream is a terminal (TTY).

#!/bin/bash

# Check if stdout (file descriptor 1) is a TTY.
if [ -t 1 ]; then
  echo "stdout is a TTY"
else
  echo "stdout is not a TTY"
fi

In most cases, something SHOULD be sent to stdout on success, even if it is just a simple confirmation message that the operation has finished successfully. Some commands such as cp don’t print anything, which has come to be regarded as bad practice. The user SHOULD be given explicit feedback on all operations, and there should be something to pipe into the next program.

Expect the output of every program to become the input to another, as yet unknown, program.

– Doug McIlroy
cite title

To support commands being used in an automated way via scripts, and to avoid clumsy redirection to /dev/null, provide a --quiet/-q flag to run your program in "quiet mode", which will suppress all non-essential output and help declutter log files.

Pagers

If you are outputting a lot of text, use a pager like less. git diff does this behind the scenes.

Be careful with your implementation. Pagers can cause unexpected behaviors and, implemented badly, can make the user experience worse. You MUST NOT use a pager if stdin or stdout is not an interactive terminal.

A sensible set of options for less is -FIRX, which does not page if the output fits within one screen, leaves the output on screen when the user quits less, ignores case when the user searches the output, and enables colors and formatting.

Formatting

Do make use of color and symbols in your programs' output. But don’t overdo it. If everything is colorful, the benefits are lost. And not everyone likes colorful output.

You SHOULD provide a flag for the user to disable color formatting (--no-color is recommended), and color formatting SHOULD be disabled by default in the following circumstances:

  • When stdout is not an interactive terminal (a TTY).
  • When an environment variable named NO_COLOR exists.
  • When an environment variable named [APP]_NO_COLOR exists, where [APP] is a prefix identifying your program.

Restrained use of symbols and emoji can be beneficial. Characters such as ✅ and ❌ can be useful to draw the user’s attention to successes and failures respectively.

Focus on good formatting over symbols and colors. Work to increase information density while decreasing visual noise and making judicious use of space to improve readability. The output from ls is a good example of this. An awful lot of information is communicated in a very compact way, and consistent patterns in the output allow you to quickly pick out the information you need and ignore the rest.

-rw-r--r-- 1 root root     68 Aug 22 23:20 resolv.conf
lrwxrwxrwx 1 root root     13 Mar 14 20:24 rmt -> /usr/sbin/rmt
drwxr-xr-x 4 root root   4.0K Jul 20 14:51 security
drwxr-xr-x 2 root root   4.0K Jul 20 14:53 selinux
-rw-r----- 1 root shadow  501 Jul 20 14:44 shadow
-rw-r--r-- 1 root root    116 Jul 20 14:43 shells
drwxr-xr-x 2 root root   4.0K Jul 20 14:57 skel
-rw-r--r-- 1 root root      0 Jul 20 14:43 subgid
-rw-r--r-- 1 root root      0 Jul 20 14:43 subuid

Animations

If stdout is not an interactive terminal, do not display any animations. This will stop progress bars turning into Christmas trees in log files.

Errors

Errors SHOULD go to stderr. This means that, when commands are piped together, the error messages will get displayed to the user but the standard output will get piped into the next command.

Error messages MUST be sent to stderr whenever a command returns a non-zero exit code. The error message MUST provide sufficient information to help the user fix the problem identified by the exit code.

Error formatting

Different platforms have different error formatting conventions. It is RECOMMENDED to follow the prevailing conventions of the platform. For example, in Unix shells the classic shape is:

<program-name>: <error message>

Or, when location matters:

<program-name>: <file>:<line>:<column>: <error message>

Keeping the error format predictable matters more than the exact punctuation.

Error message content

A good error message communicates three things: what went wrong, where, and what to do next. For example:

permission denied: cannot write to /etc/foo - try running with sudo or use --output to choose a writable path

is better than:

permission denied

Do not treat stderr like a log file. Don’t print log-level labels here — "ERROR", "WARN", etc. — or any other extraneous contextual information, unless your program was explicitly invoked in verbose mode.

On wording, follow these conventions:

  • Start with lowercase (unless starting with a proper name).
  • No trailing period or exclamation marks.
  • Write one sentence per error so messages compose cleanly when wrapped.

Example of good wording:

can't write to file.txt - you might need to make it writable, try running 'chmod +w file.txt'

Error discovery

If the user enters an invalid command name, the program SHOULD suggest valid commands with similar spellings. This user interface convention was poplularized by Git, and it makes for an excellent user experince. Example:

unknown command "buidl" — did you mean "build"?

Signal-to-noise ratio

Signal-to-noise ratio is crucial. The more irrelevant output you produce, the longer it’s going to take the user to figure out what went wrong. If your program produces multiple errors of the same type, consider grouping them under a single explanatory header instead of printing many similar-looking lines.

Consider where the user will look first. Put the most important information at the end of the error output, not the start.

The eye will be drawn to red text, so use it intentionally and sparingly.

If there is an unexpected or unexplainable error, provide debug and traceback information, and instructions on how to submit a bug. That said, don’t forget about the signal-to-noise ratio. You don’t want to overwhelm the user with information they don’t understand. Not all users are developers! Consider, for example, writing scary-looking stack traces to a debug log, instead of printing it to the terminal. Hide stack traces and internal details behind a --verbose or --debug flag.

Error presentation

It is RECOMMENDED to use only a limited amount of color in stderr output, or no color at all. Prefer to use only indentation and other plain-text formatting to give the output structure. However, it can be beneficial to use some color to draw the user’s attention to important information. The convention for errors is to use red.

When using colors and symbols (such as ❌ for failures), follow these rules:

  • Use color and symbols only when stderr is an interactive terminal (a TTY).
  • Disable colors in the stderr output in these circumstances:
    • When stderr is not an interactive terminal.
    • When an environment variable named NO_COLOR exists.
    • When an environment variable named [APP]_NO_COLOR exists, where [APP] is a prefix identifying your program.
  • Do not emit progress bars, spinners, or other animations to stderr if the output is being piped or logged.

Distinguishing error types

Distinguish user errors (bad flags, missing files) from internal errors ("this shouldn’t happen, please report at …​") so people know whether to fix their command or file a bug.

Exit codes play an important role here. While there is no single canonical standard, the BSD sysexits.h conventions assign meanings to codes 64–78 (EX_USAGE=64, EX_DATAERR=65, EX_NOINPUT=66, etc.). At minimum:

  • 0 for success
  • 1 for general failure
  • 2 for usage errors
  • 130 when interrupted by Ctrl-C (128 + SIGINT)

Scripts depend on exit codes, so be deliberate and document them thoroughly.

Supporting bug reporting

Make it effortless to submit bug reports. Provide a URL where users can report issues, and have the bug submission form pre-populated with as much information as possible.

If your CLI has a website or documentation, consider linking to a stable error code page (for example, https://example.com/errors/E042). This lets you keep the in-terminal message short while still being thorough.

References

The GNU coding standards have more guidance on formatting error messages. The Command Line Interface Guidelines and the Rust and Go style guides offer coherent approaches, too.

Documentation

CLI API signature notation

The POSIX specification defines a notation for describing the APIs — the subcommands, arguments and flags — of command line utilities. Most CLI environments, including Node.js, adopt this notation for documentation purposes.

This notation is perhaps best described using a real example — from Git:

$ git --help
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           [--super-prefix=<path>] [--config-env=<name>=<envvar>]
           <command> [<args>]

Another useful notation is the use of an ellipsis …​ to indicate repetition:

  • <arg>…​: one or more arguments.
  • [--flag <value>]…​: option can be repeated.

A convention – but not part of the POSIX spec – is to use [options…​] to indicate that multiple unspecified options can be passed in.

This notation MUST be used to document CLI APIs, whether the documentation is outputted from --help/-h flags or from man pages, or whether it is written in Markdown or AsciiDoc files, or published online.

Help text

User documentation MUST be built-in to all CLI apps. Help text is triggered through the --help/-h flags.

It is not necessary for help text to document every option in a command’s signature, only the most useful and most widely used options. It is RECOMMENDED that extended user documentation be published online, in which lesser-used commands and options are fully documented, too.

Consider also providing your help texts via man pages. These are UNIX’s original system of documentation, and they’re still in use today. Many users reflexively check man some-tool as a first step when trying to learn a new tool. However, not everyone knows about man, and it doesn’t work on all platforms. For this reason, man pages are an optional extra, and all important documentation MUST be accessible via the tool directly.

If a command or sub-command requires one or more arguments or flags, you SHOULD design the command to return abbreviated help text by default, when no arguments or flags are provided. So my-tool cmd would produce an abbreviated version of the output from my-tool cmd --help and my-tool cmd -h. Obviously, this is not possible for simple commands that do exactly one operation, and for programs that read input interactively (eg. cat).

The abbreviated help text SHOULD include:

  • A description of what the program or command does.
  • The command’s API signature, or one or two usage examples.
  • Descriptions of the most useful flags.
  • Instructions to use the --help/-h flag to get more detailed information.

jq does this well. When you type jq without any options, it shows abbreviated help text, and suggests using jq --help to get the full help text.

$ jq
jq - commandline JSON processor [version 1.6]

Usage:    jq [options] <jq filter> [file...]
    jq [options] --args <jq filter> [strings...]
    jq [options] --jsonargs <jq filter> [JSON_TEXTS...]

jq is a tool for processing JSON inputs, applying the given filter to
its JSON text inputs and producing the filter's results as JSON on
standard output.

The simplest filter is ., which copies jq's input to its output
unmodified (except for formatting, but note that IEEE754 is used
for number representation internally, with all that that implies).

For more advanced filters see the jq(1) manpage ("man jq")
and/or https://stedolan.github.io/jq

Example:

    $ echo '{"foo": 0}' | jq .
    {
        "foo": 0
    }

For a listing of options, use jq --help.

When --help/-h flags are passed, all other flags and arguments SHOULD be ignored, and an extended help text MUST be outputted. This SHOULD include the following:

  • A more detailed description of what the program or command does.
  • The command’s API signature.
  • OPTIONALLY, example invocations of common use cases. Users tend to use examples over other forms of documentation, so it is best to print these early in the help text. If it is useful, include the program’s expected output in the examples, too.
  • Descriptions of all available subcommands.
  • Descriptions of all the available flags. If there are too many of these, list the most useful, and provide a link to online documentation of the rest.
  • OPTIONALLY, a link to an online version of the documentation. If included, this MUST be versioned documentation, corresponding to the installed version of the program.
  • OPTIONALLY, a link to provide feedback, log issues, or request support.

Help texts MAY be formatted. Make headings bold, for example, so the text is easier to scan. But do this only if it can be done in a terminal-independent way, so no user sees escape characters.

The heroku app has some of the best examples of CLI help texts. The whole API is very easily discoverable through the help texts. There’s no color, except some of the less important text is a mid-grey. It mostly uses plain text formatting, with indentation providing structure. Here are a few examples:

$ heroku --help
CLI to interact with Heroku

VERSION
  heroku/7.60.1 linux-x64 node-v14.19.0

USAGE
  $ heroku [COMMAND]

COMMANDS
  access          manage user access to apps
  addons          tools and services for developing, extending, and operating your app
  apps            manage apps on Heroku
  auth            check 2fa status
  authorizations  OAuth authorizations
  autocomplete    display autocomplete installation instructions
  buildpacks      scripts used to compile apps
  certs           a topic for the ssl plugin
  ci              run an application test suite on Heroku
  clients         OAuth clients on the platform
  config          environment variables of apps
  container       Use containers to build and deploy Heroku apps
  domains         custom domains for apps
  drains          forward logs to syslog or HTTPS
  features        add/remove app features
  git             manage local git repository for app
  help            display help for heroku
  keys            add/remove account ssh keys
  labs            add/remove experimental features
  local           run Heroku app locally
  logs            display recent log output
  maintenance     enable/disable access to app
  members         manage organization members
  notifications   display notifications
  orgs            manage organizations
  pg              manage postgresql databases
  pipelines       manage pipelines
  plugins         list installed plugins
  ps              Client tools for Heroku Exec
  psql            open a psql shell to the database
  redis           manage heroku redis instances
  regions         list available regions for deployment
  releases        display the releases for an app
  reviewapps      manage reviewapps in pipelines
  run             run a one-off process inside a Heroku dyno
  sessions        OAuth sessions
  spaces          manage heroku private spaces
  status          status of the Heroku platform
  teams           manage teams
  update          update the Heroku CLI
  webhooks        list webhooks on an app
$ heroku apps --help
list your apps

USAGE
  $ heroku apps

OPTIONS
  -A, --all          include apps in all teams
  -p, --personal     list apps in personal account when a default team is set
  -s, --space=space  filter by space
  -t, --team=team    team to use
  --json             output in json format

EXAMPLES
  $ heroku apps
  === My Apps
  example
  example2

  === Collaborated Apps
  theirapp   other@owner.name

COMMANDS
  apps:create     creates a new app
  apps:destroy    permanently destroy an app
  apps:errors     view app errors
  apps:favorites  list favorited apps
  apps:info       show detailed app information
  apps:join       add yourself to a team app
  apps:leave      remove yourself from a team app
  apps:lock       prevent team members from joining an app
  apps:open       open the app in a web browser
  apps:rename     rename an app
  apps:stacks     show the list of available stacks
  apps:transfer   transfer applications to another user or team
  apps:unlock     unlock an app so any team member can join
$ heroku logs --help
display recent log output

USAGE
  $ heroku logs

OPTIONS
  -a, --app=app        (required) app to run command against
  -d, --dyno=dyno      only show output from this dyno type (such as "web" or "worker")
  -n, --num=num        number of lines to display
  -r, --remote=remote  git remote of app to use
  -s, --source=source  only show output from this source (such as "app" or "heroku")
  -t, --tail           continually stream logs
  --force-colors       force use of colors (even on non-tty output)

DESCRIPTION
  disable colors with --no-color, HEROKU_LOGS_COLOR=0, or HEROKU_COLOR=0

EXAMPLES
  $ heroku logs --app=my-app
  $ heroku logs --num=50
  $ heroku logs --dyno=web --app=my-app
  $ heroku logs --app=my-app --tail

References

  • Command Line Interface Guidelines. — A free, open source, online guide to writing better command line programs, taking traditional Unix principles and updating them for the modern day.
  • Stallman, R et al. GNU Coding Standards. Free Software Foundation. — Coding guidelines for GNU projects. It focuses on writing programs in C, but much of this covers the command line interface and many of the core guidelines are applicable to all CLIs.
  • Tashian, C. The Poetics of CLI Command Names. Smallstep. — Sound advice on naming CLI commands.
  • Starich, J (2019). User Experience, CLIs, and Breaking the World. UX Collective. — Lessons learnt from revamping the CLI for the IBM Cloud Kubernetes Service.
  • Heroku. Heroku CLI Style Guide. — Design guidelines for CLI plugins for the Heroku platform.
  • Dickey, J (2018). 12 Factor CLI Apps. Medium. — Adapts Heroku’s 12 factor app principles to CLI programs. These design principles were codified in Oclif, a framework for CLI apps running in Node.
  • The Open Group. Utility Conventions. POSIX. — Describes the argument syntax of standard UNIX utilities, and defines a notation for documenting utility arguments.