CLI Guidelines

Define standard options to create a consistent CLI experience across tools. When developers get used to certain option behavior, having them available across all tools makes things much easier & smoother. Conversely, when the same option name is used but with wildly different meanings, this can be very surprising for developers, and possibly lead them to do something destructive they didn't intend.

See the Chromite CLI Framework document for Chromite-specific APIs.

Short Options

Short options should be used prudently, after careful thought & consideration. Do not add them to long options purely for the sake of having a short option. Not every option needs a short option, plus the set of possible short options is significantly smaller than the set of possible long options (since a short option, practically speaking, can only be printable ASCII).

Short options should only be used when a developer is expected to type it often themselves (not for script usage), ideally should be “obvious” as to what long option it implies, and should be considered in context of other tools. See Required Option Conventions for more information.

All short options must provide a long option. See Long Option Naming Conventions for more details.

See Short Long Options as an alternative to using a short option.

Boolean Options

Long options that control boolean settings should provide both positive and negative options, and allow them to be specified multiple times. The default value should be clearly documented.

Naming

The negative option prefix is --no-. For example, --reboot and --no-reboot.

Do not omit the - after the no. This makes reading the option at a glance more difficult (e.g. --noclean), or end up making it look like a different word (e.g. is --nobody “no body”, or is it the “nobody” user account).

Do not use other prefix words like skip or set or disable or enable, nor use them in conjunction with no (e.g. --skip-reboot and --no-skip-reboot). This provides consistent naming & style for developers.

Internal Variables

Even when the default behavior is the negative value, the code should avoid negative variable names.

Python's argparse module makes it easy to support multiple boolean options that store the result in a specifically named variable. See the Example Code below.

Chaining

Specifying multiple boolean options should work fine, and should follow the standard “last option wins” policy. This makes it easy for developers to copy & paste long commands (e.g. from logs) and change options slightly by adding another flag to the end without having to scan the entire command line and edit it in the terminal.

For example:

  • --wipe: “wipe” is enabled.
  • --no-wipe: “wipe” is disabled.
  • --wipe --no-wipe: “wipe” is disabled.
  • --no-wipe --wipe: “wipe” is enabled.
  • --wipe --wipe --no-wipe --wipe --no-wipe: “wipe” is disabled.

Python's argparse module already behaves this way by default.

If you need to add more complicated options, such as aliases, you probably want to define a custom action when calling add_argument(). Custom actions are called immediately when processing which allows for updating the state rather than post-processing at the end. See the Example Code below.

Example Code

# Create a boolean option.  The default is None to indicate the user hasn't made
# a choice.  This can sometimes be useful when processing default behavior.  If
# the default should be explicit, use `default=...` with the --reboot option.
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--reboot', action='store_true')
parser.add_argument('--no-reboot', dest='reboot', action='store_false')
# Create boolean options with another option that implies others.  This is
# written so that multiple stacked options are handled correctly.  The custom
# action hooks directly into the option processing state machine.
class _ActionAliasForAB(argparse.Action):
  def __call__(self, parser, namespace, values, option_string=None):
    setattr(namespace, 'A', True)
    setattr(namespace, 'B', True)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--A', action='store_true')
parser.add_argument('--B', action='store_true')
parser.add_argument('--no-B', dest='B', action='store_false')
parser.add_argument('--alias-for-A-B', nargs=0, action=_ActionAliasForAB)

Long Option Separators

Long options use kebab-case, not snake_case, when separating words. For example, --a-long-option is correct while --a_long_option is not.

Since the option has to start with two dashes (--) and never two underscores (__), using dashes consistently is preferred.

Dashes are easier to type on the most common keyboard layouts our developers use (US English [QWERTY]) as _ requires holding the Shift key.

Do not omit separators entirely as it makes it hard for readers at a glance. Tryreadingthiswithoutseparators.

If backwards compatibility is a factor, both variants can usually be supported. Document the dashes (e.g. --an-opt) as the primary one, and list the underscores (e.g. --an_opt) as a deprecated compatibility form.

Long Option Naming Conventions

Long option names should use full words when possible and avoid unnecessary abbreviations or uncommon acronyms. The point of long options is to aid in clarity & readability (and logs), and abbreviations can often be inconsistent between tools.

Some examples:

  • Use --description, not --desc.
  • Use --message, not --msg.
  • Use --text, not --txt.

Short Long Options

While long options are great for scripts & automation, they can be painful for developers, especially for longer multi-word options. This can lead people to providing nonsensical short options simply so users don‘t have to type out the full long option. This is an anti-pattern and leads to inconsistent short options between tools, and makes developers have to read help/manuals constantly since they won’t be able to easily remember which option does what for every tool.

One alternative is that Python's argparse, and some other option parsing APIs, will automatically complete unambiguous partial long options for you. So if a CLI supports --description, but no other long option that starts with a --d, users can already use --d, --de, --desc, etc... for free. Keep in mind this should never be used in a script as there is no long term guarantee that these remain unambiguous. If --delicious is added later, then --d and --de are no longer unambiguous leading to errors (although --des, etc... still work).

Another alternative is to provide a terse short long option. Again, this is purely for users to type out, not to use in automation. These should only be provided as secondary aliases to the more formal option, and never as the only one per the naming conventions outlined above. This should also only be provided when demand suggests that it's an option that users will regularly use -- do not provide a terse short long option purely for the sake of it.

Some examples:

  • --description & --desc
  • --branch & --br

Required Option Conventions

Tools should adhere to these conventions whenever possible, and any deviation should be strongly reconsidered.

ShortLongDescription
--debugShow debugging information.
-f--forceForce an operation.
--formatControl output format.
-h--helpShow tool help/usage output.
-j--jobsControl parallelization.
-n--dry-runShow what would be done.
-q--quietQuiet(er) output.
-v--verboseVerbose(r) output.
--versionShow tool version information.
-y--yesSkip prompts.

--debug

Show internal debugging information that the user would find helpful. This may be very verbose.

--debug may be specified multiple times to make the output even debuggier.

The short option -d may be used depending on the tool, but usually enabling extra debugging is not a common operation.

--force

The user wants to bypass any safety checks. The help text should provide clear guidance on how this will affect behavior since it can potentially be very dangerous and lead to data loss.

For example, when imaging a device, do not prompt the user whether they really meant to erase the non-removable /dev/sda hard drive. Or if there are mismatches in hashes or other integrity checks, attempt to do the operation anyways (installing files, etc...).

Do not confuse this with the --yes option for agreeing to common prompts.

The short option -f may be used or omitted depending on how dangerous the option actually is in practice. If the tool might delete the user's desktop or source, probably safer to make them type out the full --force. If the tool would only delete some temporary files or something that could be recreated (even if it requires a bit of effort), then -f is OK.

--format

Control the output format of the tool.

Only add a single --format=<format> option. Avoid adding any --<format> options at all. The default should always be automatic, even if there aren't that many choices. This provides some future proofing as tooling evolves, and signals to users that they have to pick a specific format when writing scripts to avoid things breaking in the future.

Tools often handle a variety of formats, in their inputs & outputs, and with users wanting to explicitly control what is displayed to them. Providing automatic detection with reasonable default behavior is OK, but options should be provided for explicit control.

For example, the gerrit tool can output in JSON, markdown, and more. The default is automatic which detects whether to use readable human-centric summaries or terse raw data which is useful for developers running things directly, but can be a nightmare when attempting to write scripts on top of it.

Often tools start off with a single output format, and then someone requests a second one. It can be tempting to use an option name that matches the format, but this is inevitably a short term trap. For example, when adding a new JSON output format, you might start with --json. But when other formats come up, adding --markdown and such easily get out of hand, both for the user (lots of options in --help output), and for the tool author (who has to register each option and then handle conflicting --markdown --json situations).

While --format is most commonly associated with the output of a tool, this is not a strict requirement. If a tool never outputs anything and only takes an input, using --format is permissible. Use your best judgment.

Do not provide --fmt as a shorter name. It really doesn't save that many bytes compared to --format.

Format Names

Some common format names:

  • automatic (alias: auto): Good for switching between pretty and some other format (e.g. raw) depending on whether stdout is connected to a terminal. Good for sniffing input files and guessing at their format.
  • json: JSON output of some format; good for returning structured results. Some care should be taken to support backwards compatibility (e.g. always return an object with at least a version field), but this is not strictly required.
  • raw: A bare minimum output format useful for extracting the most common data that developers would want. e.g. gerrit --raw search only returns CL numbers with no metadata. This should never be structured content.
  • pretty: Good for humans to read, and never for machines to parse.

Multiple Streams

A single --format option is common when a tool only outputs to one place, and all of its inputs are of a specific format. When there are multiple inputs or outputs whose format can be controlled, add dedicated options to control each stream (using the pattern --<name>-format=<format>), with the --format option setting a common default.

For example, if there is a single input and a single output, --input-format=<format> & --output-format=<format> is a good choice, in addition to the common --format.

The ordering of options should not override each other. For example, using --output-format=foo --format=bar should behave the same as --format=bar --output-format=foo. In these cases, --format is only for selecting a default if a stream-specific format has not been specified.

Definitely do not provide separate format-specific options for each stream, otherwise the set of options really grows out of control. For example, do not use options like --input-json, --input-binary, --output-json, and --output-binary.

This has the nice benefit of disambiguating options that control paths and options that control formats. For example, is --input-json a boolean, or does it expect a path to a JSON file? Use --input <path> and --input-format=<format> instead.

--help

Show the tool usage, examples, and other helpful information. Since the user has explicitly requested the help output, this does not need to be terse. Normal output should only go to stdout, and the tool should exit 0.

When processing unknown or invalid options, it's OK to show a short summary of the specific option, or of valid options, but it should not be the full detailed output like --help. The user should be able to focus on what they got wrong, not wade throw pages of output to try and track down the one error line. This output should only go to stderr, and the tool should exit non-zero (either 1 or 64).

Use of the short -h option is recommended out of wide convention. Avoid reusing this for something else like --human, and definitely never use it for something destructive or unrecoverable.

--jobs

Limit how much parallelism should be used. This typically translates to how many threads or CPUs to utilize.

The special value 0 should be used to indicate “use all available CPU cores”.

The default does not have to always be 1 thus forcing users to manually pick a value, nor does it have to always be 0. Try to balance how expensive a particular job is (network, servers, I/O, RAM, etc...) with a reasonable default. For example, when talking to a network service, high values like 72 will probably cause many connections to be rejected so it won't be overwhelmed, so find a default that balances real world improvements (compared to -j1) with the server costs.

The default should rarely be hardcoded to a value above 1 -- run it through a max function with how many cores are available. For example, the default could be max(4, os.cpu_count()).

To determine how many CPU cores are available:

  • Python: os.cpu_count()
  • Shell: getconf _NPROCESSORS_CONF 2>/dev/null || echo 1

Use of the short -j option is recommended out of wide convention.

Do not use --cpus or --cores or --procs or --threads or similar names as the options should be user focused, not internal implementation details. In other words, users want to run multiple tasks in parallel, they don't care about matching jobs to hardware or OS level concepts. Especially considering internals change (multiprocessing vs multithreading), and the distinction between cores & cpus can be easily lost or irrelevant, and they might run more tasks than corresponding hardware is available. For example, using --cores=10 to run 10 slow I/O jobs in parallel on a system with 2 cpus is confusing.

--dry-run

Show what would be done, but don't actually “do” anything. Users should be able to add this option anywhere and expect that the tool will not make any changes anywhere. Basically this should always be a harmless idempotent operation.

Use of --dry-run is, by itself, not an error, thus it should normally exit 0. This allows the user to quickly detect problems before trying to make changes. For example, if invalid options were specified, the tool can show fatal errors. Or if inputs don't exist, or are corrupt, they may be diagnosed.

The tool may talk to network services, or otherwise make expensive computations, as long as it doesn't make any changes, and may be rerun many times.

The tool should display what it would have done were --dry-run not specified. This will often take the form like Would have run command: git push ....

Bypassing reasonable prompts is permitted as long as no changes are made. In other words, this may behave like --yes or --force in some situations. Use your best judgment as to what constitutes the best user experience.

Use of the short -n option is recommended out of wide convention, and because use of dry-run first is a common user flow.

The --dryrun option should be accepted as an alias to --dry-run, but should be omitted from documentation (i.e. --help output). This helps users who typo things, and because some tools have adopted that convention instead of --dry-run (although the latter is still more common).

Internally, code should prefer to use dryrun as the variable name rather than dry_run for consistency, and because it's easier to type.

--quiet

Make the output less verbose. General information should be omitted, and only display warnings or errors.

--quiet may be specified multiple times to make the output even quieter. Some recommended settings:

  • default: Show important events, warnings, and worse.
  • -q: Only show warnings and worse.
  • -qq: Only show (fatal) errors and worse.
  • -qqq: Don't show any output -- rely on exit status to indicate pass/fail.

Use of the short -q option is recommended out of wide convention.

If desired, --silent may be used as an alias to -qqq behavior; i.e. do not emit any output, only exit 0/non-zero.

--verbose

Make the output more verbose. This may include some helpful info or progress steps. Important information for the user should not be hidden behind --verbose; i.e. users should not be expected to use --verbose all the time.

--verbose may be specified multiple times to make the output even verboser.

Debugging information should not be included here -- use --debug instead. Use your best judgment as to what is verbose output and what is debug output.

Use of the short -v option is recommended out of wide convention, and because it can be common to type -vvv to quickly get more verbose output when trying to track down problems.

--version

Show version information for the current tool. Normal output should only go to stdout, and the tool should exit 0.

The default output should be short & to the point, and cheap to produce. This may include terse authorship information if desired.

If combined with --verbose, related package/tool information may be included.

If combined with --quiet, the output should be just the version number.

For example, common output might look like:

$ tool --version
CrOS tool v1.2.3
Written by some decent engineers.

$ tool --version --quiet
1.2.3

$ tool --version --verbose
CrOS tool v1.2.3
Current git repo is at add119cea9ebfd7ba89f7606ed04cac7cacaa43d.

Some important library information:
  foo lib v2
  another lib v3.4

Written by some decent engineers.

No short option should be provided for --version. It's not a common operation, so allocating one of the limited short options is a waste.

--yes

The user wants to “agree” to any standard prompts shown by the tool. This should not be used by itself to bypass safety checks -- see --force instead, with which this can be combined.

The prompts that would have been shown should still be emitted so it's clear what the user has agreed to, and include fake input. For example:

$ do-something --yes
Do you want to do something? (Y/n) <yes>

Use of the short -y option is recommended out of wide convention, and because skipping the same set of prompts is a common flow to avoid annoying users.