playbook/outfitter-agents/plugins/cli-dev/skills/cli-development-guidelines/references/REFERENCE.md

13 KiB
Raw Blame History

CLI Development Guidelines Reference

Scope and sources

This reference is a condensed, operational guide for building well-behaved CLI tools.

  • Primary source (adapted heavily): Command Line Interface Guidelines (https://clig.dev/)
    • License: CC BY-SA 4.0
    • Authors: Aanand Prasad, Ben Firshman, Carl Tashian, Eva Parish
  • Additional sources: POSIX utility conventions, GNU standards, Heroku CLI style guide, 12-factor CLI apps, XDG base directory spec, NO_COLOR convention.

Table of contents

Design principles

Human-first, but composable

  • Optimize the default UX for humans:
    • Clear, calm messages
    • Example-first help
    • Progress indicators for long operations
  • Still be composable in UNIX pipelines:
    • Clean stdout for data
    • Meaningful exit codes
    • No unexpected prompts in scripts

Consistency is a power tool

  • Prefer established CLI conventions when possible.
  • Be consistent within your tool:
    • Same option names mean the same thing everywhere.
    • Output formats don't randomly change between subcommands.

Say just enough

  • Too little:
    • Silent hangs
    • No confirmation that anything happened
  • Too much:
    • Verbose debug spew in normal mode
    • Walls of text hiding the one important line

Discovery beats memorization

  • --help should teach quickly.
  • Suggest the "next command" in multi-step workflows.

CLI as a conversation

  • Users will iterate: run → error → fix → run.
  • Respond like a helpful conversational partner:
    • Point out what went wrong
    • Suggest the simplest fix
    • Make it easy to learn the correct syntax

The basics: being a good CLI citizen

Use a parsing library

  • Don't hand-roll parsing, help formatting, or error rendering.
  • A good parser will usually also give you:
    • Help output
    • Unknown-flag handling
    • Sometimes: typo suggestions

Streams: stdout vs stderr

  • stdout
    • The command's primary output
    • Machine-readable output (piped into the next command)
  • stderr
    • Errors
    • Warnings
    • Progress / status messages
    • Human "what's happening" narration

Exit codes

  • 0 means success.
  • Non-zero means failure.
  • Prefer a small set of stable, documented failure codes over "random integers."
  • Consider reserving 2 for argument/usage errors.
  • If you need a more granular taxonomy, consider the BSD sysexits family (e.g., EX_USAGE = 64), but be aware that many tools simply use 1/2 in practice.

Help and documentation

Required behaviors

  • --help shows help and exits successfully.
  • Ideally also support -h (and do not overload it with a different meaning).
  • If your CLI has subcommands:
    • tool subcmd --help
    • tool help subcmd (optional but common in git-like tools)

Concise help by default (when invocation is incomplete)

If the user runs a command with missing required args/flags, print a concise help block:

  • What the tool does (one line)
  • 12 common examples
  • The most important flags (or a pointer to full help)
  • "Run --help for full usage"

Full help when asked

Full help should include:

  • Usage line(s)
  • Description
  • Commands (if any)
  • Options
  • Examples (lead with examples; users will copy-paste them)
  • Support path (issues / repo)
  • Link to web docs (especially to a subcommand anchor if you have it)

Formatting guidance

  • Use scan-friendly formatting:
    • Uppercased section headings
    • Alignment for options
    • Avoid ANSI escape sequences if help is piped (your output should not become "escape soup")

If stdin is required but not provided

If your tool expects piped input and stdin is a TTY, don't hang.

  • Print help or a clear message.
  • Exit non-zero.

Output, formatting, and modes

Human-readable output is the default

A practical heuristic:

  • If output is going to a TTY, it's probably a human.
  • If output is being captured/piped, it's probably a program.

Provide machine-readable output when it doesn't harm usability

Common patterns:

  • --json outputs structured JSON (stable shape, versioned if needed).
  • --plain outputs simple line/tabular output with one record per line.
  • Encourage scripts to use --json/--plain rather than scraping the human UI.

Keep success output brief, but not mysterious

  • Printing nothing can feel like "it hung."
  • Printing too much becomes noise.
  • If you changed state, tell the user what changed.

Color and symbols

  • Use color with intention:
    • Red for errors
    • Yellow for warnings
    • Highlight important parts only
  • Disable color when:
    • The relevant stream is not a TTY
    • NO_COLOR is set (non-empty)
    • TERM=dumb
    • User passes --no-color
  • Consider supporting FORCE_COLOR (some ecosystems use it), but don't let it break logs.

Animations and progress

  • Never animate when output is not a TTY.
  • If something takes "long," show progress.
  • If parallel work is happening, avoid interleaving chaos (multi-progress-bar libs help).

Paging

  • If output is long and you're on a TTY, consider a pager.
  • Respect PAGER if set.
  • A common less default is: less -FIRX
    • Doesn't page if one screen
    • Keeps formatting, doesn't clear screen on exit

Errors and diagnostics

Rewrite expected errors for humans

Don't dump raw stack traces for normal user errors.

  • Say what failed
  • Say why it might have failed (likely causes)
  • Say what to do next (actionable fix)

Keep signal-to-noise high

  • Group repetitive errors under one explanation.
  • Put the most important info at the end (recency bias in terminals is real).

Suggest corrections carefully

  • Typo suggestions are great when safe:
    • "Unknown command pss. Did you mean ps?"
  • Avoid "DWIM" behavior that silently changes meaning for destructive operations.

Unexpected errors

When something truly unexpected happens:

  • Provide a short human summary
  • Offer a way to get debug details:
    • --debug or --verbose
    • Optional log file path
  • Provide a bug report path and include reproducibility info

Arguments, flags, and subcommands

Prefer flags to positional args (usually)

  • Flags are self-documenting and easier to extend without breaking compatibility.
  • Exception: "classic" two-arg patterns (cp <src> <dst>) where brevity is worth it.

Provide long forms for all flags

  • If you have -h, also have --help.
  • Long forms are friendlier in scripts and documentation.

Reserve one-letter flags for truly common options

Short flags are a scarce resource. Spend them wisely.

Standard flag names (use existing conventions)

Common conventions across CLI ecosystems:

  • -h, --help
  • --version
  • -v, --verbose (but note ambiguity: sometimes -v is version)
  • -q, --quiet
  • -d, --debug
  • -f, --force
  • -n, --dry-run
  • --json
  • --no-input
  • --no-color
  • -o, --output

Order independence (when feasible)

Users often add flags to the end of the previous command via ↑. If possible, allow:

  • tool --flag subcmd
  • tool subcmd --flag

Subcommand naming

  • Avoid near-synonyms (update vs upgrade) unless the difference is extremely clear.
  • For object/action CLIs, noun verb is common:
    • docker container create
  • Keep verbs consistent across objects:
    • If you use create, also use delete/list/get consistently.

Interactivity and safety

Prompts only when stdin is a TTY

  • If stdin is not a TTY:
    • Fail with a clear message describing the required flag(s)
    • Do not block waiting for input that will never arrive

--no-input should disable prompts

  • If required info is missing:
    • Exit non-zero
    • Tell the user how to provide it via flags or stdin

Confirm dangerous operations

Different danger levels:

  • Mild:
    • Deleting an explicit file the user named
  • Moderate:
    • Bulk deletes, remote deletes, complex irreversible changes
  • Severe:
    • "Delete the whole app/account/project"
    • Require explicit confirmation:
      • Type the resource name, or
      • --confirm="exact-name"

Provide dry-run where it reduces fear

  • --dry-run should describe intended changes without doing them.

Configuration and environment variables

Choose the right configuration surface

  • Flags:
    • High variability per invocation
  • Environment variables:
    • Varies by execution context (shell/session/CI)
  • Project config file:
    • Stable for a project and shareable in version control
  • User config:
    • Stable per machine/user

Precedence (high → low)

A common, predictable precedence order:

  • Flags
  • Process environment
  • Project config (.env / tool config in repo)
  • User config
  • System config

XDG base directory spec

Prefer:

  • Config: $XDG_CONFIG_HOME (default ~/.config)
  • Data: $XDG_DATA_HOME (default ~/.local/share)
  • Cache: $XDG_CACHE_HOME (default ~/.cache)

Environment variable naming

  • Uppercase, numbers, underscores.
  • Prefer tool-specific prefixes:
    • MYTOOL_FOO=1

.env is not a real config system

.env is useful for small "context knobs," but it's limited:

  • Everything is a string
  • Often not versioned
  • Often abused for secrets

Secrets and sensitive data

  • Do not accept secrets via flags:
    • Leaks into shell history and process listings (ps)
  • Do not accept secrets via environment variables:
    • Easy to leak into logs, docker inspect, systemd unit displays, etc. Prefer:
  • --token-file path
  • --password-stdin
  • OS keychains / secret managers
  • Pipes and local IPC when appropriate

Robustness: timeouts, retries, signals

Responsive beats fast

  • Aim to print something within ~100ms for operations that might take time:
    • "Fetching…"
    • "Computing…"
    • "Connecting to …"

Timeouts and retries

  • Network requests should have timeouts.
  • Consider retries for transient failures (with backoff).
  • Make retries visible (don't silently hide minutes of retrying).

Recoverability and idempotence

  • If a command fails mid-way, a rerun should:
    • Pick up where it left off, or
    • Fail safely without corrupting state

Ctrl-C behavior

  • On SIGINT (Ctrl-C), stop quickly and say what happened.
  • If cleanup is long:
    • Allow a second Ctrl-C to force quit
    • Don't hang forever in cleanup

Future-proofing

  • Treat the CLI as a public API:
    • Commands, flags, output formats, config keys are all interfaces
  • Prefer additive changes:
    • New flags > changing behavior of old flags
  • Deprecate explicitly:
    • Warn when deprecated flags are used
    • Tell the user the replacement
  • Output for humans can evolve.
  • Output for scripts should be stabilized via --plain or --json.

Distribution and lifecycle

  • Prefer a single binary distribution if reasonable.
  • Make uninstall easy and documented.
  • Provide version output (--version).
  • Consider:
    • Man pages
    • Shell completions
    • Web docs with deep links to subcommands

Analytics and telemetry

  • Don't "phone home" without consent.
  • If you collect anything:
    • Explain what, why, how anonymized, and retention period
    • Make opting out easy Consider alternatives:
  • Instrument docs
  • Measure downloads
  • Talk to users

Implementation notes

Parser libraries (examples, not exhaustive)

  • Go: Cobra, urfave/cli
  • Rust: clap
  • Python: argparse, Click, Typer
  • Node: oclif, commander, yargs
  • Java: picocli
  • Kotlin: clikt
  • Swift: swift-argument-parser

Practical output design tip

When in doubt:

  • Human UI = defaults when TTY
  • Machine UI = explicit flags (--json, --plain)
  • Debug UI = explicit flags (--debug, --verbose)

Further reading