Skip to content

arturtamborski/cli-best-practices

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

13 Commits
Β 
Β 
Β 
Β 

Repository files navigation


Collection of best practices for CLI tools πŸ‘©β€πŸ’»

β–ͺ️ view on github.com β–ͺ️ view on hackmd.io β–ͺ️


1. BASICS

This section points out a minimal set of necessary requirements to meet in order to make the CLI friendly and usable. Think cargo instead of gcc, or httpie instead of curl.


1.1. Make sure that your tool works

That may sound obvious but it's very important that your tool works. It prints something to stdout, it does not trip on it's own flags and that these flags actually are documented and have an effect on the application. This point is intentionally obvious, because it's 'obvious', so you might not focus on it a lot and just assume you meet it, but to your user - it's the first and most important rule one sees when interacting with the tool.

Imagine how terrible it would be if you'd run a fresh'n'cool CLI tool only to see it breaking imidiately with some stack trace or even worse - obscure, undocumented error.

✨ friendly tip ✨ Consider adding at least some simple test scenario to your CI release pipeline, just to be sure that the CLI at least works. Might not be fully functional, but it has to at least work.

Example

Bad

$ my-cool-tool
Stack trace: [...]

Good

$ my-cool-tool
Error: no input specified! Please use --help to find out more.

You should aim much higher than that, but this is sensible starting point for your tool.


1.2. Document all public flags

Every flag that's available to the user must be documented. It is discouraged to have hidden flags but if that's the case - make sure that hidden flags are not used as an example in documentation or do not show up in answers.

Example

https://stackoverflow.com/a/24470998
https://stackoverflow.com/a/31092052
https://cgold.readthedocs.io/en/latest/glossary/-H.html

cmake -H # set home directory
cmake -B # set build directory

Both flags are undocumented, yet incredibly useful, so they are used and do appear in various online answers and code examples.


1.3. Follow well established conventions

Don't reinvent the flags with your fancy syntax like -Cool_Flag:someValue. It's confusing for everyone, including you.

(Suggested by zapier.com)

Example

Bad

$ 7z a ../lambda.zip -xr'!.venv' .

# to ignore directory you have to specify -x (exclude) and -r (recursive)
# and '!<directory>' -- to exclude it again?
# man 7z: Do not use "-r" because this flag does not do what you think.

Good

$ zip -r ../lambda.zip -x '.git' .
$ zip -r ../lambda.zip --exclude='.git' .  # alternative version

# much cleaner :)

1.4. Use exit codes

Use, document, and inform the user about them.

Example

$ ls /usrer/
ls: cannot access '/usrer/': No such file or directory
$ echo $?
2
$ man ls
   [...]
   Exit status:
       0      if OK,
       1      if minor problems (e.g., cannot access subdirectory),
       2      if serious trouble (e.g., cannot access command-line argument).

1.5. Always provide -h/--help flag

Your tool has to provide some built-in way of informing the user what it does and how to use it. That's the minimum requirement but it's absolutely necessary. Man pages aren't always installed.

Example

Bad

$ 411toppm -h
option `--height' requires an argument
$ 411toppm --help
411toppm: Use 'man 411toppm' for help.

Good

$ xz -h  # or xz --help, it's the same output
Usage: xz [OPTION]... [FILE]...
Compress or decompress FILEs in the .xz format.

  -z, --compress      force compression
  -d, --decompress    force decompression
[...]

1.6. Learn from others, copy the best

gh-cli, heroku cli, httpie, cargo


1.7. Keep the tool name short, unique and easy to remember

If that's not possible consider supporting alternative binary name (angular-cli β†’ ng).

This point is highly specific to your case, it might be that you're building a collection of related CLI tools, where it makes sense to name the binary in a namespaced manner, like gnome-session / gnome-session-new-session / gnome-session-properties...


1.8. Tool has to be predictable and idempotent

In other words, it should behave the same way no matter the time of execution or other external factors.

For expanded explanation, see:


1.9. Tool has to be task oriented

The idea here is to use the tool to fulfill some need (like a need to copy a file, copy a repository, or format a filesystem). Your CLI tool might be specific to the service you're offering (like a gh-cli) in which case there are many tasks you can achieve, but because gh-cli is task oriented, it's easy to execute desired action.

Example

$ gh --help
Work seamlessly with GitHub from the command line.

USAGE
  gh <command> <subcommand> [flags]

CORE COMMANDS
  browse:     Open the repository in the browser
  codespace:  Connect to and manage your codespaces
  gist:       Manage gists
  issue:      Manage issues
  pr:         Manage pull requests
  release:    Manage GitHub releases
  repo:       Create, clone, fork, and view repositories

ACTIONS COMMANDS
  actions:    Learn about working with GitHub actions
  run:        View details about workflow runs
  workflow:   View details about GitHub Actions workflows

ADDITIONAL COMMANDS
  alias:      Create command shortcuts
  api:        Make an authenticated GitHub API request
  auth:       Login, logout, and refresh your authentication
  completion: Generate shell completion scripts
  config:     Manage configuration for gh
  extension:  Manage gh extensions
  gpg-key:    Manage GPG keys
  help:       Help about any command
  secret:     Manage GitHub secrets
  ssh-key:    Manage SSH keys

FLAGS
  --help      Show help for command
  --version   Show gh version

EXAMPLES
  $ gh issue create
  $ gh repo clone cli/cli
  $ gh pr checkout 321

1.10. Tool has to be friendly people and scripts

This point is often missing, especially in older CLI tools which were designed for proficient CLI users but with little thought for use in automated scripts. That's why tools like cut, awk and others are so often seen in bash scripts - it's the only way to convert human readable output to machine readable format used by other tools.

Example

Bad

$ docker ps -a | grep alpine | cut -c 1-12
# get container IDs which are based on alpine image

This command is very fragile to output-specific behaviour - it can easily fail if the output changes and it also isn't exactly correct, because grep alpine will only find the obvious cases, but it will not show containers which are based on apline by inheritance.

Good

$ docker ps -a --filter=ancestor=alpine --format "{{ .ID }}"
# get container IDs which are based on alpine image

Docker CLI was specifically designed to be parseable by scripts without the impact on user experience (when output changed for some reason). It will work reliably every time and is more correct, because it will check the actual dependency on alpine image instead of just searching it in the output.


1.11. Tool has to be high quality

It's as important as your API. Actually, it is your API, but for humans.


1.12. Commands that read like sentences are easier to remember




2. FLAGS


2.1. As always, Consistency is the Key

By far the most commonly used (and thus the only good way) of writting flags is with the dash-case. Do not use anything else. Yeah, java uses -Xmx, but it's their problem, not your option. Do not use anything else other than --option-name. Please.


2.2. This applies to environment variables as well

GOOD_ENV_VAR_NAME vs BAD_envVarName. Yes, they are case sensitive, no, you should not leverage that. Please stick with upper casing or SCREAMING_SNAKE_CASE if that convinces you more.


2.3. Every flag that can have a default value, should have default value

Defaults have to be sensible and meaningful. If you are not sure, better to leave it off and come back after reasuring yourself on the most commonly used value.


2.4. Provide long options first, then shorten the most commonly used ones


2.5. Don't use -longopt, instead use --longopt

Copypaste of someones opinion:

I hate -long style options too. It's not just aesthetic/confusing, it's objectively worse/more limited:

  • It means -thing != -t -h -i -n -g, so the latter has to be that long;
  • You also can't have readable-long-things-as-an-option;
  • You can't (or not without crazy logic and a confusing UX) have variables passed with no space, like -ffoobar

2.6. Named flags should be position independent

ls --list --all .
# is the same as...
ls --all --list .

2.7. Long named flags > short named flags > positional arguments

find -R . -name '*.txt' -type f
vs
find . -R -n '*.txt' -f
vs
find . r '*.txt' f

2.8. List of common conventions often used in CLI tools

  • - for marking stdin / stdout
  • -- for marking end of arguments
  • --longopt=somevalue for passing values with long flags
  • -s somevalue for passing values with short flags
  • --pointer / --no-pointer prefix flags with no for switch-like functionality
  • -f / --file for selecting file
  • -o / --output for specyfing output file
  • -i / --input for specyfing input file
  • -h / --help for displaying help
  • -n for dry run
  • -v / --verbose for increasing verbosity
  • -q / --quiet for decreasing verbosity
  • -V / --version for showing version
  • http://docopt.org/

Commonly established conventions take priority.

-v -> verbose / version
-r -> reverse / recursive
-a -> all / append
-l -> list / lines
-n -> n things

--recursive -> -r / --reverse -> -r

--all -> -a


2.9. Allow passing sensitive values trough multiple channels

Flags are not always the best option, for example --password=qwerty123 really won't work. There has to be a way to pass sensitive data without it being shown to the user. A good rule of thumb is to expect your tool to be used in public CI pipeline or shown in youtube tutorial. If you have to obfuscate the secrets in your docs to show the option then you're probably doing it incorrectly, because every user will have to keep that in mind as well.

env MY_TOOL_SECRET | my-tool --password - is a good way of sending the input secretly.


2.10. If your tool is big, split it into subcommands

aws [global flags] acm [acm specific args] [acm specific flags]
aws [global flags] s3 [s3 specific args] [s3 specific flags]

aws --region=us-east-1 s3 ls --bucket=test




3. USER EXPERIENCE


3.1. Inform the user early


3.2. Don't go for a long period without output to the user

If you do print status messages like that, make sure to send them to STDERR if your utility outputs any actual data (like a report or file listing). Same for progress meters.


3.3. If a command has a side effect provide a dry-run/whatif/no_post option


3.4. For long running operations, allow the user to recover at a failure point if possible


3.5. Support scaffolding, if applicable


3.6. Support autocompletion


3.7. Make it easy to install and easy to update


3.8. Suggest commands on typing errors (git satus β†’ Did you mean git status)


3.9. Always do the least surprising thing

rm -rf /* - rm has to be aware what might happen an try to block the user. It's too destructive to accept it without confirmation.

pkill -v <some process id> - v stands for "inverse input", so this actually kills every process except the one passed in input. Why?


3.10. Repair what you can β€” but when you must fail, fail noisily and as soon as possible


3.11. Design for the future, because it will be here sooner than you think


3.12. Aliases provide balance between brevity and discoverability

Consider splitting your cli into Resources: as in what you can achieve and Aliases as in list of shortcuts to do stuff.


3.13. Piping is good for automation but people don't want to pipe

If its a common operation then provide a dedicated command for it. Either you will or every user will wrap some commands with subshells and pipes without setting β€˜set -o pipefail”


3.14. Default to human-first output, but support multiple (json ftw)

Optimize output for humans, 10 days ago > 2019-07-15T14:32:22Z

You can use ISO for JSON :)


3.15. Avoid positional arguments where the order matters

which one is correct and why?

emote add repo funk https://x.com/funk.json
vs
emote add repo https://x.com/funk.json funk

answer: neither! it's not obvious, you have to check docs to make sure = bad UX.

emote add repo funk --url https://x.com/funk.json

command subcommand NAME β€”more-flags


3.16. Positional arguments are cool when the order doesn't matter

emote repo delete funk more-funk
vs
emote repo delete more-funk funk

3.17. Expect that user will try to stop the tool at the worst moment (eg. during lengthy process like cloning a repo)

Be sure to not fail unexpectely and to not leave trash after unfinished action. You can overwrite action on CTRL+C to clean up right before the program finishes.




4. ACCESSIBILITY


4.1. Keep the output short

Try to not go over 80 characters per line of output

Bad

$ ls --help
[...]
  --all -- this option allows you to print every file in given directory specified in the input so that you can see every file in local directory even if its hidden.

Good

$ ls --help
[...]
  --all -- print every file including hidden ones

4.2. $NO_COLOR support

The CLI tool should support commonly used modifiers for it's output. See https://no-color.org/ for more information.


4.3. --verbose support

Preferably with some way of increasing/decreasing it.


4.4. Localization support

Common pitfal: http://www.scalingbits.com/aws/CLI


4.5. Translations

I don't actually know much about that and how it should work...


4.6. $DEBUG support

Tool should provide sensible way to debug it.


4.7. Don't use too many colors and don't expect users have more than 16 colors by default

CLI tools don't control their background so the ouput must be background-color agnostic. There shouldn't be any asumption as to the color scheme used by the user, instead all colors should be treated as hints only. Eample: use bold to visibly separate short important parts (like headers),

If in doubt, change the color to default and say what you have to say, most often than not this will end up looking better than some combination of inverse bold text with custom background color.

Unless your application controls the whole terminal (like vim, mutt, etc) it's just not worth playing too much with colors because you're raising your chances of readable output which should be your priority.

More info on colors: https://accessibility.psu.edu/legibility/contrast/


4.8. Decrease use of emojis to minimum and stick to the old and well known emojis

Think clearly on what you're trying to communicate, too many emojis may obfuscate this where a few words would do. Use emojis to transfer emotions and common behaviours (hand waving to say hello, confetti to express joy or sucess) instead of using them as pictograms with some underlying meaning. This meaning might not be valid in other cultures (praise hands is a good example) or it might not mean what you thought it means. If you have to stop and think what your output with emoji means then you should change that to clarify your intentions.

Another issue is with support - many fonts fonts don't support recent emojis or are visualised differently across operating system (consider every font used in every CI pipeline + every monospaced font on commonly used OS + some of these are not update (Ubuntu 12? Ubuntu 10? Old mac? They all will have problems with emojis) Use emojipedia.com to verify that emojis are actually representing the same thing and same meaning.


4.9. Use gender neutral language

https://man7.org/linux/man-pages/man7/man-pages.7.html


4.10. Follow american spelling convention

color > colour, etc. It's just more common and causes less confusion, don't take this personally.


4.11. Support diagnostic flag of sorts

When your tool fails someone will try to report it,

(find example of issue where they ask to rerun the command with some diagnostic switch for easier debugging


4.12. Use short, terse and simple language for describing the program's behaviour

Do not use slang-specific words if they obfuscate otherwise simple meaning. Try to keep the program-specific slang or brand-specific slang to minimum. One example of that is yargs which uses pirate-like terminology to explain its meaning and create a brand around it. It's okay to do so as long as it doesn't spread accross the whole help page or documentation.

Simple things should be named simply, known things should be named with known words. Do not reinvent the common terms because you will not be understood by most of the world.

Example: some programs support plugins / addons / extensions and then there's ansible galaxy.


4.13. Be aware of your users, their backgrounds and your environment

Go commands often use get for downloading, so your tool should also use it if it needs that feature.

Try to discover, observe and mimic common behaviour of your ecosystem's tools.


4.14. Don’t use color as the only way to convey information

Users who are color blind cannot receive information that is conveyed only through color, such as in a color status indicator. Include other visual cues, preferably text, to ensure that information is accessible.


4.15. Avoid using blinking font (ANSI modifier)

Testing your work:

  • assistive Tech Familiraize yourself with screen-reader tech like VoiceOver JAWS and NVDA. If you can, get an assistive technology lab and test out your software. If you can't, improvise!
  • Content and naming Am I saying this in the simplest, most direct way? Do my error messages make sense and give the users a path forward?
  • Yes, you too Test with differently-abled users!
  • Automation Add automated a11y linter to your CI pipeline. Design automated tests with a11y in mind.

https://speakerdeck.com/raqueldesigns/everyone-should-be-able-to-use-your-software-accessibility-for-balanced-teams?slide=42


4.16. Give users enough time to complete an action


4.17. Explain what will happen after completing a service / command


4.18. Make important information clear


4.19. Let users check and change their answers before they submit them

Accessibility is important - make your applications accessible because its the right thing to do, but if you don't want to do it because it's the right thing to do, do it because it's the legally required thing - https://section508.gov/




5. DOCUMENTATION

At minimum your tool should be able to inform the user on how to use it effectively. This is most commonly done with --help flag.


5.1. Provide --version flag with understandable versioning scheme

If you're not sure which one to use go with semver, because it's the most popular one.https://semver.org/


5.2. Document every file that will impact the behaviour

If your app uses something from /etc document that. If your app reads ~/.config/<my-app>/config.ini document that as well. Every file that impacts the CLI has to be documented, or at least mentioned.


5.3. Provide easy way of updating the tool by itself

$ tool update should be built in, tested and working out of the box. This might be as simple as downloading appropriate binary and replacing the original one, that's simple enough to support it without any dependencies. If your tool needs more things to update itself, consider simplifing it.


5.4. --help is not man

If possible, support man pages as well as your built-in documentation. Many CLI tools have extensive documentation built in under help subcommand which is useful, but also limited in some regards. The same docs can be easily regenerated to different format using tools like ronn.

https://rtomayko.github.io/ronn/

https://man7.org/linux/man-pages/man7/man-pages.7.html


5.5. Try to follow man pages when displaying help

NAMESYNOPSIS
              CONFIGURATION    [Normally only in Section 4]
DESCRIPTION
              OPTIONS          [Normally only in Sections 1, 8]
              EXIT STATUS      [Normally only in Sections 1, 8]
              RETURN VALUE     [Normally only in Sections 2, 3]
              ERRORS           [Typically only in Sections 2, 3]
              ENVIRONMENT
              FILES
              VERSIONS         [Normally only in Sections 2, 3]
              ATTRIBUTES       [Normally only in Sections 2, 3]
              CONFORMING TO
              NOTES
              BUGS

              EXAMPLES
              AUTHORS          [Discouraged]
              REPORTING BUGS   [Not used in man-pages]
              COPYRIGHT        [Not used in man-pages]
SEE ALSO




6. CONFIGURATION


6.1. Support XDG specification

https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html

https://github.com/aws/aws-sdk/issues/30

this one drives me crazy, one of the most requested changes and it just hangs there for over three years now.


6.2. Don't invent your own config syntax for configuration

Please, don't. Every config format, even the most basic one like INI is better than your own.


6.3. Support multiple output formats if they are required by the users

It's better to do it correctly on your side than to let the users pipe your json to yq just because they need that output format badly for some reason.

You are creating this tool for humans, make it human friendly :)


6.4. If behaviour can be altered with env variables then it should be possible to do the same with flags

If your program uses XDG_CONFIG_DIR to place it's config, then you should also support β€”config-dir for doing the same thing.




7. STATEFULNESS

Keep it low. CLI is a simple state machine, and it should stay simple.


7.1. If your app has to store some state, store it in one, clearly indicated file.

Don't set environment variables to store anything, it just won't work. The best way is to not have a state, second best is to keep it in one (and only one) file.

Good example: terraform

Bad example: ???




8. SCRIPTING

Always assume that your tool will be wrapped with some script. Folks from apt-get never expected that, but it's the reality.


8.1 Support JSON output

It's the most universal and by far the most popular way of outputing machine-readable data.

Bonus points: support filtering / limiting the output as well.


8.2. Be aware of signals

Expect that your tool will receive signals. Your tool should react most commonly to that.

(how, exaclty?)


8.3. Stderr is for errors, stdout is for output

Stick to it.


8.4. Expect input from stdin, pipe, unix socket, redirection from files etc

In other words: make it input-agnostic. Don't assume anything about the source input because it probably won't be true.


8.5. STDOUT is your API. You have to expect people wrapping it like an api

If you don't provide a machine-readable output (β€”json), then stdout will be used as a machine-readable output. It will be parsed, processed with various tools and will be relied upon to do the work. This matters a lot when your tool is big enough.

β€”json (or any other switch for machine readable output) is your safety valve, if you don't have that then you have to be way more concius on how output is used.




9. DEVELOPMENT


9.1. Test your code, heavly. Test your CLI commands heavly

One tip is to don't hook up annonymous function to your CLI framework but instead keep them in separate module and then hook them to framework's API explicitly in some separate module. That way you will be able to easily test your command runners (functions) without the burden of dealing with CLI framework.


9.2. Connect your command handlers (functions) to the CLI framework boringly

It has to be simple. boring, moundaine and uninteresting code, because it is uninteresting code. The interesting part is in the command handler function, that's where magic happens! Keep the complexity out of the handler<>CLI framework bridge.






10. RESOURCES USED


10.1. Links



10.2. Collections





Thank you for reading :) You're welcome to comment this list on hackmd or on github.
my notes, please ignore for now :)
  • TODO: ask @carolynvs for more resources, correct errors?
  • TODO: find more people who care about good CLIs, ask for their help
  • TODO: share this on github (awesome-cli-practices? Still thinking on good title, probably something starting with awesome- so it's easier to find it among other GH repos).
  • TODO: talk with a11y folks on their ideas for this specific environment (no resources so far...)
  • TODO: clean it up, add example of bad/good to every point.
  • TODO: add footnotes with references to source knowledge