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.
Terminology
This standard uses several terms – terminal, shell, tty, console, and terminal emulator – that are closely related and often used loosely as synonyms. They are not the same thing, and the distinction matters when reasoning about I/O, signals, and environment detection.
Shell. The command-line interpreter a user interacts with directly – Bash,
Zsh, cmd.exe, PowerShell, etc. Its primary purpose is to read commands and
start other programs. A CLI program is normally invoked from a shell, but is
not itself a shell.
Terminal, in the Unix sense, and tty. A text input/output environment: the
thing that reads keystrokes and displays output. In Unix terminology,
"terminal" is synonymous with tty – a particular kind of device file
supporting a set of control operations (ioctl\s) beyond plain reads and
writes. Some ttys are backed directly by a hardware device or a kernel-level
virtual console; others, called pseudo-ttys (ptys), are provided through a
thin kernel layer by a terminal emulator.
Terminal emulator. A program that provides a pseudo-tty and renders it as a
window or a remote session – for example Xterm, screen, tmux, or an SSH
client connecting a local terminal to programs running on a remote machine.
"Terminal," in its everyday sense, most often refers to this kind of program:
a window with a keyboard and a display, standing in for the physical
teleprinters the terminology originally described.
Console. A terminal in the physical sense – conventionally, the primary terminal directly connected to a machine. It appears to the operating system as a tty like any other, so the distinction from "terminal" is about physical/primary status, not about the underlying mechanism.
A CLI program’s own I/O model – reading from standard input, writing to
standard output and standard error, detecting whether it is attached to a
tty (see Piping) – is the same regardless of which of
these sits behind it. The terms matter mainly for correctly describing
where a behavior applies: a NO_COLOR-style check is a tty check on the
process’s streams, not a check for any particular shell or terminal
emulator.
Simplicity
Above all, robustness is achieved by keeping the program simple. Complex code and special-case handling make a program fragile and unpredictable: every branch is a place where an assumption can turn out to be wrong, and every edge case handled is an edge case that must be maintained. A CLI with fewer moving parts is easier to reason about, easier to test exhaustively, and less likely to surprise its users.
Favor a small number of well-understood behaviors over a large number of special-cased ones. Where a feature would only handle a rare combination of inputs, consider whether the CLI is better off rejecting that combination outright, with a clear error, than trying to handle it correctly.
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.
Prefer explicit actions
Reading or writing files that the user did not explicitly pass as arguments, and talking to a remote server – for example, to check for updates or download a file – SHOULD usually be an explicit, visible action. Don’t surprise the user with side effects they didn’t ask for.
The exception is storing internal program state, such as a cache or a telemetry queue (see Analytics). Users don’t need to be told every time a program reads its own cache directory.
Analytics
CLI programs MUST NOT phone home usage or crash data without the user’s consent. Where a program does collect telemetry, be explicit with users about what is collected, why it is collected, how it is anonymized, and how long it is retained.
Prefer an opt-in model. Where telemetry is opt-out instead, tell users clearly
that data is being collected and make it easy to disable – for example, via a
--no-analytics flag or a [APP]_NO_ANALYTICS environment variable,
mirroring the NO_COLOR convention (see Formatting).
Consider alternatives to in-program telemetry before reaching for it: download counts from your package registry or website analytics on your documentation site often answer the same questions without instrumenting the CLI itself. And talking to users directly – support channels, issue trackers, user interviews – surfaces the "why", which telemetry cannot.
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.
Responsiveness also covers cold-start time – how long it takes the program to begin doing useful work after invocation, before any of its own feedback can be shown. As a rough guide: under 100ms feels instant; 100–500ms is a reasonable target for most CLIs; 500ms–2s is usable but noticeable; beyond 2s, users start to avoid the tool, scripting around it or reaching for alternatives. Startup time matters most for CLIs invoked frequently in tight loops – shell prompts, git hooks, editor integrations – where even a few hundred milliseconds compounds quickly.
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.
Use Semantic Versioning for CLI programs. Reserve major version bumps for breaking changes to the interface – removed or renamed subcommands, flags, or environment variables; changed defaults; changed exit codes – so that users and downstream scripts can tell from the version number alone whether upgrading is safe.
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. A good deprecation warning does three things: tells the user, at the moment they use the deprecated flag or subcommand, that it is going to change or be removed; shows them the future-proof alternative to switch to; and, where possible, detects when they have already updated their usage elsewhere and stops showing the warning, so it doesn’t nag users who have nothing left to fix.
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. Don’t build in a blocking call to a third-party analytics service, for example – if that service is ever discontinued or blocks the request, your program should not hang or fail because of it.
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". The exception is where the name is both literal and
niche enough that no other meaning could be confused for it, eg.
mkfs. Conversely, a deliberately meaningless but easy-to-type name (eg.emacs) is a sound choice when the tool’s solution domain is expected to evolve over time – naming it after today’s domain would only date it. - Don’t put emoji in a command name. It’s technically possible, but it defeats memorability and typeability, which matter more for a command name than for output (see Formatting).
- Don’t suffix a command name with a version number, eg.
python3.7m. A version-suffixed name is usually a sign that the ecosystem has failed to support multiple coexisting versions of the tool cleanly.
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.
Consistency across programs
Naming conventions, common flags, and interaction patterns are not only about
consistency within your own program – they are also about consistency across
programs. Where possible, follow patterns that already exist elsewhere in the
CLI ecosystem your users work in. That’s a large part of what makes CLIs
intuitive and guessable: a user who already knows --force and --dry-run
from other tools shouldn’t have to relearn the concept for yours.
This isn’t absolute. Where following an existing convention would compromise the usability of your specific program, it may be time to break with it – but do so deliberately, and be prepared to explain the deviation, since every departure from convention is a small tax on users who bring expectations from elsewhere.
Distribution
CLI programs MUST be easy to uninstall. Where uninstall requires its own instructions, put them at the bottom of the install instructions – right after installing is one of the most common times a user wants to know how to remove something, whether because they installed the wrong tool or are comparing alternatives.
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.
A single self-contained binary is the distribution ideal: it has no runtime dependencies to install separately, and it can be removed by deleting one file. For languages that don’t compile to a native binary, use a bundler (eg. PyInstaller for Python) to package the interpreter and the program together into something that behaves like a single binary for installation and removal purposes.
The exception is a language-specific tool – a linter or formatter for a
particular language, for example – where the user can safely be assumed to
already have that language’s interpreter or runtime installed. Such a tool
MAY instead be distributed through the language’s own package manager (eg.
npm, pip), since that is where its users already look for tools in that
ecosystem.
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.
You can’t always predict how your program will actually be used. ls was
originally designed for terse, scriptable output, but in practice it is most
commonly run interactively as ls -lhF. Where you have evidence – from
support requests, from how your own team uses the tool, from documentation
that always shows a command with the same flags – let that evidence override
your assumptions about what the default should be.
It is also acceptable to have a group of options that can never have their defaults adjusted, in order to keep some behavior consistent across every environment the program runs in. Not every option needs to be configurable; some are part of the contract the program makes with its users and callers, and making them adjustable would undermine that.
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.
Use a command-line argument-parsing library – whether the language’s built-in
one or a well-established third-party one – rather than hand-rolling argument
parsing. A good library handles flags, abbreviated and combined short flags,
--flag=value and --flag value forms, help-text generation, and "did you
mean" spelling suggestions consistently, which is easy to get subtly wrong by
hand and tedious to re-implement in every program.
By convention, arguments given as ordinary file-path parameters SHOULD be
treated as input files only. Where a command produces an output file, that
output SHOULD be specified via an option – preferably -o/--output – rather
than another bare argument. Even where accepting an output file as an argument
is kept for compatibility with an existing convention, an equivalent option
SHOULD also be provided, so scripts can be explicit about which argument is
which.
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 | |
|
| Describe what changes the command would make, without making them |
|
|
| 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.
Where a version shorthand is offered, prefer -V (uppercase) instead, which
avoids the ambiguity entirely.
$ my-tool -v The flag "-v" is not recognized. Did you mean "--version"?
--version output
The --version flag SHOULD produce a predictable, parseable format: a first
line giving the program’s canonical name – a constant string, not derived
from argv[0], so it reads the same regardless of how the program was
invoked – followed by the version number, then optionally a copyright line
and a license/no-warranty line. For example:
$ my-tool --version my-tool 2.4.1 Copyright (C) 2026 Example Org
--version is also a reasonable place to print additional debugging
information (build metadata, platform, dependency versions), and – for a
program that makes HTTP requests – to source the string sent as the
User-Agent header, which helps with server-side debugging of client
issues. Version SHOULD also be reachable via a version subcommand, in
addition to --version/-V.
As with --help (see Help text), once --version is seen, all other
options and arguments SHOULD be ignored, and the program SHOULD exit
successfully after printing the version.
The -- end-of-options delimiter
Support the bare -- delimiter to mark the end of options. Everything after
it is treated as a positional argument, even if it looks like a flag. This is
the standard POSIX convention, and it exists for two reasons.
First, it lets a value that begins with a hyphen be passed unambiguously, eg.
grep — -foo file.txt searches for the literal string -foo rather than
trying to parse it as a flag.
Second, it lets your program pass its remaining arguments through to a subprocess without your own flag parser trying to interpret them. For example:
$ heroku run -a myapp -- myscript.sh -a arg1
Here, -a myapp is consumed by heroku run, and everything after -- –
including its own -a arg1 – is passed through untouched to myscript.sh.
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.
The XDG Base Directory specification also covers data and cache files, not
just configuration. Use ~/.local/share/<app> for data files your program
needs to persist (eg. a local database, downloaded assets) and
~/.cache/<app> for files that are safe to delete at any time without loss of
functionality (eg. a rebuildable index). On platforms that don’t follow the
XDG specification, use the platform’s conventional equivalents instead – for
example, ~/Library/Caches/<app> on macOS and %LOCALAPPDATA%\<app> on
Windows.
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.
It is RECOMMENDED to allow individual users to provide a custom file name or
path for the local configuration file, via an optional flag and/or an
environment variable (eg. --config <path> or [APP]_CONFIG). Teams
sometimes need to maintain multiple configuration profiles within the same
project directory, and a hardcoded file name prevents that.
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 |
| Check these before emitting terminal-specific escape sequences |
SHELL names the user’s preferred shell for opening an interactive session
– for example, when your program drops the user into a subshell. If instead
you need to execute a shell script or a one-off shell command, use a specific,
predictable interpreter such as /bin/sh rather than $SHELL. The user’s
preferred interactive shell may not be POSIX-compatible, and scripting against
it makes your program’s behavior depend on the user’s personal environment.
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 credential files, pipes (stdin), AF_UNIX sockets, or a
dedicated secret-management service or other IPC mechanism.
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.
The choice has a maintenance dimension worth considering as your command set
grows. If every noun will always support the same set of verbs, verb noun
scales easily – you add a new verb once, and it applies uniformly. If
different nouns support very different verbs, noun verb (grouping every
verb under its noun) tends to be lower-maintenance, since each noun’s verb
set can evolve independently without the top-level verb list becoming a
grab-bag of noun-specific operations.
Where subcommands are given a second level using a namespace-like delimiter
rather than a space – eg. heroku domains:add instead of heroku domains
add – the colon form lets the topic command (domains) itself accept
arguments while still having subcommands, something a purely space-delimited
parser cannot disambiguate. Whichever delimiter you choose, never create a
*:list-style command purely to list the nouns under a topic – the topic
root itself (heroku domains, with no subcommand) should list them.
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.
When a subcommand rename or restructuring is unavoidable, consider going beyond a deprecation warning and shipping an automated migration command that rewrites a user’s own shell scripts to use the new subcommand names. This is more work than a warning, but for a widely-scripted CLI it removes the manual-update burden from every downstream user at once, rather than leaving each of them to find and fix their own scripts.
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.
Not every dangerous action deserves the same level of friction. Grade the confirmation to the severity of what’s about to happen:
- Mild. A small, local change – for example, deleting one file the user named explicitly. This MAY need no prompt at all, since the user’s own command already made their intent explicit.
- Moderate. Deleting a directory, a change to a remote system, or a bulk
modification that can’t be easily undone. This SHOULD prompt for
confirmation by default, and offering a
--dry-runfirst is especially valuable here. - Severe. Deleting something complex and hard to recreate – an entire
remote application or environment, for example. Make confirmation hard to
trigger by accident: ask the user to type something non-trivial, such as
the name of the thing being deleted, rather than a bare
y/yes. Still allow this to be scripted, via a flag that supplies the same value non-interactively (eg.--confirm=<name>), so severe operations remain usable in automation without weakening the safeguard for humans.
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.
For choosing between several options, consider a richer interactive selection UI – an arrow-key list, checkboxes, or radio buttons – rather than asking the user to type a value exactly. Presenting the choices visually removes typos and the need to remember exact option spellings. As with any prompt, this MUST be gated on stdin being a TTY (see above), with an equivalent flag-based way to supply the same choice non-interactively.
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. Make the exit path obvious – the user should never be left wondering
how to get out of the program. Don’t trap the user in a mode with no visible
way back to the shell, the way some full-screen terminal programs do.
For a program that wraps execution of something else – an SSH session, a
tmux client, a telnet session – Ctrl+C may be consumed by the wrapped
program instead of reaching your CLI. Make the escape route clear in these
cases, following the precedent of SSH’s ~ escape sequences (eg. ~. to
terminate the connection), and document it in the program’s help text.
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.
Where the second Ctrl+C would trigger something destructive – Docker
Compose, for example, forces its containers to stop immediately on a second
Ctrl+C rather than waiting for a graceful shutdown – tell the user what will
happen if they press it again, at the moment clean-up begins. Don’t let a
destructive shortcut be a surprise.
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.
Human-readable output is not a stable interface, and it is fine to iterate on
its wording and layout between releases. Machine-readable output
(--plain/--json) is different: once a script depends on it, changing its
shape is a breaking change (see Future proofing). Encourage users to build
scripts against --plain/--json rather than parsing the human-readable
default, and treat the human-readable format as free to evolve.
Keep human-readable output grep-parseable: aim for one record per line, so a
user can filter it with grep and count matches with wc without your output
format getting in the way. Avoid multi-section, grouped-header formats – a
single tabular row per item, as described in Tables, is easier to filter
than output broken up under several headings.
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.
Tables
When output is naturally tabular – a list of resources, for example – emit one
record per row, so the output stays grep-parseable (see Standard output).
Never emit table borders or box-drawing characters in the default output;
they add visual noise and break grep/awk filtering.
It is RECOMMENDED to support conventional flags for tabular output:
--columns– choose which columns to display.--no-truncate– don’t truncate column values to fit the terminal width.--no-headers– omit the header row, useful for scripting.--filter– filter rows by a field value.--sort– sort rows by a field.--csv– emit CSV instead of the default aligned-column format.
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. - When the
TERMenvironment variable is set todumb, which signals a terminal with no color or escape-sequence support.
Conversely, it is RECOMMENDED to support FORCE_COLOR as the counterpart to
NO_COLOR: when set, it forces color output on even where TTY detection would
otherwise disable it – for example, when output is being piped into another
program that itself renders color, such as a pager.
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.
Progress bars, spinners, and other "action" output are out-of-band information
about what the program is doing, not the program’s actual result. Send this
output to stderr, not stdout, so that stdout can still be redirected or piped
while the user continues to see progress. curl follows this convention –
its progress meter goes to stderr, leaving stdout free for the downloaded
content.
If a progress bar gets stuck in the same place for a long time, the user cannot tell whether work is still happening or the program has crashed or hung. Show an estimated time remaining, or keep some part of the display animated (a spinner character, a pulsing bar), so the user has continuous evidence that the program is still alive.
Reporting progress for work done in parallel is harder than for sequential work: naively interleaved output from multiple concurrent tasks is confusing and hard to read. Use a library designed for parallel progress reporting where one is available, rather than writing raw progress lines from multiple workers directly to the terminal.
Hiding detailed logs behind a progress bar or spinner, while things are going well, keeps output easy to follow. But if an error occurs, print the hidden logs – otherwise the failure is very hard to debug, because the information the user needs was suppressed exactly when they needed it most.
For a task that runs long enough that the user is likely to switch away to another window, consider triggering an OS-level desktop notification when it completes, in addition to the terminal output.
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.
Interactive programs – ones that read commands from a terminal in a loop,
such as a REPL – SHOULD NOT include the program name in error messages;
identity is already conveyed by the prompt or the surrounding screen layout,
so repeating it is redundant. The same program, when it reads from a
non-terminal (a script feeding it commands, for example), SHOULD switch to
the noninteractive <program-name>: <message> style described above, since
there is no prompt to carry that context.
For tools that report source locations – linters, compilers, formatters –
error positions SHOULD include both column and line numbers, calculated with
tab stops every 8 columns and assuming equal-width ASCII characters, or the
correct Unicode character widths in UTF-8 locales. Where an error spans a
range rather than a single point, use a start-and-end position format such as
file:line1.column1-line2.column2, or, for a span across multiple files,
file1:line1.column1-file2:line2.column2.
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
For errors that are complex enough to warrant it, structure the message around four parts: an error code, a short error title, an optional longer description, and how to fix it – linking to the stable error-code page (see Supporting bug reporting) for more detail rather than dumping everything into the terminal.
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.
Where a program does write an error log file for post-mortem debugging, each entry MUST include a timestamp, and the file MUST be truncated or rotated occasionally so it does not grow without bound. Log files MUST NOT contain ANSI color codes – they are written for later reading with tools that don’t render them, and the stray escape sequences make the file harder to grep.
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, or an equivalent DEBUG environment variable – the
environment variable is convenient for wrapping a command in a script or CI
job without having to change the invocation itself.
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.
Use the | notation to indicate mutually-exclusive options, eg.
[-p | --paginate | -P | --no-pager] from the Git example above means exactly
one of these four options may be given, not several at once. Where a command
has multiple mutually-exclusive sets of arguments – for example, a subcommand
that accepts either a name or an ID, but combines with different additional
flags depending on which – show each set on its own synopsis line, rather than
trying to express the whole combination with | and [] alone.
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. Common options
that behave the same way across every command – --version and --help
themselves, for example – can be documented once, elsewhere, with help text
simply linking to the "full documentation" rather than repeating their
descriptions in every command’s output. It is RECOMMENDED
that extended user documentation be published online, in which lesser-used
commands and options are fully documented, too. For public applications, this
online documentation MUST be indexable by public search engines, so users can
find it without already knowing the tool’s name.
Built-in documentation, accessible directly from the tool, has two advantages over online documentation: it always matches the installed version exactly – there’s no risk of reading docs for a newer or older release than the one in front of you – and it works offline, which matters for tools used in restricted or air-gapped environments.
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. Tools such as help2man and ronn can
generate man pages directly from a command’s --help output, which is a much
lower-effort path than writing and maintaining man pages by hand.
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. Fit descriptions within an 80-character width, begin with lowercase, and do not end with a period – the same wording conventions as Error message content.
- 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.
In addition to --help/-h flags, consider offering a help subcommand –
myapp help for the top-level help text, and myapp help subcommand
equivalent to myapp subcommand --help. This pattern was popularized by Git
and npm (npm help ls is the same as man npm-ls), and some users reach for
it instinctively before trying a flag.
For CLIs with a very large number of commands, listing every command in a
single flat help text becomes a wall of text nobody can scan. Prefer
structured help: show only the immediate subcommands of whatever the user
just typed, rather than the whole tree at once, and group related commands
visually under headings – the heroku --help example above already does
this, listing top-level topics rather than every heroku apps:* subcommand.
Shell completion
Provide shell completion scripts for the shells your users are likely to use
(bash, zsh, fish, PowerShell). Completion is one of the most effective aids to
discoverability and correct usage: typing --app <tab><tab> and seeing the
valid values removes the guesswork of remembering flag names and value
formats, and it catches typos before the command is ever run. Document how to
install the completion script as part of your installation instructions.
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.
- Unix & Linux Stack Exchange (2010). What is the exact difference between a "terminal", a "shell", a "tty" and a "console"?. — The source for Terminology.