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
- $PROGNAME_EDITOR
- $VISUAL
- $EDITOR
- vi(1)
- ed(1)
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
)
- git (
- 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)
- built in configuration
-
system-wide configuration from either
/etc/${PROGNAME}rc
,/etc/${PROGNAME}.conf
,/etc/${PROGNAME}/*
,/usr/local/etc/${PROGNAME}rc
or/usr/local/etc/${PROGNAME}.conf
-
user configuration file in
~/.config/$PROGNAME/*
- environment variables
- command-line options
-
On Microsoft Windows,
these conventions may differ.
Notably, the use of
/option:value
instead of--option=value