A comprehensive guide to what NOT to do when building command-line interfaces
These anti-patterns represent common mistakes that frustrate users and make CLIs harder to use. Each pattern includes the problem, why it's harmful, and how to fix it.
The Mistake:
$ mkcfg 3 prod ~/config
$ dpl -e p -SWhy It's Bad:
- Cryptic abbreviations confuse users
- Positional arguments are hard to remember
- No clear indication of what will happen
The Fix:
$ myapp config create --type production --output ~/config
$ myapp deploy --environment production --skip-testsReal Example: Early versions of kubectl used kubectl run for multiple purposes (creating deployments, jobs, pods). They fixed it by separating into clear commands: kubectl create deployment, kubectl create job.
The Mistake:
$ myapp --help
Usage: myapp [--verbose|-v] [--quiet|-q] [--config|-c FILE] [--no-color]
[--format|-f FORMAT] [--output|-o FILE] [--input|-i FILE] [--recursive|-r]
[--force] [--dry-run] [--yes|-y] [--timeout DURATION] [--retries N]
[--parallel|-p N] [--profile PROFILE] [--region REGION] [--api-key KEY]
[--api-secret SECRET] [--endpoint URL] [--insecure] [--cert FILE]
[--key FILE] [--ca FILE] [--help|-h] [--version] COMMAND [ARGS...]Why It's Bad:
- Overwhelming and hard to scan
- No visual hierarchy
- Users can't find what they need
The Fix:
$ myapp --help
Usage: myapp [options] <command> [args]
Commands:
init Initialize a new project
deploy Deploy your application
status Check deployment status
Common options:
-h, --help Show help
-v, --version Show version
Run 'myapp <command> --help' for command detailsThe Mistake:
$ myapp deploy
Error: ENOENT
$ myapp init
Error: Operation failed
$ myapp config set key
Error: Invalid inputWhy It's Bad:
- Doesn't explain what went wrong
- No guidance on how to fix it
- Frustrates users
The Fix:
$ myapp deploy
Error: No config file found
Could not find 'myapp.config.yml' in current directory.
To fix this:
1. Run 'myapp init' to create a config file
2. Or specify a config with --config <path>
$ myapp config set key
Error: Missing value for 'key'
Usage: myapp config set <key> <value>
Example:
myapp config set api.endpoint https://api.example.comThe Mistake:
$ myapp process data.json
$ echo $?
0
$ ls
data.json # No output file, but no error either!Why It's Bad:
- Users think operation succeeded
- Wastes time debugging
- Breaks automation
The Fix:
$ myapp process data.json
Error: Failed to process data.json
Input file is empty (0 bytes)
$ echo $?
1The Mistake:
$ myapp create --projectName my-project
$ myapp delete -proj my-project
$ myapp list --project_type=web
$ myapp status -p:my-projectWhy It's Bad:
- Users can't remember the right format
- Breaks muscle memory
- Looks unprofessional
The Fix:
$ myapp create --project-name my-project
$ myapp delete --project my-project
$ myapp list --project-type web
$ myapp status --project my-project
# Always use:
# - Lowercase with hyphens for long flags
# - Single letters for short flags
# - Consistent naming across commandsThe Mistake:
$ myapp process large-dataset.csv
# Nothing happens for 5 minutes...
# Is it working? Frozen? How long will it take?Why It's Bad:
- Users think it's broken
- Can't estimate completion time
- Leads to Ctrl+C interruptions
The Fix:
$ myapp process large-dataset.csv
Processing dataset (2.3GB)...
[████████████░░░░░░░░] 60% | 1.4GB/2.3GB | ETA: 2m 15sThe Mistake:
$ myapp deploy
Initializing...
Loading configuration...
Validating settings...
Checking dependencies...
Preparing artifacts...
Building container...
Optimizing assets...
Compressing files...
WARNING: No backup configured - data loss possible!
Uploading to server...
Configuring services...
Starting application...
Deployment complete!Why It's Bad:
- Critical warning hidden in output
- Users miss important information
- Can lead to data loss
The Fix:
$ myapp deploy
⚠ WARNING: No backup configured - data loss possible!
Configure backups with: myapp config set backup.enabled true
Continue deployment? [y/N]: _The Mistake:
$ myapp list
Error: Config file not found
Please create .myapp.yml
$ echo 'version: 1' > .myapp.yml
$ myapp list
Error: Invalid config: missing required field 'api_key'Why It's Bad:
- Can't try the tool quickly
- Requires setup before basic operations
- Frustrating onboarding
The Fix:
$ myapp list
Using default settings (no config file found)
Projects:
- my-project
- another-project
Tip: Run 'myapp init' to create a config fileThe Mistake:
$ myapp /h # Instead of --help
$ myapp -version # Instead of --version
$ myapp quit # Instead of exit code
$ myapp --yes=true # Instead of just --yesWhy It's Bad:
- Breaks user expectations
- Harder to learn
- Incompatible with shell conventions
The Fix:
$ myapp --help # or -h
$ myapp --version # or -v
$ myapp && echo "Success"
$ myapp --yes # Boolean flags don't need valuesThe Mistake:
$ myapp transform --help
Usage: myapp transform [options] <input> <output>
Options:
-f, --format FORMAT Output format
-t, --template FILE Template file
-v, --variables FILE Variables file
-s, --strict Strict modeWhy It's Bad:
- Abstract descriptions aren't helpful
- Users have to guess usage
- Increases support burden
The Fix:
$ myapp transform --help
Transform files using templates
Usage: myapp transform [options] <input> <output>
Options:
-f, --format FORMAT Output format (json, yaml, xml)
-t, --template FILE Template file path
-v, --variables FILE Variables file for template
Examples:
# Transform JSON to YAML
myapp transform data.json data.yaml --format yaml
# Apply template with variables
myapp transform input.json output.html \
--template report.tmpl \
--variables vars.json# Bad: Unix-only paths
$ myapp --config ~/.myapp/config
# Good: Cross-platform paths
$ myapp --config $HOME/.myapp/config# Bad: Breaks in pipes
$ myapp list | grep active
Error: Cannot use color output in pipe
# Good: Detects pipe automatically
$ myapp list | grep active # Works, no color- Write help text first - If it's hard to explain, it's hard to use
- Test with new users - Watch them struggle, fix the pain points
- Follow conventions - Don't reinvent the wheel
- Start simple - You can always add features later
- Handle all error cases - Every error needs a helpful message
- Add progress indicators - Any operation over 1 second
- Support scripting - Exit codes, quiet mode, JSON output
- Test edge cases - Empty files, network failures, interruptions
- Review all error messages - Are they helpful?
- Check platform compatibility - Windows, Mac, Linux
- Verify pipe behavior - Does it work in scripts?
- Get user feedback - Beta test with real users
If you remember nothing else:
- Clear is better than clever
- Examples are better than descriptions
- Errors should help, not frustrate
- Follow existing conventions
- Test with real users
The Mistake: Using different terms for the same concept across commands.
# Different words for the same action
$ myapp new project
$ myapp create-user john
$ myapp init config
$ myapp generate reportWhy It's Bad:
- Users can't predict command names
- Harder to discover features
- Increases cognitive load
The Fix: Pick one verb and stick with it:
$ myapp create project
$ myapp create user john
$ myapp create config
$ myapp create reportThe Mistake:
$ deploy-tool production api-server v2.1.0 true false 3 /tmp/logs
# What does each argument mean?Why It's Bad:
- Impossible to remember argument order
- Easy to swap arguments accidentally
- No self-documentation
The Fix:
$ deploy-tool \
--environment production \
--service api-server \
--version v2.1.0 \
--skip-tests \
--no-backup \
--replicas 3 \
--log-dir /tmp/logs13. ❌ Hidden Dependencies
The Mistake:
$ myapp start
Error: jq not found in PATHWhy It's Bad:
- Surprises users at runtime
- No indication of what's needed
- Breaks in minimal environments
The Fix:
$ myapp doctor
Checking dependencies...
✓ git (2.34.0)
✗ jq (required for JSON processing)
✓ curl (7.81.0)
To install missing dependencies:
brew install jq # macOS
apt install jq # Ubuntu/DebianThe Mistake:
$ cleanup
Deleting all files in /home/user...
[No confirmation, starts immediately]Why It's Bad:
- Data loss without warning
- No chance to reconsider
- Violates principle of least surprise
The Fix:
$ cleanup
This will delete 1,234 files (5.6 GB) from /home/user
Files to be deleted:
• *.tmp (456 files)
• *.log (678 files)
• cache/* (100 files)
Continue? [y/N]: _The Mistake:
$ myapp set-env production
$ myapp set-region us-west
$ myapp set-profile admin
$ myapp deploy # Uses hidden state from above commandsWhy It's Bad:
- Hidden state causes surprises
- Hard to reproduce commands
- Context not clear from command alone
The Fix:
# Explicit context
$ myapp deploy --env production --region us-west --profile admin
# Or with context command
$ myapp --context prod-west deployThe Mistake:
$ build
# Detects project type and runs different commands
# Node.js → npm build
# Python → python setup.py build
# Go → go build
# But with different behaviors and options!Why It's Bad:
- Unpredictable behavior
- Hard to debug when wrong
- Documentation nightmare
The Fix:
$ build --type node
$ build --type python
# Or better: use native tools
$ npm build
$ python setup.py buildThe Mistake:
$ app --verbose --debug --trace --log-level=debug \
--output-format=json --pretty --color --progress \
--timeout=30 --retry=3 --retry-delay=5 \
--config=./app.yml --env-file=.env \
--profile=dev --context=test \
actionWhy It's Bad:
- Overwhelming option count
- Overlapping functionality
- Hard to remember all flags
The Fix: Group related options and use config files:
$ app action --profile dev # Profile includes all settings
$ app action -vvv # Verbosity levels instead of multiple flagsThe Mistake:
$ migrate-database --from v1 --to v2
Migrating production database...
[Already running, no way to preview]Why It's Bad:
- No way to preview changes
- Can't verify before execution
- Risky for production operations
The Fix:
$ migrate-database --from v1 --to v2 --dry-run
Would perform the following migrations:
1. Add column 'email_verified' to users table
2. Create index on orders.created_at
3. Drop deprecated 'legacy_data' table
No changes made. Run without --dry-run to apply.The Mistake:
$ myapp list | grep active
Error: Cannot use interactive mode when pipedWhy It's Bad:
- Breaks Unix philosophy
- Prevents automation
- Forces workarounds
The Fix:
$ myapp list | grep active
active-service-1
active-service-2
# Auto-detect pipe and adjust output
if [ -t 1 ]; then
# Terminal: use colors and formatting
else
# Pipe: plain text output
fiThe Mistake:
$ myapp version
2.1.0
$ myapp --version
myapp v2.1.0
$ myapp -v
Verbose mode enabled... # -v is verbose, not version!Why It's Bad:
- Inconsistent version flags
- Conflicts with common conventions
- Confuses users
The Fix:
$ myapp --version
myapp version 2.1.0
$ myapp version
myapp version 2.1.0
Build: 2023-10-15T10:30:00Z
Commit: abc123def
$ myapp -V # Capital V for version if -v is verbose
myapp version 2.1.0- Mystery meat commands
- Positional argument hell
- Flag soup
- Inconsistent terminology
- Wall of text syndrome
- Buried important information
- Ignored piping
- No progress feedback
- Unhelpful error messages
- Silent failures
- Missing error recovery
- Poor error codes
- Required config files
- Hidden dependencies
- State soup
- Magical behavior
- Destructive defaults
- Missing dry run
- No confirmations
- No undo/rollback
- Non-standard conventions
- Version hell
- Inconsistent flag styles
- Platform-specific assumptions
Before releasing your CLI, ensure you're NOT doing any of these:
Classic Mistakes:
- Using cryptic abbreviated commands
- Requiring positional arguments for everything
- Showing walls of text without structure
- Giving unhelpful error messages
- Failing silently without feedback
- Using inconsistent flag formats
- Requiring configuration before basic use
- Performing destructive actions without confirmation
- Ignoring pipe detection
- Breaking platform conventions
Common Issues:
- Hiding important warnings in output
- Using hidden global state
- Having unclear command purposes
- Missing --help on commands
- Lacking examples in help text
- CLI Principles - The right way to design CLIs
- Design Patterns - Proven patterns that work
- Quick Start - Build your first CLI right
Remember: Every anti-pattern here was discovered through real user frustration. Learn from these mistakes to build CLIs that users love rather than tolerate.