Skip to main content

CLI best practices

This article pulls together a number of tips and best practices for building CLI applications.

Input

  • Use standard library input functions to read from stdin that can play nicely with rlwrap(1) and other input sources.
  • If using readline functionality, provide an option to disable it.
  • Only use readline functionality if you have used isatty(3) to test if stdin is a terminal.
  • Don't pass sensitive information like passwords and API tokens on the command-line since they will appear in the output of ps(1). Instead store them in a file with appropriate ownership & permissions or pass them to the application in an environment variable.
  • If you need to interactively read sensitive information such as passwords, read directly from /dev/tty or use curses/ncurses calls to prevent sensitive input from displaying on the screen.

Output

  • Though obvious, put errors on stderr and output on stdout.
  • Only show progress meters if requested via a command-line option.
  • Allow tunable output verbosity, preferrably including a completely silent mode (often -q/--quiet or -s/--silent) and increasing levels of info/debugging output (often one or more -v/--verbose options).
  • Make color optional/smart by respecting $NO_COLOR if set, or $COLOR={always,never,auto}. If $COLOR (or $PROGNAME_COLOR) is not set, use isatty(3) to establish a default.
  • Default output format should have one record per line with easy-to-discern/use column delimiters, not one record spanning multiple lines (unless requested as an output option like sqlite's ".mode line"). Use command-line options to specify other output formats such as JSON, XML, YAML, S-expressions, CSV or TSV.
  • Don't produce binary output on stdout unless explicitly requested.
  • Print numbers unformatted unless requested otherwise. This might include internationalizing/localizing things like currency markers, thousands separators, and decimal separators. Or this might involve humanizing units, often with a -k (Kilobytes), -m (Megabytes), -g (Gigibytes), or -h/--humanize flag.
  • Offer formatting control for dates/time including internationalizing/localizing or explicitly specifying a format string. If using a format string, stick to one standard: preferrably strftime(3) strings (%Y-%m-%d %H:%M), but CLDR format date strings, or PHP style date format strings are also common in certain communities.

Command-line options & switches

  • Provide both short (-x) and long (--long-option) versions of options. On non-GNU systems, long options are less expected.
  • Use common command-line option conventions:
    • -h/--help should give an example of usage, a description of what the program does, along with a list of options and their descriptions. Note that -h may conflict with "human output".
    • -v/--version should give version information. Note that -v may conflict with verbosity flags.
    • If a command has side-effets such as deleting/renaming/modifying files or transferring large volumes of data, providea -n/--dry-run to let the user see what will happen without actually performing the requested action.
    • -- to separate options and arguments.
    • Use - as a file-name to read from stdin.
    • Use @filename to read data from filename instead of data provided directly on the command-line.
  • For long options that take arguments, allow either an equals sign or a space to separate the option from its arguments. E.g. --name=Sam or --name Sam.
  • It can be beneficial to provide an option to expose expected completions for shell command-line completion functions.

Defaults

  • If an option can have a default value, provide one.
  • If output is truncated to the width of the tty, use curses calls to determine the width and fall back to 80 columns otherwise.
  • If spawning an external editor, check the following environment variables and binaries in order
    1. $PROGNAME_EDITOR
    2. $VISUAL
    3. $EDITOR
    4. vi(1)
    5. ed(1)
    using the first one that matches.

Other considerations

  • Where possible, allow the user to specify options in any order. Requiring options in a particular order makes things more fragile.
  • If one tool provides multiple actions, use the form "programname {global options} action {action-specific options}". Examples include
    • git (git commit)
    • Subversion (svn commit)
    • Mercurial (hg commit)
    • CVS (cvs commit)
    • taskwarrior (task list)
    • managing services (service httpd start)
  • Use $LC_CTYPE to determine the encoding of stdin/stdout but allow the user to specify alternate encodings and error/replacement strategies.
  • Return a 0 exit status on success. Return a non-0 exit status if any errors occurred, preferrably using standard values from /usr/include/sysexits.h.
  • Store local user configuration in ~/.config/$PROGNAME/
  • Settings should be determined in the following order (from lowest priority to highest precedence)
    1. built in configuration
    2. system-wide configuration from either /etc/${PROGNAME}rc, /etc/${PROGNAME}.conf, /etc/${PROGNAME}/*, /usr/local/etc/${PROGNAME}rc or /usr/local/etc/${PROGNAME}.conf
    3. user configuration file in ~/.config/$PROGNAME/*
    4. environment variables
    5. command-line options
  • On Microsoft Windows, these conventions may differ. Notably, the use of /option:value instead of --option=value

Other resources