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
cpare arguments. Their order is significant:cp foo baris not equivalent tocp 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 |
|
|
| Show debugging output | |
|
| Force a destructive or dangerous operation without confirmation |
|
| Display JSON output | ||
|
| Show help text | |
| Non-interactive mode | ||
|
| Output file |
|
|
| Port |
|
|
| Quiet mode (display less output or none at all) | |
|
| User |
|
| Version | ||
| 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 |
|---|---|
| Disable colorful output |
| Enable more verbose output |
| The user’s preferred program for editing files or inputting multiple lines of text |
| Check these when performing network operations |
| Open interactive sessions in the user’s preferred shell |
| Use this directory to store temporary files |
| The user’s home directory, used to locate user-level configuration files |
| The default tool to enable paged output |
| 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_COLORexists. - When an environment variable named
[APP]_NO_COLORexists, 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_COLORexists. - When an environment variable named
[APP]_NO_COLORexists, 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/-hflag 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.