Aquefir

Massively multipurpose software.

ADP6: Command Line Interface Guidelines

This is a collection of practises to make better command-line programs. The first section contains the essentials, while later items are more nice-to-have. The further down this list a program’s design makes it, the better it should be. These guidelines make it practical to avoid thinking too hard about program design.

These rules are set in stone, not cast in iron. Real-world judgment and context is required for excellency, and this document is no substitute for that. Proposals are welcome, however, for changes.

To improve this protocol’s usability, all rules and sections are hierarchically numbered and priorities are applied to each rule, with an easy colour coding scheme: a red diamond signals a crucial rule, a yellow circle signals a nice-to-have rule, and a blue star signals an ‘extra’ rule.

1. Basics

1. Return zero exit code on success, non-zero on failure. Exit codes are how scripts determine whether a program succeeded or failed, so consistency is key with this. Also map the non-zero exit codes to the most important failure modes.

2. Send output to stdout. The primary output of a command should go to stdout. Anything that is machine readable should also go to stdout — this is where piping sends things by default.

3. Send messaging to stderr. Log messages, errors, and so on should all be sent to stderr. This means that when commands are piped together, these messages are displayed to the user, not fed into the next command.

4. Use a command-line argument parsing library. They will handle the parsing of arguments and flags, displaying help text, and even spelling suggestions in a sensible way.

Here are some to try for various languages:

Help

1. Display help text when passed no options, the -h flag, or the --help flag.

2. Ignore any other flags and arguments that are passed when -h or --help is passed. it should be possible to append -h to the end of any invocation and the program show help.

If the program’s invocation structure permits, the following should also offer help:

$ myfoo help
$ myfoo help subcommand
$ myfoo subcommand --help
$ myfoo subcommand -h

3. Show full help when -h and --help is passed. All of these should show help:

$ myfoo
$ myfoo --help
$ myfoo -h

4. Provide a support path for feedback and issues. A website or GitHub link in the top-level help text is common.

5. In help text, link to the manpage and/or the online documentation. If there is a specific page or anchor for a subcommand, link directly to that. This is particularly useful for more complex programs that demand further reading.

6. Use formatting in help text. Bold headings make it much easier to scan. Occasional use of colour can improve skimming potential too. However, if help text is piped through to another program, do not emit formatting codes.

7. Lead with examples. Users tend to use examples over other forms of documentation, so show them first in the help page, particularly the common complex uses. If it helps explain what it is doing and it is not too long, show the actual output too.

8. Display a concise help text by default. If possible, display help by default when the program is run with no arguments. Unless the program is very simple and does something obvious by default (e.g. ls), or the program reads input interactively (e.g. cat).

The concise help text should only include:

jq does this well. Upon invocation, it displays an introductory description and an example, then prompts the user to run jq --help for the full listing of flags:

$ jq
jq - commandline JSON processor [version 1.6]

Usage:    jq [options]  [file...]
    jq [options] --args  [strings...]
    jq [options] --jsonargs  [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.

9. Display the most common flags and commands at the start of the help text. It is fine to have lots of flags, but if some are really basic and essential, display them first. For example, the git command displays the commands for getting started and the most commonly used subcommands first:

$ git
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>]
           <command> [<args>]

These are common Git commands used in various situations:

start a working area (see also: git help tutorial)
   clone             Clone a repository into a new directory
   init              Create an empty Git repository or reinitialize an existing one

work on the current change (see also: git help everyday)
   add               Add file contents to the index
   mv                Move or rename a file, a directory, or a symlink
   restore           Restore working tree files
   rm                Remove files from the working tree and from the index
   sparse-checkout   Initialize and modify the sparse-checkout

examine the history and state (see also: git help revisions)
   bisect            Use binary search to find the commit that introduced a bug
   diff              Show changes between commits, commit and working tree, etc
   grep              Print lines matching a pattern
   log               Show commit logs
   show              Show various types of objects
   status            Show the working tree status

grow, mark and tweak your common history
   branch            List, create, or delete branches
   commit            Record changes to the repository
   merge             Join two or more development histories together
   rebase            Reapply commits on top of another base tip
   reset             Reset current HEAD to the specified state
   switch            Switch branches
   tag               Create, list, delete or verify a tag object signed with GPG

collaborate (see also: git help workflows)
   fetch             Download objects and refs from another repository
   pull              Fetch from and integrate with another repository or a local branch
   push              Update remote refs along with associated objects

'git help -a' and 'git help -g' list available subcommands and some
concept guides. See 'git help <command>' or 'git help <concept>'
to read about a specific subcommand or concept.
See 'git help git' for an overview of the system.

3. Output

1. Human-readable output is paramount. The most simple and straightforward heuristic for whether a particular output stream is being read by a human is whether or not it is a TTY. Whatever the language, it should have a utility library for discerning this.

2. Have machine-readable output where it does not impact usability. The text stream is the universal interface in UNIX. Programs typically output lines of text, and programs typically expect lines of text as input, therefore programs can be composed together. This is called scripting.

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

3. If human-readable output breaks machine-readable output, provide an option for --plain output. It is important to provide a tabular text format for integration with tools like grep or awk.

4. Disable color if the program is not in a terminal or the user requested it. These things should disable colors:

It may also be good to add a {MYFOO}_NO_COLOR environment variable in case users want to disable color specifically for that program.

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

6. Actions crossing the boundary between the program’s internal world and the greater system should be mentioned explicity. This includes things like:

By default, do not print information that is only meaningful to the developers of the software. If some output is only useful to a developer, it is not a good idea to print it out for users. However, it is helpful to provide a -v or --verbose flag to print such information.

7. Use a pager (e.g. less) if the program is outputting a lot of text. For example, git diff does this by default. That said, using a pager can be error-prone, so be careful with implementation. Pagers should not be used if standard IO is not a TTY.

A sensible set of options to use for less is less -FIRX. This does not page if the content fills one screen, ignores case when searching, enables color and formatting, and leaves the contents on the screen when less quits.

8. Display output on success, but keep it brief. When nothing is wrong, UNIX commands tend to display no output to the user. This makes sense when they are being used in scripts, but can make commands appear to be hanging or broken when used by humans. For example, cp will not print anything, even if it takes a long time.

It is rare that printing nothing at all is the best default behavior, but it is usually best to err on the side of less.

9. If system state is changed, say so. When a command changes the state of a system, it is very valuable to explain what has just happened, so the user can model the state of the system in their head — especially if the result does not directly map to what the user requested.

For example, git push says exactly what it is doing, and what the new state of the remote branch is:

$ git push
Enumerating objects: 18, done.
Counting objects: 100% (18/18), done.
Delta compression using up to 8 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (10/10), 2.09 KiB | 2.09 MiB/s, done.
Total 10 (delta 8), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (8/8), completed with 8 local objects.
To github.com:replicate/replicate.git
 + 6c22c90...a2a5217 bfirsh/fix-delete -> bfirsh/fix-delete

10. Make it easy to see the current state of the system. If the program does a lot of complex state changes and it is not immediately visible in the filesystem, make sure this is easy to view.

For example, git status gives as much information as possible about the current state of a Git repository, and some hints at how to modify the state:

$ git status
On branch bfirsh/fix-delete
Your branch is up to date with 'origin/bfirsh/fix-delete'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   cli/pkg/cli/rm.go

no changes added to commit (use "git add" and/or "git commit -a")

11. Increase information density — with ASCII art! For example, ls shows permissions in a scannable way. When a user first sees it, they can ignore most of the information. Then, as they learn how it works, they pick out patterns more easily.

-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

12. Use color with intention. For example, it may be necessary to highlight some text so the user notices it, or to indicate an error state. Do not overdo it. If everything is a different color, then the color means nothing and only makes it harder to read.

13. Display output formatted as JSON if --json is passed. JSON allows for more structure than plain text, so it makes it much easier to output and handle complex data structures. jq is a common command-line tool for working with JSON, and there is now a whole ecosystem of tools that output and manipulate JSON.

14. Suggest commands the user may wish to run. When several commands form a workflow, suggesting commands to the user to run next aids in learning how the program works. For example, in the git status output above, it suggests commands for modifying the state shown.

4. Errors

1. Catch errors and rewrite them for humans. If an error can be expected, catch it and rewrite the error message to be useful. Think of it like a conversation, where the user has done something wrong and the program is guiding them in the right direction. Example: “Cannot write to file.txt. It may need to be made writable by running ‘chmod +w file.txt’.”

2. Consider where the user will look first. Put the most important information at the end of the output. The eye will be drawn to red text, so use it intentionally and sparingly.

3. If there is an unexpected or unexplainable error, provide debug and traceback information, and instructions on how to submit a bug. That said, do not overwhelm the user with information they may not understand. Consider writing the debug log to a file instead of printing it to the terminal.

4. Signal-to-noise ratio is important. The more irrelevant output produced, the longer it is going to take the user to figure out what they did wrong. If a program produces multiple errors of the same type, consider grouping them under a single explanatory header instead of printing many similar-looking lines.

5. Make it easy to submit bug reports. One idea is providing a URL and have it pre-populate as much information as possible.

5. Arguments and flags

Note
First, a bit on terminology:

Arguments, or args, are positional parameters to a command. For example, the file paths provided to cp are args. The order of args is often important: cp foo bar means something different from cp bar foo.

Flags are named parameters, denoted with either a hyphen and a single-letter name (-r) or a double hyphen and a multiple-letter name (--recursive). They may or may not also include a user-specified value (e.g. --file foo.txt, or --file=foo.txt). The order of flags should not affect program semantics.

1. Use multiple arguments for simple actions on multiple files. For example: rm file1.txt file2.txt file3.txt. This also makes it work with globbing: rm *.txt.

2. Make the default the right thing for most users. Making things configurable is good, but most users are not going to find the right flag and remember to use it all the time (or alias it). Good defaults make a better user experience.

3. Never require a prompt. Always provide a way of passing input with flags or arguments. If stdin is not an interactive terminal, skip prompting and just require those flags/args.

4. Confirm before doing anything dangerous. A common convention is to prompt for the user to type y or yes if running interactively, or requiring them to pass -f or --force otherwise.

“Dangerous” is a subjective term, but generally there are three levels of danger to consider:

5. If input or output is a file, support - to read from stdin or write to stdout. This lets the output of a command be the input of another command and vice versa, without using a temporary file.

6. Allow using -- to stop the interpretation of arguments as flags. This is crucial for making it possible to operate on files with names beginning with a hyphen-minus -.

7. Allow sensitive argument values to be passed in by file. For example, a raw --password flag can leak the secret into ps output and shell history. Consider allowing secrets by file, e.g. with a --password-file flag.

8. Make arguments, flags and subcommands order-independent. A lot of CLIs with subcommands have unspoken rules on argument placement. For example, a command might have a --foo flag that only works before the subcommand:

$ mycmd --foo=1 subcmd
works

$ mycmd subcmd --foo=1
unknown flag: --foo

This can be very confusing for the user — especially given that one of the most common things users do when trying to get a command to work is to hit the up arrow to get the last invocation, stick another option on the end, and run it again. If possible, try to make both forms equivalent, although this may stretch argument parser libraries beyond their limits.

9. If a flag can accept an optional value, allow a special word like “none.” For example, ssh -F takes an optional filename of an alternative ssh_config file, and ssh -F none runs SSH with no config file. Do not use a blank value to accomplish this — this can make it ambiguous.

10. Have full-length versions of all flags. For example, have both -h and --help. Having the full version is useful in scripts where verbosity is valued and the cost of documentation lookups for comprehension is higher.

Prefer flags to args. While somewhat more verbose, it makes it clearer what is going on. It also makes it easier to make changes to how input can be accepted in the future. Sometimes when using args, it is impossible to add new input without breaking existing behavior or creating ambiguity.

11. Adhere to de facto standard names for flags, if applicable. If another commonly used command uses a certain flag name, it is best to follow that existing pattern. That way, a user does not have to remember two different options or which command they apply to, and users can even guess an option without having to look at the help text.

Here are some commonly used options:

12. Prompt for user input. If a user does not pass an argument or flag, prompt for it.

Interactivity

1. Only use prompts or interactive elements if stdin is a TTY. Prompts normally do not work if this is not the case, so error out and tell the user to pass the appropriate flag instead.

2. If prompting for a password, do not print it as the user types. This is done by turning off echo in the terminal. Common runtimes should have helpers for this.

3. Let the user escape. Make it clear how to get out. (Do not do what vim does.) If the program hangs on network I/O or similar, ensure Ctrl+C still works. If it is a wrapper around program execution where Ctrl+C canot quit (SSH, tmux, telnet, …), make it clear how to do that. For example, SSH allows escape sequences with the ~ escape character.

4. If --no-input is passed, do not do anything interactive. This gives users an explicit way to disable all prompts in commands. If the command requires input, fail and tell the user how to pass the information as a flag.

Subcommands

1. If a tool is sufficiently complex, it is good to reduce its complexity by making a set of subcommands. If several tools that are very closely related, they can be made easier to use and discover by combining them into a single command (for example, RCS vs. Git).

They are useful for sharing stuff – global flags, help text, configuration, storage mechanisms.

2. Be consistent across subcommands. Use the same flag names for the same things, have similar output formatting, etc.

3. Use consistent names for multiple levels of a subcommand. If a complex piece of software has lots of objects and operations that can be performed on those objects, it is a common pattern to use two levels of subcommand for this, where one is a noun and one is a verb. For example, docker container create. Be consistent with the verbs you use across different types of objects.

Note
Either noun verb or verb noun ordering works, but noun verb seems to be more common.

4. Do not have ambiguous or similarly-named commands. For example, having two subcommands called update and upgrade is quite confusing. Alternatives include using different words, or disambiguating with extra words.

Robustness

1. Validate user input. Everywhere your program accepts data from the user, it will eventually be given bad data. Check early and bail out before anything bad happens, and make the errors understandable.

2. Allow things to time out. Also allow network timeouts to be configured, and have a reasonable default so it does not hang.

3. Make it idempotent. If the program fails for some transient reason (e.g. the internet connection went down), it should be possible to hit <Up> and <Return> and the program should pick up from where it left off.

4. Make it crash-only. This is the next step up from idempotence. If cleanup can be avoided or deferred to the next run, the program can exit immediately on failure or interruption. This makes it both more robust and more responsive.

5. Responsiveness is more important than speed. Print something to the user in < 100 ms. If there is a network request ongoing, print something beforehand do it so it does not hang and look broken.

6. Show progress if something takes a long time. If the program displays no output for a while, it will look broken. A good spinner or progress indicator can make a program appear to be faster than it is.

7. Do stuff in parallel where possible, but be thoughtful about it. It is already difficult to report progress in the shell. Doing it for parallel processes is ten times harder. Ensure it is robust, and that the output is not confusingly interleaved. If a library is availaboe to provide this, use it. This is code better not written from scratch. Libraries like tqdm for Python and schollz/progressbar for Go support multiple progress bars natively.

Signals

1. If a user sends a SIGINT (typically by pressing Ctrl+C), exit as soon as possible. Say something immediately, before starting clean-up. Add a timeout to any clean-up code so it cannot hang.

2. If a user sends SIGINT during clean-up operations that might take a long time, skip them. Tell the user what will happen when they send SIGINT again, in case it is a destructive action.

For example, when quitting Docker Compose, sending SIGINT a second forces containers to stop immediately instead of letting them shut down gracefully.

$  docker-compose up
...
^CGracefully stopping... (press Ctrl+C again to force)

Configuration

Command-line tools have lots of different types of configuration, and lots of different ways to supply it (flags, environment variables, project-level config files). The best way to supply each piece of configuration depends on a few factors, namely specificity, stability and complexity.

Configuration generally falls into a few categories:

  1. Likely to vary from one invocation of the command to the next. For example, setting the level of debugging output, or enabling a safe mode or dry run of a program.

    Recommendation: use flags.

  • Generally stable from one invocation to the next, but not always. Usually vary between projects, or users of a program. For example, providing a non-default path to items needed for a program to start, specifying how or whether color should appear in output, or specifying an HTTP proxy server to route all requests through.

    Recommendation: use flags and/or environment variables.

  • Stable within a project, for all users. This is the type of configuration that belongs in version control. Files like a Makefile, package.json and docker-compose.yml are all examples of this.

    Recommendation: Use a command-specific, version-controlled config file.

  • 1. If the program modifies the configuration settings of some other part of the system, ask for the user’s consent to do so. Prefer creating a new config file (e.g. /etc/cron.d/myapp) rather than appending to an existing config file (e.g. /etc/crontab). If appending to a system-wide config file is the only way to change settings, use a dated comment in that file to delineate such additions.

    2. Apply configuration parameters in order of precedence. Here is the precedence for config parameters, from highest to lowest:

    1. Flags
    2. The running shell’s environment variables
    3. Project-level configuration files (e.g. .env)
    4. User-level configuration files (e.g. ~/.config)
    5. System-level configuration files (e.g. /etc/myapp.conf)

    Environment variables

    1. Environment variable names must only contain uppercase letters, numbers, and underscores, and they cannot start with a number. This is to maximise portability and minimise conflicts with script variables which are often lowercase.

    2. Do not use newlines in the contents of environment variable values. They often create issues with commands like env and are also not as portable.

    3. Avoid commandeering widely used names. Here is a list of POSIX standard environment variables.

    4. Check general-purpose environment variables for configuration values when possible. These include:

    5. Read environment variables from .env where appropriate. If a command defines environment variables that are unlikely to change as long as the user is working in a particular directory, then it should also read them from a local .env file so users can configure it differently for different projects without having to specify them every time. Many languages have libraries for reading .env files (Rust, Node.js, Ruby).