cli-design
npx skills add https://github.com/michaelliv/dotskills --skill cli-design
Agent 安装分布
Skill 文档
CLI Design Guidelines
Condensed from clig.dev â an open-source guide to writing better command-line programs, updating UNIX principles for the modern day.
When reviewing or building a CLI, work through each section below as a checklist. The examples are as important as the rules â they show what good looks like.
Core Philosophy
- Human-first design â If a command is used primarily by humans, design for humans first. Shed the baggage of machine-first UNIX conventions where they hurt usability.
- Simple parts that work together â Composability matters. Respect stdin/stdout/stderr, exit codes, signals, and line-based text. Use JSON when structure is needed. Your software will become a part in a larger system â your only choice is whether it will be a well-behaved part.
- Consistency across programs â Follow existing CLI conventions. Users have muscle memory. Break convention only with care and good reason. When convention would compromise usability, it might be time to break with it â but make the decision deliberately.
- Say just enough â A command hanging silently for minutes is as bad as dumping pages of debug output. Both leave the user confused. Clarity over volume.
- Ease of discovery â Comprehensive help, lots of examples, suggest next commands, suggest fixes on errors. Steal ideas from GUIs. Remember-and-type and see-and-point are not mutually exclusive.
- Conversation as the norm â CLI interaction is conversational. Users learn through trial-and-error, multi-step workflows (multiple
git adds thengit commit), exploration (cdandlsto understand a directory), and dry-runs before real runs. Guide users through the conversation â suggest corrections, show intermediate state, confirm before scary actions. At best, it’s a pleasant exchange that speeds them on their way. At worst, it’s hostile and makes them feel stupid. - Robustness â Both objective (handle unexpected input gracefully, be idempotent) and subjective (feel solid, not flimsy). Keep users informed, explain common errors, don’t print scary stack traces. Simplicity breeds robustness.
- Empathy â Give users the feeling you’re on their side, that you want them to succeed, that you’ve thought carefully about their problems. Delight means exceeding expectations at every turn.
- Chaos â The terminal is a mess. Inconsistencies are everywhere. Yet this chaos has been a source of power â few constraints means all manner of invention. Sometimes you should break the rules. Do so with intention and clarity of purpose. “Abandon a standard when it is demonstrably harmful to productivity or user satisfaction.” â Jef Raskin
The Basics
- Use a command-line argument parsing library. Don’t hand-roll flag parsing. Good ones: Commander.js (Node), Click/Typer (Python), Cobra (Go), clap (Rust), picocli (Java).
- Exit code 0 on success, non-zero on failure. Map non-zero codes to the most important failure modes.
- stdout for primary output and anything machine-readable (this is where piping sends things).
- stderr for log messages, errors, and diagnostics. When commands are piped together, stderr is displayed to the user, not fed into the next command.
Help
-
Display help on
-hand--help. For git-like tools, alsomyapp helpandmyapp help subcommand. -
No-args behavior: If the command requires arguments and gets none, display concise help â not full help. Include only:
- A description of what the program does
- One or two example invocations
- Key flags (unless there are too many)
- A pointer to
--helpfor more
jqdoes this well:$ jq jq - commandline JSON processor [version 1.6] Usage: jq [options] <jq filter> [file...] jq is a tool for processing JSON inputs, applying the given filter to its JSON text inputs and producing the filter's results as JSON on standard output. Example: $ echo '{"foo": 0}' | jq . { "foo": 0 } For a listing of options, use jq --help. -
Show full help on
--help. Ignore any other flags/args when-h/--helpis passed â you should be able to add-hto the end of anything and get help. Don’t overload-h. -
Lead with examples. Users prefer examples over abstract descriptions. Show common uses first, with actual output if it helps. Tell a story with a series of examples, building toward complex uses. Put exhaustive examples in a cheat sheet command or web page.
-
Display the most common flags and commands first in help text, not alphabetically. Git does this â it groups commands by workflow stage (“start a working area”, “work on the current change”, “examine the history and state”).
-
Use formatting (bold headings) for scannability, but in a terminal-independent way. When piped through a pager, emit no escape characters.
Heroku’s help is a good model:
$ heroku apps --help list your apps USAGE $ heroku apps OPTIONS -A, --all include apps in all teams -p, --personal list apps in personal account when a default team is set -s, --space=space filter by space -t, --team=team team to use --json output in json format EXAMPLES $ heroku apps === My Apps example example2 COMMANDS apps:create creates a new app apps:destroy permanently destroy an app apps:info show detailed app information -
Suggest corrections when the user mistyped a command. Ask, don’t auto-execute â invalid input might be a logical mistake, not a typo. And if you auto-correct silently, you’re committing to supporting that syntax forever.
$ heroku pss ⺠Warning: pss is not a heroku command. Did you mean ps? [y/n]: -
Provide a support path (website/GitHub link) in top-level help.
-
Link to web docs from help text. Deep-link to relevant subcommand pages.
-
If the command expects piped input and stdin is a TTY, show help immediately instead of hanging like
catdoes.
Documentation
Help text gives a brief, immediate sense of what the tool does. Documentation is the full detail â what it’s for, what it isn’t for, how everything works.
- Provide web-based documentation. People need to search for it online and link others to specific parts.
- Provide terminal-based documentation. Fast to access, stays in sync with the installed version, works offline.
- Consider man pages. Many users reflexively try
man mycmd. Use tools likeronnto generate them from Markdown. Also make terminal docs accessible via the tool itself (e.g.,npm help lsis equivalent toman npm-ls).
Output
- Human-readable output is paramount. Detect TTY to decide formatting. Humans first, machines second.
- Have machine-readable output where it doesn’t impact usability. Users should be able to pipe output to
grepand have it work as expected. “Expect the output of every program to become the input to another, as yet unknown, program.” â Doug McIlroy - Support
--plainfor plain, tabular text output (one record per line, no wrapping/splitting). Use this when human-readable formatting breaks machine-readable output. - Support
--jsonfor structured output. JSON integrates withjqand web services viacurl. - Display output on success, but keep it brief. Printing nothing is rarely the best default (it makes commands look broken), but err on the side of less. Offer
-q/--quietfor scripts to suppress non-essential output. - If you change state, tell the user. Explain what just happened so they can model the system in their head.
git pushis the gold standard:$ git push Enumerating objects: 18, done. Counting objects: 100% (18/18), done. Writing objects: 100% (10/10), 2.09 KiB | 2.09 MiB/s, done. To github.com:replicate/replicate.git + 6c22c90...a2a5217 bfirsh/fix-delete -> bfirsh/fix-delete - Make current state easy to see.
git statusis the model â it shows state and hints at next actions:$ git status On branch bfirsh/fix-delete Your branch is up to date with 'origin/bfirsh/fix-delete'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: cli/pkg/cli/rm.go no changes added to commit (use "git add" and/or "git commit -a") - Suggest next commands when commands form a workflow. This is how users discover functionality.
- Actions crossing program boundaries (reading/writing files not passed as args, network requests) should be explicit.
- Increase information density with ASCII art.
ls -lpermissions are a masterclass â scannable at a glance, more patterns emerge as you learn:-rw-r--r-- 1 root root 68 Aug 22 23:20 resolv.conf lrwxrwxrwx 1 root root 13 Mar 14 20:24 rmt -> /usr/sbin/rmt drwxr-xr-x 4 root root 4.0K Jul 20 14:51 security - Use color with intention. Highlight important info, red for errors. Don’t overuse â if everything is colored, color means nothing.
- Disable color when:
- stdout/stderr is not a TTY (check individually â colors on stderr are still useful when piping stdout)
NO_COLORenv var is set (non-empty)TERM=dumb--no-coloris passed- Consider also supporting a
MYAPP_NO_COLORenv var
- No animations when stdout isn’t a TTY. Prevents progress bars becoming Christmas trees in CI logs.
- Use symbols and emoji where they add clarity or structure, not as decoration.
yubikey-agentuses them well to break up a wall of text:$ yubikey-agent -setup ð The PIN is up to 8 numbers, letters, or symbols. Not just numbers! â The key will be lost if the PIN and PUK are locked after 3 incorrect tries. Choose a new PIN/PUK: Repeat the PIN/PUK: 𧪠Reticulating splines ⦠â Done! This YubiKey is secured and ready to go. ð¤ When the YubiKey blinks, touch it to authorize the login. - Don’t show internal debug info by default. If output only helps the developer, it shouldn’t be shown to users â verbose mode only.
- Don’t treat stderr like a log file â no
ERR,WARNlabels or extraneous context unless in verbose mode. - Use a pager (
less -FIRX) for long output, only when stdout is a TTY.-Fdoesn’t page if content fits one screen,-Iignores case in search,-Renables color,-Xleaves content on screen when quitting.
Errors
Errors are the most common reason users consult documentation. If you make errors into documentation, you save them enormous time.
- Catch errors and rewrite them for humans. Think of it as a conversation guiding the user in the right direction:
"Can't write to file.txt. You might need to make it writable by running 'chmod +w file.txt'." - Signal-to-noise ratio is crucial. Group similar errors under a single explanatory header. The more irrelevant output, the longer it takes users to find what went wrong.
- Put the most important information at the end of output â that’s where the eye lands. Use red sparingly and intentionally (it draws the eye).
- For unexpected errors, provide debug info and bug-report instructions â but consider writing debug logs to a file instead of printing to the terminal. Don’t overwhelm users with info they don’t understand.
- Make bug reports effortless. Provide a URL that pre-populates as much info as possible.
Arguments and Flags
Terminology: Arguments (args) are positional parameters â order matters (cp foo bar â cp bar foo). Flags are named parameters (-r, --recursive) â order generally doesn’t matter.
-
Prefer flags to args. Flags are self-documenting and easier to evolve. With args, it’s sometimes impossible to add new input without breaking existing behavior.
-
Full-length versions of all flags. Both
-hand--help. Long forms are self-documenting in scripts. -
Single-letter flags only for commonly used flags. Don’t pollute the short-flag namespace â you’ll need letters for flags you add later.
-
Multiple args are fine for the same kind of thing (e.g.,
rm file1 file2, works with globbing:rm *.txt). Two args for different things is usually wrong â exception: common primary actions likecp <src> <dest>. -
Use standard flag names when a standard exists:
Flag Meaning Examples -a, --allAll ps,fetchmail-d, --debugDebug output -f, --forceForce / skip confirmation rm -f--jsonJSON output -h, --helpHelp (only ever help) -n, --dry-runShow what would happen rsync,git add--no-inputDisable all prompts -o, --outputOutput file sort,gcc-p, --portPort psql,ssh-q, --quietLess output -u, --userUser ps,ssh--versionVersion -vAmbiguous (verbose or version) â prefer -dfor verbose -
Make the default right for most users. If a good UX is behind a flag, most users will never find it. If
lswere designed today, it would probably default tols -lhF. -
Prompt for missing input when interactive. But never require a prompt â always allow flags/args instead. Skip prompts entirely if stdin isn’t a TTY.
-
Confirm before dangerous actions â three levels:
- Mild (deleting a file): Optional prompt. If the command is already named “delete,” you probably don’t need to ask.
- Moderate (deleting a directory, remote resource, complex bulk modification): Prompt for confirmation. Offer
--dry-run. - Severe (deleting an entire app/server): Require typing something non-trivial (the resource name). Support
--confirm="name"for scriptability. - Watch for non-obvious destruction: changing a config value from 10 to 1 might implicitly delete 9 things. Treat this as severe.
-
Support
-to read from stdin / write to stdout when input/output is a file. Example:curl https://example.com/file.tar.gz | tar xvf - -
For optional flag values, use a special word like
noneâ not blank values (ambiguous). -
Make args, flags, and subcommands order-independent where possible. Users constantly hit up-arrow, add a flag at the end, and re-run. Don’t surprise them.
-
Never read secrets from flags â they leak into
psoutput and shell history. Accept via files (--password-file), stdin, or secret management.--password $(< file.txt)has the same leakage problems.
Interactivity
- Only prompt if stdin is a TTY. In pipes/scripts, throw an error telling the user what flag to pass.
--no-inputdisables all prompts explicitly. If the command requires input, fail and explain which flags to use.- Don’t echo passwords as the user types (turn off terminal echo).
- Let the user escape. Make it clear how to quit. Ctrl-C should always work. For wrapper programs (SSH, tmux, telnet), document the escape mechanism clearly. SSH uses
~escape sequences.
Subcommands
Use subcommands to reduce complexity of a sufficiently complex tool, or to combine closely related tools into one (RCS â Git). They’re useful for sharing global flags, help text, configuration, and storage.
- Be consistent across subcommands. Same flag names for the same things, similar output formatting.
- Consistent naming across levels. For
noun verbpatterns (e.g.,docker container create), use the same verbs across different nouns. Eithernoun verborverb nounworks, butnoun verbis more common. - Don’t have ambiguous or similarly-named commands. “update” vs. “upgrade” is confusing â use different words or disambiguate.
Robustness
- Validate user input early. Check and bail before anything bad happens, with understandable errors.
- Responsive > fast. Print something within 100ms. If you’re making a network request, print something before you do it so it doesn’t look broken.
- Show progress for long operations. Spinners, progress bars, estimated time remaining. An animated component reassures the user you haven’t crashed.
docker pullis the model:
When things go well, hide logs behind progress bars. But if there’s an error, print the logs â otherwise debugging is impossible.$ docker image pull ruby latest: Pulling from library/ruby 6c33745f49b4: Pull complete ef072fc32a84: Extracting [===============> ] 7.5MB/7.8MB f2ecc74db11a: Downloading [=========> ] 89MB/192MB b0efebc74f25: Downloading [==================> ] 19MB/22MB - Parallelize where possible, but keep output clean and non-interleaved. Use libraries â this is code you don’t want to write yourself.
- Set timeouts on network operations. Make them configurable with sane defaults. Don’t hang forever.
- Make it recoverable. If it fails transiently (network went down), re-running should pick up where it left off.
- Make it crash-only. If you can avoid cleanup after operations (or defer it to next run), the program can exit immediately on failure. This makes it both more robust and more responsive.
- Expect misuse. Scripts will wrap it, bad connections will interrupt it, many instances will run concurrently, unexpected environments will surprise you. (macOS filesystems are case-insensitive but case-preserving.)
Future-proofing
Subcommands, arguments, flags, config files, env vars â these are all interfaces. You’re committing to keeping them working.
- Keep changes additive. Add new flags rather than changing existing ones incompatibly.
- Warn before breaking changes. Deprecate with in-program warnings. Show migration path. Detect when users have already migrated and stop showing the warning.
- Human output can change. Encourage
--plainor--jsonin scripts to keep output stable. - No catch-all subcommand. Don’t assume the user means
runwhen the first arg isn’t a known subcommand. You can never add a subcommand with that name without breaking existing scripts. - No arbitrary abbreviations. Don’t let
mycmd iimplicitly meaninstallâ you can never add another command starting withi. Explicit, stable aliases are fine. - No time bombs. Will your command still work in 20 years? The server most likely to not exist in 20 years is the one you maintain right now.
Signals
- Ctrl-C (SIGINT): Exit ASAP. Print something immediately, before cleanup. Timeout cleanup code so it can’t hang forever.
- Second Ctrl-C: Skip long cleanup. Tell the user what the next Ctrl-C will do if it’s destructive.
$ docker-compose up ⦠^CGracefully stopping... (press Ctrl+C again to force) - Expect unclean starts. The program may start without prior cleanup having run.
Configuration
Configuration tiers â match the mechanism to the tier:
| Tier | Examples | Mechanism |
|---|---|---|
| Changes every invocation | Debug level, dry-run | Flags (+ maybe env vars) |
| Stable per-user, varies per-project | HTTP proxy, color settings, paths | Flags + env vars + .env |
| Stable per-project, all users | Build config, docker-compose, Makefile | Version-controlled config file |
- Precedence (highest to lowest): flags â shell env vars â project config (
.env) â user config â system config. - Follow XDG Base Directory spec for config file locations (
~/.config/myapp/). Supported by yarn, fish, neovim, tmux, and many others. - Ask consent before modifying config you don’t own. Prefer creating new config files (
/etc/cron.d/myapp) over appending to existing ones (/etc/crontab). If you must append, use a dated comment to delineate your additions.
Environment Variables
-
Names: uppercase letters, numbers, underscores only. No leading numbers.
-
Aim for single-line values. Multi-line values cause usability issues with
env. -
Don’t commandeer common names. Check the POSIX standard env var list.
-
Respect standard env vars:
Var Purpose NO_COLOR/FORCE_COLORDisable/force color DEBUGMore verbose output EDITORUser’s preferred editor HTTP_PROXY,HTTPS_PROXY,ALL_PROXY,NO_PROXYNetwork proxy SHELLUser’s preferred shell (for interactive sessions; use /bin/shfor scripts)TERM,TERMINFO,TERMCAPTerminal capabilities TMPDIRTemporary file location HOMEConfig file location PAGERPreferred pager for long output LINES,COLUMNSScreen size -
Read
.envfiles where appropriate for project-specific config. Many languages have libraries (Rust, Node, Ruby). -
Don’t use
.envas a substitute for proper config files. Limitations: not in source control (no history), string-only, poorly organized, encoding issues, often contains secrets that should be stored more securely. -
Never read secrets from env vars. They leak everywhere: child processes, logs,
docker inspect,systemctl show, shell substitutions inps. Accept secrets via credential files, pipes, AF_UNIX sockets, or secret management services.
Naming
- Simple, memorable word. Not too generic (both ImageMagick and Windows used
convert), not too long. - Lowercase only, dashes if needed.
curlyes,DownloadURLno. - Short but not too short. Very short names (
cd,ls,ps) are reserved for ubiquitous tools. - Easy to type. Consider hand ergonomics. Docker Compose was renamed from
plumtofigbecauseplumwas an awkward one-handed hopscotch on the keyboard.
Distribution
- Distribute as a single binary when possible. Use PyInstaller, pkg, or similar if your language doesn’t compile to binaries. If you can’t do a single binary, use the platform’s native package installer. Tread lightly on the user’s computer.
- Language-specific tools (linters, formatters) can assume the interpreter is installed.
- Make it easy to uninstall. Put uninstall instructions at the bottom of install instructions â one of the most common times people want to uninstall is right after installing.
Analytics
- Never phone home without consent. Be explicit about what you collect, why, how it’s anonymized, and retention period. Users will find out, and they will be angry.
- Prefer opt-in. If opt-out, clearly tell users on first run and make disabling easy.
- Consider alternatives: instrument web docs, instrument downloads, talk to users directly. Encourage feedback and feature requests in your docs and repos.
Before You Ship â Quick Checklist
Fundamentals
- Exit 0 on success, non-zero on failure
- Primary output â stdout, messages/errors â stderr
-
-hand--helpwork (and on every subcommand) - No args â concise help with examples, not an error or a hang
Help & Discovery
- Help leads with examples, not abstract descriptions
- Most common flags/commands listed first
- Typos get a “did you mean?” suggestion
- Support path (URL/GitHub) in top-level help
Output & Errors
- TTY detection drives formatting decisions (color, animations, pager)
- Color disabled when
NO_COLORset,TERM=dumb, not a TTY, or--no-color - State changes explained to the user
- Errors are human-readable with actionable suggestions
-
--jsonand/or--plainavailable for scripts -
-q/--quietsuppresses non-essential output
Flags & Input
- Every flag has a
--long-form - Short flags reserved for common operations only
- Standard flag names used where applicable (
--force,--dry-run,--quiet, etc.) - Secrets never accepted via flags or env vars â use files, stdin, or secret managers
- Prompts only when stdin is a TTY;
--no-inputdisables all prompts - Dangerous actions require confirmation (severity-appropriate)
Robustness
- Something prints within 100ms â no silent hangs
- Long operations show progress (spinner, bar, ETA)
- Network operations have configurable timeouts
- Re-running after transient failure picks up where it left off
- Ctrl-C exits immediately; second Ctrl-C skips cleanup
Future-proofing
- No catch-all subcommand
- No implicit abbreviations of subcommands
- Changes are additive; breaking changes are warned in advance
- No dependency on external servers that may disappear