Authors
Design by Mark Hurrell. Thanks to Andreas Jansson for early contributions, and Andrew Reitz, Ashley Williams, Brendan Falk, Chester Ramey, Dj Walker-Morgan, Jacob Maine, James Coglan, Michael Dwan, and Steve Klabnik for reviewing drafts.
Join us on Discord if you want to discuss the guide or CLI design.
Foreword
In the 1980s, if you wanted a personal computer to do something for you, you needed to know what to type when confronted with C:\> or ~$. Help came in the form of thick, spiral-bound manuals. Error messages were opaque. There was no Stack Overflow to save you. But if you were lucky enough to have internet access, you could get help from Usenet—an early internet community filled with other people who were just as frustrated as you were.
Forty years later, computers have become so much more accessible to everyone, often at the expense of low-level end user control. On many devices, there is no command-line access at all, in part because it goes against the corporate interests of walled gardens and app stores.
Most people today don't know what the command line is, much less why they would want to bother with it. As computing pioneer Alan Kay said in a 2017 interview, “Because people don't understand what computing is about, they think they have it in the iPhone, and that illusion is as bad as the illusion that 'Guitar Hero' is the same as a real guitar.”
Kay's “real guitar” isn't the CLI—not exactly. He was talking about ways of programming computers that offer the power of the CLI and that transcend writing software in text files. There is a belief among Kay's disciples that we need to break out of a text-based local maximum that we've been living in for decades.
It's exciting to imagine a future where we program computers very differently. Even today, spreadsheets are by far the most popular programming language, and the no-code movement is taking off quickly as it attempts to replace some of the intense demand for talented programmers.
Yet with its creaky, decades-old constraints and inexplicable quirks, the command line is still the most versatile corner of the computer. It lets you pull back the curtain, see what's really going on, and creatively interact with the machine at a level of sophistication and depth that GUIs cannot afford. It's available on almost any laptop, for anyone who wants to learn it. It can be used interactively, or it can be automated. And, it doesn't change as fast as other parts of the system. There is creative value in its stability.
So, while we still have it, we should try to maximize its utility and accessibility.
A lot has changed about how we program computers since those early days. The command line of the past was machine-first: little more than a REPL on top of a scripting platform. But as general-purpose interpreted languages have flourished, the role of the shell script has shrunk. Today's command line is human-first: a text-based UI that affords access to all kinds of tools, systems and platforms. In the past, the editor was inside the terminal—today, the terminal is just as often a feature of the editor. And there's been a proliferation of git-like multi-tool commands. Commands within commands, and high-level commands that perform entire workflows rather than atomic functions.
Inspired by traditional UNIX philosophy, driven by an interest in encouraging a more delightful and accessible CLI environment, and guided by our experiences as programmers, we decided it was time to revisit the best practices and design principles for building command-line programs.
Long live the command line!
Introduction
This document covers both high-level design philosophy, and concrete guidelines. It's heavier on the guidelines because our philosophy as practitioners is not to philosophize too much. We believe in learning by example, so we've provided plenty of those.
This guide doesn't cover full-screen terminal programs like emacs and vim. Full-screen programs are niche projects—very few of us will ever be in the position to design one.
This guide is also agnostic about programming languages and tooling in general.
Who is this guide for?
- If you are creating a CLI program and you are looking for principles and concrete best practices for its UI design, this guide is for you.
- If you are a professional “CLI UI designer,” that's amazing—we'd love to learn from you.
- If you'd like to avoid obvious missteps of the variety that go against 40 years of CLI design conventions, this guide is for you.
- If you want to delight people with your program's good design and helpful help, this guide is definitely for you.
- If you are creating a GUI program, this guide is not for you—though you may learn some GUI anti-patterns if you decide to read it anyway.
- If you are designing an immersive, full-screen CLI port of Minecraft, this guide isn't for you. (But we can't wait to see it!)
Philosophy
These are what we consider to be the fundamental principles of good CLI design.
Human-first design
Traditionally, UNIX commands were written under the assumption they were going to be used primarily by other programs. They had more in common with functions in a programming language than with graphical applications.
Today, even though many CLI programs are used primarily (or even exclusively) by humans, a lot of their interaction design still carries the baggage of the past. It's time to shed some of this baggage: if a command is going to be used primarily by humans, it should be designed for humans first.
Simple parts that work together
A core tenet of the original UNIX philosophy is the idea that small, simple programs with clean interfaces can be combined to build larger systems. Rather than stuff more and more features into those programs, you make programs that are modular enough to be recombined as needed.
In the old days, pipes and shell scripts played a crucial role in the process of composing programs together. Their role might have diminished with the rise of general-purpose interpreted languages, but they certainly haven't gone away. What's more, large-scale automation—in the form of CI/CD, orchestration and configuration management—has flourished. Making programs composable is just as important as ever.
Fortunately, the long-established conventions of the UNIX environment, designed for this exact purpose, still help us today. Standard in/out/err, signals, exit codes and other mechanisms ensure that different programs click together nicely. Plain, line-based text is easy to pipe between commands. JSON, a much more recent invention, affords us more structure when we need it, and lets us more easily integrate command-line tools with the web.
Whatever software you're building, you can be absolutely certain that people will use it in ways you didn't anticipate. Your software will become a part in a larger system—your only choice is over whether it will be a well-behaved part.
Most importantly, designing for composability does not need to be at odds with designing for humans first. Much of the advice in this document is about how to achieve both.
Consistency across programs
The terminal's conventions are hardwired into our fingers. We had to pay an upfront cost by learning about command line syntax, flags, environment variables and so on, but it pays off in long-term efficiency… as long as programs are consistent.
Where possible, a CLI should follow patterns that already exist. That's what makes CLIs intuitive and guessable; that's what makes users efficient.
That being said, sometimes consistency conflicts with ease of use. For example, many long-established UNIX commands don't output much information by default, which can cause confusion or worry for people less familiar with the command line.
When following convention would compromise a program's usability, it might be time to break with it—but such a decision should be made with care.
Saying (just) enough
The terminal is a world of pure information. You could make an argument that information is the interface—and that, just like with any interface, there's often too much or too little of it.
A command is saying too little when it hangs for several minutes and the user starts to wonder if it's broken. A command is saying too much when it dumps pages and pages of debugging output, drowning what's truly important in an ocean of loose detritus. The end result is the same: a lack of clarity, leaving the user confused and irritated.
It can be very difficult to get this balance right, but it's absolutely crucial if software is to empower and serve its users.
Ease of discovery
When it comes to making functionality discoverable, GUIs have the upper hand. Everything you can do is laid out in front of you on the screen, so you can find what you need without having to learn anything, and perhaps even discover things you didn't know were possible.
It is assumed that command-line interfaces are the opposite of this—that you have to remember how to do everything. The original Macintosh Human Interface Guidelines, published in 1987, recommend “See-and-point (instead of remember-and-type),” as if you could only choose one or the other.
These things needn't be mutually exclusive. The efficiency of using the command-line comes from remembering commands, but there's no reason the commands can't help you learn and remember.
Discoverable CLIs have comprehensive help texts, provide lots of examples, suggest what command to run next, suggest what to do when there is an error. There are lots of ideas that can be stolen from GUIs to make CLIs easier to learn and use, even for power users.
Conversation as the norm
GUI design, particularly in its early days, made heavy use of metaphor: desktops, files, folders, recycle bins. It made a lot of sense, because computers were still trying to bootstrap themselves into legitimacy. The ease of implementation of metaphors was one of the huge advantages GUIs wielded over CLIs. Ironically, though, the CLI has embodied an accidental metaphor all along: it's a conversation.
Beyond the most utterly simple commands, running a program usually involves more than one invocation. Usually, this is because it's hard to get it right the first time: the user types a command, gets an error, changes the command, gets a different error, and so on, until it works. This mode of learning through repeated failure is like a conversation the user is having with the program.
Trial-and-error isn't the only type of conversational interaction, though. There are others:
- Running one command to set up a tool and then learning what commands to run to actually start using it.
- Running several commands to set up an operation, and then a final command to run it (e.g. multiple
git adds, followed by agit commit). - Exploring a system—for example, doing a lot of
cdandlsto get a sense of a directory structure. - Doing a dry-run of a complex operation before running it for real.
Acknowledging the conversational nature of command-line interaction means you can bring relevant techniques to bear on its design. You can suggest possible corrections when user input is invalid, you can make the intermediate state clear when the user is going through a multi-step process, you can confirm for them that everything looks good before they do something scary.
The user is conversing with your software, whether you intended it or not. At worst, it's a hostile conversation which makes them feel stupid and resentful. At best, it's a pleasant exchange that speeds them on their way with newfound knowledge and a feeling of achievement.
Robustness
Robustness is both an objective and a subjective property. Software should be robust, of course: unexpected input should be handled gracefully, operations should be idempotent where possible, and so on. But it should also feel robust.
You want your software to feel like it isn't going to fall apart. You want it to feel immediate and responsive, as if it were a big mechanical machine, not a flimsy plastic “soft switch.”
Subjective robustness requires attention to detail and thinking hard about what can go wrong. It's lots of little things: keeping the user informed about what's happening, explaining what common errors mean, not printing scary-looking stack traces.
As a general rule, robustness can also come from keeping it simple. Lots of special cases and complex code tend to make a program fragile.
Empathy
Command-line tools are a programmer's creative toolkit, so they should be enjoyable to use. This doesn't mean turning them into a video game, or using lots of emoji (though there's nothing inherently wrong with emoji). It means giving the user the feeling that you are on their side, that you want them to succeed, that you have thought carefully about their problems and how to solve them.
There's no list of actions you can take that will ensure they feel this way, although we hope that following our advice will take you some of the way there. Delighting the user means exceeding their expectations at every turn, and that starts with empathy.
Chaos
The world of the terminal is a mess. Inconsistencies are everywhere, slowing us down and making us second-guess ourselves.
Yet it's undeniable that this chaos has been a source of power. The terminal, like the UNIX-descended computing environment in general, places very few constraints on what you can build. In that space, all manner of invention has bloomed.
It's ironic that this document implores you to follow existing patterns, right alongside advice that contradicts decades of command-line tradition. We're just as guilty of breaking the rules as anyone.
The time might come when you, too, have to break the rules. Do so with intention and clarity of purpose.
“Abandon a standard when it is demonstrably harmful to productivity or user satisfaction.” — Jef Raskin, The Humane Interface
Guidelines
This is a collection of specific things you can do to make your command-line program better.
The first section contains the essential things you need to follow. Get these wrong, and your program will be either hard to use or a bad CLI citizen.
The rest are nice-to-haves. If you have the time and energy to add these things, your program will be a lot better than the average program.
The idea is that, if you don't want to think too hard about the design of your program, you don't have to: just follow these rules and your program will probably be good. On the other hand, if you've thought about it and determined that a rule is wrong for your program, that's fine. (There's no central authority that will reject your program for not following arbitrary rules.)
The Basics
There are a few basic rules you need to follow. Get these wrong, and your program will be either very hard to use, or flat-out broken.
Use a command-line argument parsing library where you can. Either your language's built-in one, or a good third-party one. They will normally handle arguments, flag parsing, help text, and even spelling suggestions in a sensible way.
Here are some that we like:
Return zero exit code on success, non-zero on failure. Exit codes are how scripts determine whether a program succeeded or failed, so you should report this correctly. Map the non-zero exit codes to the most important failure modes.
Send output to stdout. The primary output for your command should go to stdout. Anything that is machine readable should also go to stdout—this is where piping sends things by default.
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 and not fed into the next command.
Help
Display extensive help text when asked. Display help when passed -h or --help flags. This also applies to subcommands which might have their own help text.
Display concise help text by default. When myapp or myapp subcommand requires arguments to function, and is run with no arguments, display concise help text.
The concise help text should only include: a description of what your program does, one or two example invocations, descriptions of flags (unless there are lots of them), and an instruction to pass the --help flag for more information.
jq does this well. When you type jq, it displays an introductory description and an example, then prompts you to pass jq --help for the full listing of flags:
$ 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.
For a listing of options, use jq --help.
Show full help when -h and --help are passed. Ignore any other flags and arguments that are passed—you should be able to add -h to the end of anything and it should show help. Don't overload -h.
Provide a support path for feedback and issues. A website or GitHub link in the top-level help text is common.
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's doing and it isn't too long, show the actual output too.
Display the most common flags and commands at the start of the help text.
Use formatting in your help text. Bold headings make it much easier to scan. But, try to do it in a terminal-independent way so that your users aren't staring down a wall of escape characters.
If the user did something wrong and you can guess what they meant, suggest it. You can ask if they want to run the suggested command, but don't force it on them.
Documentation
Provide web-based documentation. People need to be able to search online for your tool's documentation, and to link other people to specific parts.
Provide terminal-based documentation. Documentation in the terminal has several nice properties: it's fast to access, it stays in sync with the specific installed version of the tool, and it works without an internet connection.
Consider providing man pages. Man pages, Unix's original system of documentation, are still in use today, and many users will reflexively check man mycmd as a first step when trying to learn about your tool.
Output
Human-readable output is paramount. Humans come first, machines second. The most simple heuristic for whether a particular output stream is being read by a human is whether or not it's a TTY.
Have machine-readable output where it does not impact usability. Streams of text is the universal interface in UNIX.
Display output as formatted 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.
Display output on success, but keep it brief. Traditionally, when nothing is wrong, UNIX commands display no output to the user. It's rare that printing nothing at all is the best default behavior, but it's usually best to err on the side of less.
If you change state, tell the user. When a command changes the state of a system, it's especially valuable to explain what has just happened, so the user can model the state of the system in their head.
Use color with intention. For example, you might want to highlight some text so the user notices it, or use red to indicate an error. Don't overuse it—if everything is a different color, then the color means nothing.
Disable color if your program is not in a terminal or the user requested it. Respect NO_COLOR, TERM=dumb, and --no-color.
Use symbols and emoji where it makes things clearer. Pictures can be better than words if you need to make several things distinct or catch the user's attention.
Errors
Catch errors and rewrite them for humans. If you're expecting an error to happen, 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.
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 they did wrong.
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.
If there is an unexpected or unexplainable error, provide debug and traceback information, and instructions on how to submit a bug.
Make it effortless to submit bug reports. One nice thing you can do is provide a URL and have it pre-populate as much information as possible.
Arguments and Flags
A note on terminology: Arguments, or args, are positional parameters to a command. 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).
Prefer flags to args. It's a bit more typing, but it makes it much clearer what is going on. It also makes it easier to make changes to how you accept input in the future.
Have full-length versions of all flags. For example, have both -h and --help. Having the full version is useful in scripts where you want to be verbose and descriptive.
Only use one-letter flags for commonly used flags. That way you don't pollute your namespace of short flags.
Use standard names for flags, if there is a standard. If another commonly used command uses a flag name, it's best to follow that existing pattern.
Commonly used options: -a, --all · -d, --debug · -f, --force · --json · -h, --help · -n, --dry-run · -o, --output · -q, --quiet · -u, --user · --version
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.
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.
Do not read secrets directly from flags. When a command accepts a secret, the flag value will leak into ps output and potentially shell history. Consider accepting sensitive data only via files or via stdin.
Interactivity
Only use prompts or interactive elements if stdin is an interactive terminal (a TTY).
If --no-input is passed, don't prompt or do anything interactive. This allows users an explicit way to disable all prompts in commands.
If you're prompting for a password, don't print it as the user types.
Let the user escape. Make it clear how to get out. If your program hangs on network I/O, always make Ctrl-C still work.
Subcommands
If you've got a tool that's sufficiently complex, you can reduce its complexity by making a set of subcommands.
Be consistent across subcommands. Use the same flag names for the same things, have similar output formatting, etc.
Use consistent names for multiple levels of subcommand. If a complex piece of software has lots of objects and operations, 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.
Don't have ambiguous or similarly-named commands. For example, having two subcommands called “update” and “upgrade” is quite confusing.
Robustness
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.
Responsive is more important than fast. Print something to the user in <100ms. If you're making a network request, print something before you do it so it doesn't hang and look broken.
Show progress if something takes a long time. A good spinner or progress indicator can make a program appear to be faster than it is.
Do stuff in parallel where you can, but be thoughtful about it. Make sure the output isn't confusingly interleaved.
Make things time out. Allow network timeouts to be configured, and have a reasonable default so it doesn't hang forever.
Make it recoverable. If the program fails for some transient reason, you should be able to hit <up> and <enter> and it should pick up from where it left off.
Make it crash-only. If you can avoid needing to do any cleanup after operations, your program can exit immediately on failure or interruption.
Future-proofing
Keep changes additive where you can. Rather than modify the behavior of a flag in a backwards-incompatible way, maybe you can add a new flag.
Warn before you make a non-additive change. Before you break an interface, forewarn your users in the program itself.
Changing output for humans is usually OK. Encourage your users to use --plain or --json in scripts to keep output stable.
Don't have a catch-all subcommand. You can never add a subcommand with a conflicting name without risking breaking existing usages.
Don't allow arbitrary abbreviations of subcommands. Aliases are fine, but they should be explicit and remain stable.
Don't create a “time bomb.” Imagine it's 20 years from now. Will your command still run the same as it does today?
Signals and Control Characters
If a user hits Ctrl-C (the INT signal), exit as soon as possible. Say something immediately, before you start clean-up. Add a timeout to any clean-up code so it can't hang forever.
If a user hits Ctrl-C during clean-up operations that might take a long time, skip them. Tell the user what will happen when they hit Ctrl-C again.
$ 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. The best way to supply each piece of configuration depends on a few factors, chief among them specificity, stability and complexity.
Follow the XDG-spec. The XDG Base Directory Specification supports a general-purpose ~/.config folder and is supported by yarn, fish, wireshark, emacs, neovim, tmux, and many other projects.
If you automatically modify configuration that is not your program's, ask the user for consent. Prefer creating a new config file rather than appending to an existing one.
Apply configuration parameters in order of precedence. Flags > shell environment variables > project-level config > user-level config > system-wide config.
Environment Variables
Environment variables are for behavior that varies with the context in which a command is run.
For maximum portability, environment variable names must only contain uppercase letters, numbers, and underscores.
Check general-purpose environment variables for configuration values when possible: NO_COLOR, DEBUG, EDITOR, HTTP_PROXY, SHELL, TERM, TMPDIR, HOME, PAGER, LINES, COLUMNS.
Read environment variables from .env where appropriate. Many languages have libraries for this (Rust, Node, Ruby).
Do not read secrets from environment variables. Exported environment variables are sent to every process and can easily leak into logs. Secrets should only be accepted via credential files, pipes, AF_UNIX sockets, or secret management services.
Naming
Make it a simple, memorable word. But not too generic, or you'll step on the toes of other commands and confuse users.
Use only lowercase letters, and dashes if you really need to. curl is a good name, DownloadURL is not.
Keep it short. Users will be typing it all the time. Don't make it too short though: the very shortest commands are best reserved for common utilities like cd, ls, ps.
Make it easy to type. A real-world example: Docker Compose went from plum to fig because it flowed much more easily on the keyboard.
Distribution
If possible, distribute as a single binary. If your language doesn't compile to binary executables as standard, see if it has something like PyInstaller.
Make it easy to uninstall. If it needs instructions, put them at the bottom of the install instructions.
Analytics
Do not phone home usage or crash data without consent. Users will find out, and they will be angry. Be very explicit about what you collect, why you collect it, and how anonymous it is.
Consider alternatives to collecting analytics: instrument your web docs, instrument your downloads, or talk to your users directly.
Further Reading
- The Unix Programming Environment — Brian W. Kernighan and Rob Pike
- POSIX Utility Conventions
- Program Behavior for All Programs — GNU Coding Standards
- 12 Factor CLI Apps — Jeff Dickey
- CLI Style Guide — Heroku