- name
- cli-development
- description
- Best practices for building CLI applications across languages. Covers CLI design principles (Unix philosophy, command structure, subcommands vs flags), argument parsing (required/optional args, flags, environment variables, config files, precedence), user interface (help text, version info, progress indicators, color output, interactive prompts), output formatting (human-readable vs machine-readable JSON/YAML, exit codes), error handling (clear messages, suggestions, debug mode), cross-platform considerations (paths, line endings, terminal capabilities), testing strategies (integration tests, output verification, exit codes), documentation (README, man pages, built-in help), and language-specific libraries. Activate when working with CLI applications, command-line tools, argument parsing, CLI utilities, argument handling, commands, subcommands, CLI frameworks, or building command-line interfaces.
# CLI Development Guidelines
Best practices for designing and implementing command-line interface (CLI) applications across programming languages.
## CLI Design Principles
### Unix Philosophy
Follow the core Unix philosophy for robust, composable CLIs:
- **Do one thing well** - Each command should have a single, well-defined responsibility
- **Make it composable** - Design output to work as input to other programs
- **Handle text streams** - CLIs work with stdin/stdout/stderr
- **Exit cleanly** - Use appropriate exit codes (0 for success, non-zero for errors)
- **Fail fast** - Detect and report problems immediately
- **Be scriptable** - All functionality must be accessible from non-interactive contexts
### Command Structure
#### Verb-Noun Pattern
Organize commands using verb-noun relationships:
```
# Good: clear action and object
git add <file> # verb: add, noun: file
docker run <image> # verb: run, noun: image
npm install <package> # verb: install, noun: package
# Avoid: unclear structure
git foo # ambiguous
docker something # unclear what happens
```
#### Subcommands vs Flags
**Use subcommands when:**
- Commands have distinct behaviors or workflows
- Different sets of options apply to different operations
- You want clear logical grouping
```
git branch # subcommand
git branch --list # subcommand with flag
git branch --delete <name> # subcommand with flag and argument
```
**Use flags when:**
- Modifying behavior of a single operation
- Toggling optional features
- Providing configuration
```
ls --color # flag modifies ls behavior
grep -r --include # flags modify search behavior
```
**Never mix deeply:**
```
# Avoid: unclear hierarchy
tool --verbose subcommand --debug --mode strict
# Better: clear structure
tool subcommand --verbose --debug --mode strict
```
### Command Hierarchy
Keep hierarchy shallow (max 2-3 levels):
```
# Good: clear, discoverable
aws s3 ls
aws ec2 describe-instances
# Avoid: too deep
cloud provider storage list all buckets
```
## Argument Parsing
### Argument Types
**Positional Arguments**
- Required by default
- Appear in specific positions
- Use for primary input/object
```bash
cp <source> <destination> # Two required positional arguments
docker run <image> # One required positional argument
```
**Optional Arguments (with defaults)**
- Used less frequently
- Should have sensible defaults
- Clearly documented
```bash
npm install [directory] # Optional, defaults to current directory
grep [options] <pattern> [file] # File argument optional
```
### Flags and Options
**Short Flags (-f)**
- Single letter, used frequently
- For common operations
```bash
ls -l # long format
grep -r # recursive
tar -xvf # combined flags
```
**Long Flags (--flag)**
- Verbose, self-documenting
- For less-common operations
- Preferred in scripts
```bash
docker run --detach --name myapp
npm install --save-dev
grep --recursive --include="*.js"
```
**Flag Styles**
```
# Boolean flags
--verbose # boolean true/false
--color=always # explicit value
# Flags with values
--output file.txt # space-separated
--output=file.txt # equals-separated
# Accept both styles when possible
--timeout 30 or --timeout=30
```
### Environment Variables
**Convention:** Use SCREAMING_SNAKE_CASE for environment variables
```bash
export DEBUG=1
export LOG_LEVEL=debug
export API_TOKEN=secret
```
**Hierarchy (high to low precedence):**
1. Command-line flags (highest priority - most explicit)
2. Environment variables (middle - applies to multiple invocations)
3. Configuration file (lower - shared settings)
4. Built-in defaults (lowest priority - fallback)
**Example precedence:**
```bash
# If user runs with flag, it takes precedence
tool --timeout 10 # Uses 10, ignores TIMEOUT env var
# If no flag, check env var
export TIMEOUT=5
tool # Uses 5 from TIMEOUT
# If no flag or env var, use config file default
tool --config config.yaml # Reads timeout from config.yaml
# If nothing specified, use built-in default
tool # Uses hardcoded default (e.g., 30)
```
### Configuration Files
**Location convention:**
- Linux/macOS: `~/.config/app/config.yaml` or `~/.apprc`
- Windows: `%APPDATA%\App\config.yaml` or `~\AppData\Local\App\config.yaml`
- All platforms: `./config.yaml` (current directory, highest priority)
**Format preference:** YAML > TOML > JSON > INI
- YAML is human-readable and concise
- TOML is simpler than YAML but supports nested structures
- JSON is universal but verbose
- INI is legacy
**Example config file:**
```yaml
# ~/.config/myapp/config.yaml
debug: false
log_level: info
timeout: 30
output_format: json
color: true
api:
endpoint: https://api.example.com
timeout: 10
```
**Load with proper precedence:**
```python
# Python example
import os
from pathlib import Path
import yaml
def load_config():
# Built-in defaults
config = {
'debug': False,
'timeout': 30,
'color': True
}
# Load from config file
config_path = Path.home() / '.config' / 'myapp' / 'config.yaml'
if config_path.exists():
with open(config_path) as f:
file_config = yaml.safe_load(f)
config.update(file_config)
# Override with environment variables
if os.getenv('DEBUG'):
config['debug'] = os.getenv('DEBUG').lower() == 'true'
if os.getenv('TIMEOUT'):
config['timeout'] = int(os.getenv('TIMEOUT'))
# Note: Command-line args handled by argument parser, applied after
return config
```
## User Interface
### Help Text
**Provide comprehensive help:**
```bash
$ tool --help
Usage: tool [OPTIONS] COMMAND [ARGS]...
A brief description of what this tool does.
Options:
-v, --verbose Increase output verbosity
-q, --quiet Suppress non-error output
-h, --help Show this message and exit
--version Show version and exit
Commands:
add Add a new item
list List all items
delete Delete an item
config Manage configuration
Examples:
tool add myitem
tool list --format json
tool delete --force
For more information: https://docs.example.com
```
**Help text structure:**
- Usage line: `Usage: tool [OPTIONS] COMMAND [ARGS]...`
- Brief description (1-2 lines)
- Options section (sorted: common first)
- Commands section (if subcommands exist)
- Examples section (show common patterns)
- Additional resources (docs link, contact info)
**Per-command help:**
```bash
$ tool add --help
Usage: tool add [OPTIONS] NAME
Add a new item to the collection.
Options:
--description TEXT Item description
--tags TEXT Comma-separated tags
--priority INT Priority level (1-10)
-h, --help Show this message and exit
Examples:
tool add "My Item"
tool add "Task" --priority 5 --tags work,urgent
```
### Version Information
**Provide version with --version / -v:**
```bash
$ tool --version
tool 1.2.3
# For complex tools, show component versions
$ tool --version
tool 1.2.3
dependency-a: 2.1.0
dependency-b: 5.4.3
```
**Store version in configuration:**
```python
# __version__.py or __init__.py
__version__ = "1.2.3"
# In main CLI file
import sys
from . import __version__
@click.command()
@click.option('--version', is_flag=True, help='Show version and exit')
def main(version):
if version:
click.echo(f"tool {__version__}")
sys.exit(0)
```
### Progress Indicators
**For long operations, show progress:**
```python
# Python: using rich library
from rich.progress import track
import time
for i in track(range(100), description="Processing..."):
time.sleep(0.1) # Long operation
```
**Output format:**
```
Processing... [████████░░] 80%
or
Processing... ⠏ (spinning indicator)
```
### Spinners and Progress Bars
**Use spinners for indeterminate progress:**
```
Connecting to server... ⠋
Uploading file... ⠙
Waiting for response... ⠹
```
**Use progress bars for determinate progress:**
```
Downloading [████████░░░░░░] 40% (2.1 MB / 5.3 MB)
Processing [██████████████████░░] 90%
```
**Implement responsibly:**
- Disable in non-interactive environments (detect piped output)
- Respect `--no-progress` flag
- Never output progress to stdout (use stderr)
### Color Output
**Support color with disable option:**
```python
# Python: using rich or click
import click
@click.command()
@click.option('--color', type=click.Choice(['always', 'auto', 'never']),
default='auto', help='Color output mode')
def main(color):
# auto: color if terminal supports it
# always: force color (for piping to less -R)
# never: disable color
use_color = color == 'always' or (color == 'auto' and is_terminal())
```
**Environment variable override:**
```bash
# User can disable via environment
NO_COLOR=1 tool # Disable color
FORCE_COLOR=1 tool # Force color
```
**Terminal detection:**
```python
import sys
import os
def supports_color():
# Check NO_COLOR env var
if os.getenv('NO_COLOR'):
return False
# Check FORCE_COLOR env var
if os.getenv('FORCE_COLOR'):
return True
# Check if stdout is a TTY
return sys.stdout.isatty()
```
### Interactive Prompts
**Use for confirmations and user input:**
```python
# Python: using click
import click
@click.command()
@click.option('--force', is_flag=True, help='Skip confirmations')
def delete_item(force):
if not force:
if not click.confirm('Are you sure you want to delete?'):
click.echo("Cancelled")
return
# Proceed with deletion
click.echo("Deleted")
# Interactive selection
choice = click.prompt(
'Choose an option',
type=click.Choice(['a', 'b', 'c']),
default='a'
)
```
**Guidelines:**
- Ask for confirmation before destructive operations
- Provide sensible defaults
- Allow bypassing with `--force` flag
- Never prompt in non-interactive contexts (detect piped input)
## Output Formatting
### Human-Readable Output
**Default format for interactive use:**
```
$ tool list
Name Status Modified
────────────────────────────────
Project A Active 2 hours ago
Project B Inactive 3 days ago
Project C Active 1 week ago
```
**Characteristics:**
- Table format with clear columns
- Natural language (e.g., "2 hours ago")
- Colors for emphasis (when supported)
- Sorted intelligently
### Machine-Readable Output
**JSON format for scripting:**
```bash
$ tool list --format json
[
{
"name": "Project A",
"status": "active",
"modified": "2024-01-15T14:30:00Z"
},
{
"name": "Project B",
"status": "inactive",
"modified": "2024-01-12T09:15:00Z"
}
]
```
**YAML format (compact, readable):**
```bash
$ tool list --format yaml
- name: Project A
status: active
modified: 2024-01-15T14:30:00Z
- name: Project B
status: inactive
modified: 2024-01-12T09:15:00Z
```
**CSV format (for spreadsheets):**
```bash
$ tool list --format csv
name,status,modified
"Project A",active,2024-01-15T14:30:00Z
"Project B",inactive,2024-01-12T09:15:00Z
```
**Implementation:**
```python
import click
import json
import csv
import io
@click.command()
@click.option('--format', type=click.Choice(['table', 'json', 'yaml', 'csv']),
default='table', help='Output format')
def list_items(format):
items = get_items()
if format == 'json':
click.echo(json.dumps(items, indent=2))
elif format == 'csv':
output = io.StringIO()
عرض على GitHub