cli-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCLI Development Guidelines
CLI开发指南
Best practices for designing and implementing command-line interface (CLI) applications across programming languages.
跨编程语言设计与实现命令行界面(CLI)应用程序的最佳实践。
CLI Design Principles
CLI设计原则
Unix Philosophy
Unix哲学
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
遵循核心Unix哲学,构建健壮、可组合的CLI:
- 专注做好一件事 - 每个命令应具备单一、明确的职责
- 支持组合使用 - 设计输出格式以适配其他程序的输入
- 处理文本流 - CLI基于stdin/stdout/stderr工作
- 干净退出 - 使用合适的退出码(0表示成功,非零表示错误)
- 快速失败 - 立即检测并报告问题
- 支持脚本化 - 所有功能必须可通过非交互式上下文访问
Command Structure
命令结构
Verb-Noun Pattern
动词-名词模式
Organize commands using verb-noun relationships:
undefined使用动词-名词关系组织命令:
undefinedGood: 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
git add <file> # 动词: add,名词: file
docker run <image> # 动词: run,名词: image
npm install <package> # 动词: install,名词: package
Avoid: unclear structure
避免:结构模糊
git foo # ambiguous
docker something # unclear what happens
undefinedgit foo # 含义不明确
docker something # 无法清晰了解操作内容
undefinedSubcommands 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 argumentUse 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 behaviorNever mix deeply:
undefined使用子命令的场景:
- 命令具有截然不同的行为或工作流
- 不同操作适用不同的选项集合
- 需要清晰的逻辑分组
git branch # 子命令
git branch --list # 带标志的子命令
git branch --delete <name> # 带标志和参数的子命令使用标志的场景:
- 修改单一操作的行为
- 切换可选功能
- 提供配置项
ls --color # 标志修改ls的行为
grep -r --include # 标志修改搜索行为切勿深度混合:
undefinedAvoid: unclear hierarchy
避免:层级模糊
tool --verbose subcommand --debug --mode strict
tool --verbose subcommand --debug --mode strict
Better: clear structure
推荐:结构清晰
tool subcommand --verbose --debug --mode strict
undefinedtool subcommand --verbose --debug --mode strict
undefinedCommand Hierarchy
命令层级
Keep hierarchy shallow (max 2-3 levels):
undefined保持层级简洁(最多2-3级):
undefinedGood: clear, discoverable
推荐:清晰、易发现
aws s3 ls
aws ec2 describe-instances
aws s3 ls
aws ec2 describe-instances
Avoid: too deep
避免:层级过深
cloud provider storage list all buckets
undefinedcloud provider storage list all buckets
undefinedArgument 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 argumentOptional 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位置参数
- 默认必填
- 出现在特定位置
- 用于主要输入/对象
bash
cp <source> <destination> # 两个必填位置参数
docker run <image> # 一个必填位置参数可选参数(带默认值)
- 使用频率较低
- 应具备合理的默认值
- 需清晰文档说明
bash
npm install [directory] # 可选参数,默认值为当前目录
grep [options] <pattern> [file] # 文件参数为可选Flags and Options
标志与选项
Short Flags (-f)
- Single letter, used frequently
- For common operations
bash
ls -l # long format
grep -r # recursive
tar -xvf # combined flagsLong 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
undefined短标志 (-f)
- 单个字母,高频使用
- 适用于常见操作
bash
ls -l # 长格式
grep -r # 递归搜索
tar -xvf # 组合标志长标志 (--flag)
- 描述性强,自文档化
- 适用于低频操作
- 脚本中优先使用
bash
docker run --detach --name myapp
npm install --save-dev
grep --recursive --include="*.js"标志格式
undefinedBoolean flags
布尔标志
--verbose # boolean true/false
--color=always # explicit value
--verbose # 布尔值true/false
--color=always # 显式指定值
Flags with values
带值的标志
--output file.txt # space-separated
--output=file.txt # equals-separated
--output file.txt # 空格分隔
--output=file.txt # 等号分隔
Accept both styles when possible
尽可能同时支持两种格式
--timeout 30 or --timeout=30
undefined--timeout 30 或 --timeout=30
undefinedEnvironment Variables
环境变量
Convention: Use SCREAMING_SNAKE_CASE for environment variables
bash
export DEBUG=1
export LOG_LEVEL=debug
export API_TOKEN=secretHierarchy (high to low precedence):
- Command-line flags (highest priority - most explicit)
- Environment variables (middle - applies to multiple invocations)
- Configuration file (lower - shared settings)
- Built-in defaults (lowest priority - fallback)
Example precedence:
bash
undefined约定: 使用SCREAMING_SNAKE_CASE格式命名环境变量
bash
export DEBUG=1
export LOG_LEVEL=debug
export API_TOKEN=secret优先级(从高到低):
- 命令行标志(最高优先级 - 最显式)
- 环境变量(中间优先级 - 适用于多次调用)
- 配置文件(较低优先级 - 共享设置)
- 内置默认值(最低优先级 - 回退方案)
优先级示例:
bash
undefinedIf user runs with flag, it takes precedence
如果用户使用标志调用,标志优先级最高
tool --timeout 10 # Uses 10, ignores TIMEOUT env var
tool --timeout 10 # 使用10,忽略TIMEOUT环境变量
If no flag, check env var
如果没有标志,检查环境变量
export TIMEOUT=5
tool # Uses 5 from TIMEOUT
export TIMEOUT=5
tool # 使用TIMEOUT环境变量的值5
If no flag or env var, use config file default
如果没有标志或环境变量,使用配置文件中的默认值
tool --config config.yaml # Reads timeout from config.yaml
tool --config config.yaml # 从config.yaml读取timeout值
If nothing specified, use built-in default
如果未指定任何内容,使用内置默认值
tool # Uses hardcoded default (e.g., 30)
undefinedtool # 使用硬编码的默认值(例如30)
undefinedConfiguration Files
配置文件
Location convention:
- Linux/macOS: or
~/.config/app/config.yaml~/.apprc - Windows: or
%APPDATA%\App\config.yaml~\AppData\Local\App\config.yaml - All platforms: (current directory, highest priority)
./config.yaml
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
undefined位置约定:
- Linux/macOS: 或
~/.config/app/config.yaml~/.apprc - Windows: 或
%APPDATA%\App\config.yaml~\AppData\Local\App\config.yaml - 全平台: (当前目录,优先级最高)
./config.yaml
格式偏好: YAML > TOML > JSON > INI
- YAML可读性强且简洁
- TOML比YAML更简单,但支持嵌套结构
- JSON通用性强但冗长
- INI为遗留格式
配置文件示例:
yaml
undefined~/.config/myapp/config.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:**
```pythondebug: false
log_level: info
timeout: 30
output_format: json
color: true
api:
endpoint: https://api.example.com
timeout: 10
**按优先级加载:**
```pythonPython example
Python示例
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 configundefinedimport os
from pathlib import Path
import yaml
def load_config():
# 内置默认值
config = {
'debug': False,
'timeout': 30,
'color': True
}
# 从配置文件加载
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)
# 用环境变量覆盖
if os.getenv('DEBUG'):
config['debug'] = os.getenv('DEBUG').lower() == 'true'
if os.getenv('TIMEOUT'):
config['timeout'] = int(os.getenv('TIMEOUT'))
# 注意:命令行参数由参数解析器处理,在最后应用
return configundefinedUser 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.comHelp 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提供全面的帮助信息:
bash
$ tool --help
Usage: tool [OPTIONS] COMMAND [ARGS]...
本工具的简要功能描述。
Options:
-v, --verbose 增加输出详细程度
-q, --quiet 抑制非错误输出
-h, --help 显示此帮助信息并退出
--version 显示版本信息并退出
Commands:
add 添加新条目
list 列出所有条目
delete 删除条目
config 管理配置
Examples:
tool add myitem
tool list --format json
tool delete --force
更多信息:https://docs.example.com帮助文本结构:
- 使用行:
Usage: tool [OPTIONS] COMMAND [ARGS]... - 简要描述(1-2行)
- 选项部分(按常用程度排序)
- 命令部分(如果存在子命令)
- 示例部分(展示常见使用模式)
- 附加资源(文档链接、联系信息)
单命令帮助:
bash
$ tool add --help
Usage: tool add [OPTIONS] NAME
向集合中添加新条目。
Options:
--description TEXT 条目描述
--tags TEXT 逗号分隔的标签
--priority INT 优先级级别(1-10)
-h, --help 显示此帮助信息并退出
Examples:
tool add "My Item"
tool add "Task" --priority 5 --tags work,urgentVersion Information
版本信息
Provide version with --version / -v:
bash
$ tool --version
tool 1.2.3通过--version / -v提供版本信息:
bash
$ tool --version
tool 1.2.3For 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$ tool --version
tool 1.2.3
dependency-a: 2.1.0
dependency-b: 5.4.3
**在配置中存储版本:**
```pythonversion.py or init.py
version.py 或 init.py
version = "1.2.3"
version = "1.2.3"
In main CLI file
在主CLI文件中
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)
undefinedimport sys
from . import version
@click.command()
@click.option('--version', is_flag=True, help='显示版本信息并退出')
def main(version):
if version:
click.echo(f"tool {version}")
sys.exit(0)
undefinedProgress Indicators
进度指示器
For long operations, show progress:
python
undefined针对长时间操作,显示进度:
python
undefinedPython: using rich library
Python: 使用rich库
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)
undefinedfrom rich.progress import track
import time
for i in track(range(100), description="处理中..."):
time.sleep(0.1) # 长时间操作
**输出格式:**处理中... [████████░░] 80%
或
处理中... ⠏ (旋转指示器)
undefinedSpinners 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 flag
--no-progress - Never output progress to stdout (use stderr)
针对不确定进度使用加载动画:
连接到服务器... ⠋
上传文件... ⠙
等待响应... ⠹针对确定进度使用进度条:
下载中 [████████░░░░░░] 40% (2.1 MB / 5.3 MB)
处理中 [██████████████████░░] 90%合理实现:
- 在非交互式环境中禁用(检测管道输出)
- 尊重标志
--no-progress - 切勿将进度输出到stdout(使用stderr)
Color Output
彩色输出
Support color with disable option:
python
undefined支持彩色输出并提供禁用选项:
python
undefinedPython: using rich or click
Python: 使用rich或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:**
```bashimport click
@click.command()
@click.option('--color', type=click.Choice(['always', 'auto', 'never']),
default='auto', help='彩色输出模式')
def main(color):
# auto: 终端支持则启用彩色
# always: 强制启用彩色(适用于管道到less -R)
# never: 禁用彩色
use_color = color == 'always' or (color == 'auto' and is_terminal())
**环境变量覆盖:**
```bashUser 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()NO_COLOR=1 tool # 禁用彩色
FORCE_COLOR=1 tool # 强制启用彩色
**终端检测:**
```python
import sys
import os
def supports_color():
# 检查NO_COLOR环境变量
if os.getenv('NO_COLOR'):
return False
# 检查FORCE_COLOR环境变量
if os.getenv('FORCE_COLOR'):
return True
# 检查stdout是否为TTY
return sys.stdout.isatty()Interactive Prompts
交互式提示
Use for confirmations and user input:
python
undefined用于确认操作和用户输入:
python
undefinedPython: using click
Python: 使用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")import click
@click.command()
@click.option('--force', is_flag=True, help='跳过确认步骤')
def delete_item(force):
if not force:
if not click.confirm('确定要删除吗?'):
click.echo("已取消")
return
# 执行删除操作
click.echo("已删除")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)choice = click.prompt(
'选择一个选项',
type=click.Choice(['a', 'b', 'c']),
default='a'
)
**指导原则:**
- 执行破坏性操作前要求确认
- 提供合理的默认值
- 允许通过`--force`标志跳过提示
- 切勿在非交互式上下文中弹出提示(检测管道输入)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 agoCharacteristics:
- Table format with clear columns
- Natural language (e.g., "2 hours ago")
- Colors for emphasis (when supported)
- Sorted intelligently
交互式使用的默认格式:
$ tool list
名称 状态 修改时间
────────────────────────────────
项目A 活跃 2小时前
项目B 不活跃 3天前
项目C 活跃 1周前特点:
- 表格格式,列清晰
- 自然语言描述(例如“2小时前”)
- 支持彩色强调(终端支持时)
- 智能排序
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:00ZCSV 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:00ZImplementation:
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()
writer = csv.DictWriter(output, fieldnames=items[0].keys())
writer.writeheader()
writer.writerows(items)
click.echo(output.getvalue())
elif format == 'yaml':
import yaml
click.echo(yaml.dump(items, default_flow_style=False))
else: # table
display_table(items)脚本化使用的JSON格式:
bash
$ tool list --format json
[
{
"name": "项目A",
"status": "active",
"modified": "2024-01-15T14:30:00Z"
},
{
"name": "项目B",
"status": "inactive",
"modified": "2024-01-12T09:15:00Z"
}
]YAML格式(紧凑、可读):
bash
$ tool list --format yaml
- name: 项目A
status: active
modified: 2024-01-15T14:30:00Z
- name: 项目B
status: inactive
modified: 2024-01-12T09:15:00ZCSV格式(适用于电子表格):
bash
$ tool list --format csv
name,status,modified
"项目A",active,2024-01-15T14:30:00Z
"项目B",inactive,2024-01-12T09:15:00Z实现方式:
python
import click
import json
import csv
import io
@click.command()
@click.option('--format', type=click.Choice(['table', 'json', 'yaml', 'csv']),
default='table', help='输出格式')
def list_items(format):
items = get_items()
if format == 'json':
click.echo(json.dumps(items, indent=2))
elif format == 'csv':
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=items[0].keys())
writer.writeheader()
writer.writerows(items)
click.echo(output.getvalue())
elif format == 'yaml':
import yaml
click.echo(yaml.dump(items, default_flow_style=False))
else: # table
display_table(items)Exit Codes
退出码
Standard exit codes:
0 - Success, no errors
1 - General error
2 - Misuse of shell command (invalid arguments)
3-125 - Application-specific errors
126 - Command cannot execute
127 - Command not found
128+130 - Fatal signal "N"
130 - Script terminated by Ctrl+CCommon application codes:
0 - Success
1 - General/unspecified error
2 - Misuse of command syntax
64 - Bad input data
65 - Data format error
66 - No input file
69 - Service unavailable
70 - Internal software error
71 - System error
77 - Permission denied
78 - Configuration errorImplementation:
python
import click
import sys
@click.command()
def main():
try:
result = do_work()
if not result:
sys.exit(1) # Error condition
except ValueError as e:
click.echo(f"Error: {e}", err=True)
sys.exit(2) # Bad input
except PermissionError as e:
click.echo(f"Permission denied: {e}", err=True)
sys.exit(77)
except Exception as e:
click.echo(f"Internal error: {e}", err=True)
sys.exit(70)
sys.exit(0) # SuccessAlways exit explicitly:
bash
undefined标准退出码:
0 - 成功,无错误
1 - 通用错误
2 - shell命令误用(无效参数)
3-125 - 应用程序专属错误
126 - 命令无法执行
127 - 命令未找到
128+130 - 致命信号"N"
130 - 脚本被Ctrl+C终止常见应用程序退出码:
0 - 成功
1 - 通用/未指定错误
2 - 命令语法误用
64 - 输入数据错误
65 - 数据格式错误
66 - 无输入文件
69 - 服务不可用
70 - 内部软件错误
71 - 系统错误
77 - 权限被拒绝
78 - 配置错误实现方式:
python
import click
import sys
@click.command()
def main():
try:
result = do_work()
if not result:
sys.exit(1) # 错误状态
except ValueError as e:
click.echo(f"错误: {e}", err=True)
sys.exit(2) # 输入错误
except PermissionError as e:
click.echo(f"权限被拒绝: {e}", err=True)
sys.exit(77)
except Exception as e:
click.echo(f"内部错误: {e}", err=True)
sys.exit(70)
sys.exit(0) # 成功始终显式退出:
bash
undefinedGood: script can detect success/failure
推荐:脚本可检测成功/失败
tool && echo "Success" || echo "Failed"
tool && echo "成功" || echo "失败"
Bad: unclear exit status
不推荐:退出状态不明确
tool
echo "Done" # Prints regardless of success
undefinedtool
echo "完成" # 无论成功与否都会输出
undefinedError Handling
错误处理
Clear Error Messages
清晰的错误信息
Good error messages:
- Concise (one line if possible)
- Specific about what went wrong
- Suggest how to fix it
bash
undefined优质错误信息:
- 简洁(尽可能一行)
- 明确说明问题所在
- 提供修复建议
bash
undefinedGood
推荐
$ tool add --priority invalid
Error: --priority must be a number (1-10), got 'invalid'
$ tool add --priority invalid
错误: --priority必须是1-10之间的数字,您输入的是'invalid'
Bad
不推荐
$ tool add --priority invalid
Error: Invalid argument
$ tool add --priority invalid
错误: 参数无效
Good
推荐
$ tool delete nonexistent
Error: Item 'nonexistent' not found. Use 'tool list' to see available items.
$ tool delete nonexistent
错误: 条目'nonexistent'不存在。使用'tool list'查看可用条目。
Bad
不推荐
$ tool delete nonexistent
File not found
**Error format:**Error: [what happened]. [How to fix it or where to learn more].
undefined$ tool delete nonexistent
文件未找到
**错误格式:**错误: [发生的问题]。[修复方法或更多信息来源]。
undefinedSuggestions for Fixes
修复建议
Provide actionable suggestions:
python
import click
@click.command()
@click.argument('command')
def main(command):
valid_commands = ['add', 'list', 'delete']
if command not in valid_commands:
# Suggest closest match
from difflib import get_close_matches
suggestions = get_close_matches(command, valid_commands, n=1)
msg = f"Unknown command '{command}'."
if suggestions:
msg += f" Did you mean '{suggestions[0]}'?"
else:
msg += f" Available commands: {', '.join(valid_commands)}"
click.echo(f"Error: {msg}", err=True)
raise SystemExit(1)提供可执行的修复建议:
python
import click
@click.command()
@click.argument('command')
def main(command):
valid_commands = ['add', 'list', 'delete']
if command not in valid_commands:
# 推荐最接近的匹配项
from difflib import get_close_matches
suggestions = get_close_matches(command, valid_commands, n=1)
msg = f"未知命令'{command}'。"
if suggestions:
msg += f" 您是不是想输入'{suggestions[0]}'?"
else:
msg += f" 可用命令: {', '.join(valid_commands)}"
click.echo(f"错误: {msg}", err=True)
raise SystemExit(1)Debug Mode
调试模式
Provide --debug flag for detailed output:
python
import click
import sys
import traceback
@click.command()
@click.option('--debug', is_flag=True, help='Show detailed error information')
def main(debug):
try:
do_work()
except Exception as e:
if debug:
# Show full traceback
traceback.print_exc(file=sys.stderr)
else:
# Show user-friendly message
click.echo(f"Error: {str(e)}", err=True)
click.echo("Use --debug to see details", err=True)
sys.exit(1)Example output:
bash
$ tool --debug
Error: Connection failed
Traceback (most recent call last):
File "tool.py", line 42, in main
connect_to_server()
File "tool.py", line 15, in connect_to_server
raise ConnectionError("Timeout after 30s")
ConnectionError: Timeout after 30s提供--debug标志以显示详细输出:
python
import click
import sys
import traceback
@click.command()
@click.option('--debug', is_flag=True, help='显示详细错误信息')
def main(debug):
try:
do_work()
except Exception as e:
if debug:
# 显示完整堆栈跟踪
traceback.print_exc(file=sys.stderr)
else:
# 显示用户友好的信息
click.echo(f"错误: {str(e)}", err=True)
click.echo("使用--debug查看详细信息", err=True)
sys.exit(1)示例输出:
bash
$ tool --debug
错误: 连接失败
Traceback (most recent call last):
File "tool.py", line 42, in main
connect_to_server()
File "tool.py", line 15, in connect_to_server
raise ConnectionError("30秒后超时")
ConnectionError: 30秒后超时Cross-Platform Considerations
跨平台注意事项
Path Separators
路径分隔符
Always use proper path handling:
python
undefined始终使用正确的路径处理方式:
python
undefinedGood: use pathlib (cross-platform)
推荐:使用pathlib(跨平台)
from pathlib import Path
config_path = Path.home() / '.config' / 'app' / 'config.yaml'
output_path = Path('output') / 'result.txt'
from pathlib import Path
config_path = Path.home() / '.config' / 'app' / 'config.yaml'
output_path = Path('output') / 'result.txt'
Also good: use os.path
也推荐:使用os.path
import os
config_path = os.path.join(os.path.expanduser('~'), '.config', 'app', 'config.yaml')
import os
config_path = os.path.join(os.path.expanduser('~'), '.config', 'app', 'config.yaml')
Avoid: hardcoded separators
避免:硬编码分隔符
config_path = '~/.config/app/config.yaml' # Wrong on Windows
**Node.js example:**
```javascript
const path = require('path');
const os = require('os');
const configPath = path.join(os.homedir(), '.config', 'app', 'config.yaml');
const outputPath = path.join('output', 'result.txt');config_path = '~/.config/app/config.yaml' # Windows系统下无效
**Node.js示例:**
```javascript
const path = require('path');
const os = require('os');
const configPath = path.join(os.homedir(), '.config', 'app', 'config.yaml');
const outputPath = path.join('output', 'result.txt');Line Endings
行尾
Normalize line endings:
python
undefined规范化行尾:
python
undefinedPython: open files with universal newline support (default)
Python: 使用通用换行支持打开文件(默认设置)
with open('file.txt', 'r') as f: # Automatically handles \r\n, \n, \r
content = f.read()
with open('file.txt', 'r') as f: # 自动处理\r\n、\n、\r
content = f.read()
When writing, use default newline handling
写入时使用默认换行处理
with open('file.txt', 'w') as f:
f.write(content) # Uses platform default newline
**Git configuration:**
```bashwith open('file.txt', 'w') as f:
f.write(content) # 使用平台默认的换行符
**Git配置:**
```bashSet in .gitattributes to normalize line endings
在.gitattributes中设置以规范化行尾
- text=auto *.py text eol=lf *.json text eol=lf
undefined- text=auto *.py text eol=lf *.json text eol=lf
undefinedTerminal Capabilities
终端能力
Detect terminal capabilities:
python
import sys
import os
def get_terminal_width():
"""Get terminal width, default to 80."""
try:
return os.get_terminal_size().columns
except (AttributeError, ValueError):
return 80
def supports_unicode():
"""Check if terminal supports Unicode."""
encoding = sys.stdout.encoding or 'utf-8'
return encoding.lower() in ('utf-8', 'utf8')
def supports_color():
"""Check if terminal supports colors."""
# Already covered above
return True检测终端能力:
python
import sys
import os
def get_terminal_width():
"""获取终端宽度,默认80列。"""
try:
return os.get_terminal_size().columns
except (AttributeError, ValueError):
return 80
def supports_unicode():
"""检查终端是否支持Unicode。"""
encoding = sys.stdout.encoding or 'utf-8'
return encoding.lower() in ('utf-8', 'utf8')
def supports_color():
"""检查终端是否支持彩色。"""
# 前面已覆盖此实现
return TrueUse capabilities to adjust output
根据能力调整输出
if supports_unicode():
symbol = '✓' # Check mark
else:
symbol = '✔' # Alternative
**Avoid assumptions:**
```pythonif supports_unicode():
symbol = '✓' # 勾选标记
else:
symbol = '✔' # 替代标记
**避免假设:**
```pythonBad: assumes capabilities
不推荐:假设终端能力
output = "✓ Success\n"
output = "✓ 成功\n"
Good: checks capabilities
推荐:检查终端能力
symbol = '✓' if supports_unicode() else '✔'
output = f"{symbol} Success\n"
undefinedsymbol = '✓' if supports_unicode() else '✔'
output = f"{symbol} 成功\n"
undefinedTesting CLI Applications
CLI应用程序测试
Integration Tests
集成测试
Test the complete CLI, not just functions:
python
import subprocess
import json
def test_list_command():
"""Test list command output."""
result = subprocess.run(
['tool', 'list', '--format', 'json'],
capture_output=True,
text=True
)
assert result.returncode == 0
output = json.loads(result.stdout)
assert isinstance(output, list)
def test_list_command_human_readable():
"""Test list command with default format."""
result = subprocess.run(
['tool', 'list'],
capture_output=True,
text=True
)
assert result.returncode == 0
assert 'Name' in result.stdout # Table headerTest with Click (Python):
python
from click.testing import CliRunner
from tool.cli import main
def test_list_command():
runner = CliRunner()
result = runner.invoke(main, ['list', '--format', 'json'])
assert result.exit_code == 0
output = json.loads(result.output)
assert isinstance(output, list)测试完整的CLI,而非仅测试函数:
python
import subprocess
import json
def test_list_command():
"""测试list命令的输出。"""
result = subprocess.run(
['tool', 'list', '--format', 'json'],
capture_output=True,
text=True
)
assert result.returncode == 0
output = json.loads(result.stdout)
assert isinstance(output, list)
def test_list_command_human_readable():
"""测试list命令的默认格式输出。"""
result = subprocess.run(
['tool', 'list'],
capture_output=True,
text=True
)
assert result.returncode == 0
assert '名称' in result.stdout # 表格表头使用Click测试(Python):
python
from click.testing import CliRunner
from tool.cli import main
def test_list_command():
runner = CliRunner()
result = runner.invoke(main, ['list', '--format', 'json'])
assert result.exit_code == 0
output = json.loads(result.output)
assert isinstance(output, list)Output Verification
输出验证
Verify output format and content:
python
import subprocess
def test_add_command_success():
"""Test successful add operation."""
result = subprocess.run(
['tool', 'add', 'Test Item', '--priority', '5'],
capture_output=True,
text=True
)
assert result.returncode == 0
assert 'Added' in result.stdout or 'Success' in result.stdout
def test_add_command_invalid_priority():
"""Test validation of priority argument."""
result = subprocess.run(
['tool', 'add', 'Test Item', '--priority', 'invalid'],
capture_output=True,
text=True
)
assert result.returncode != 0
assert 'Error' in result.stderr
assert 'priority' in result.stderr.lower()验证输出格式和内容:
python
import subprocess
def test_add_command_success():
"""测试add操作成功的场景。"""
result = subprocess.run(
['tool', 'add', '测试条目', '--priority', '5'],
capture_output=True,
text=True
)
assert result.returncode == 0
assert '已添加' in result.stdout or '成功' in result.stdout
def test_add_command_invalid_priority():
"""测试priority参数的验证逻辑。"""
result = subprocess.run(
['tool', 'add', '测试条目', '--priority', 'invalid'],
capture_output=True,
text=True
)
assert result.returncode != 0
assert '错误' in result.stderr
assert 'priority' in result.stderr.lower()Exit Code Checks
退出码检查
Verify proper exit codes:
python
import subprocess
def test_help_flag_exit_code():
"""Test --help returns success."""
result = subprocess.run(['tool', '--help'], capture_output=True)
assert result.returncode == 0
def test_invalid_command_exit_code():
"""Test invalid command returns error code."""
result = subprocess.run(['tool', 'invalid'], capture_output=True)
assert result.returncode != 0
def test_missing_required_arg():
"""Test missing required argument returns error."""
result = subprocess.run(['tool', 'delete'], capture_output=True)
assert result.returncode == 2 # Misuse of command syntax验证正确的退出码:
python
import subprocess
def test_help_flag_exit_code():
"""测试--help返回成功状态。"""
result = subprocess.run(['tool', '--help'], capture_output=True)
assert result.returncode == 0
def test_invalid_command_exit_code():
"""测试无效命令返回错误码。"""
result = subprocess.run(['tool', 'invalid'], capture_output=True)
assert result.returncode != 0
def test_missing_required_arg():
"""测试缺少必填参数返回错误。"""
result = subprocess.run(['tool', 'delete'], capture_output=True)
assert result.returncode == 2 # 命令语法误用Documentation
文档
README
README
CLI tools need clear README documentation:
markdown
undefinedCLI工具需要清晰的README文档:
markdown
undefinedTool Name
工具名称
Brief description of what the tool does.
工具的简要功能描述。
Installation
安装
Installation instructions (package manager, build from source, etc.)
安装说明(包管理器、从源码构建等)。
Usage
使用方法
Basic Usage
基础用法
bash
tool [COMMAND] [OPTIONS] [ARGUMENTS]bash
tool [COMMAND] [OPTIONS] [ARGUMENTS]Examples
示例
bash
undefinedbash
undefinedList all items
列出所有条目
tool list
tool list
Add new item
添加新条目
tool add "My Item" --priority 5
tool add "我的条目" --priority 5
Delete with confirmation
带确认的删除
tool delete "My Item"
tool delete "我的条目"
Suppress confirmation
跳过确认直接删除
tool delete "My Item" --force
undefinedtool delete "我的条目" --force
undefinedCommands
命令
- - Add a new item
add [NAME] - - List all items
list - - Delete an item
delete [NAME] - - Manage configuration
config
- - 添加新条目
add [NAME] - - 列出所有条目
list - - 删除条目
delete [NAME] - - 管理配置
config
Options
选项
- - Increase output verbosity
-v, --verbose - - Output format
--format [json|yaml|csv] - - Show debug information
--debug - - Show help message
-h, --help - - Show version
--version
- - 增加输出详细程度
-v, --verbose - - 输出格式
--format [json|yaml|csv] - - 显示调试信息
--debug - - 显示帮助信息
-h, --help - - 显示版本
--version
Configuration
配置
Settings can be configured via:
- Command-line flags (highest priority)
- Environment variables
- Config file at
~/.config/tool/config.yaml - Built-in defaults
可通过以下方式配置设置:
- 命令行标志(最高优先级)
- 环境变量
- 配置文件
~/.config/tool/config.yaml - 内置默认值
Environment Variables
环境变量
- - Enable debug mode
DEBUG - - Path to config file
TOOL_CONFIG - - Disable colored output
NO_COLOR
- - 启用调试模式
DEBUG - - 配置文件路径
TOOL_CONFIG - - 禁用彩色输出
NO_COLOR
Configuration File
配置文件
Create :
~/.config/tool/config.yamlyaml
debug: false
format: table
color: true创建:
~/.config/tool/config.yamlyaml
debug: false
format: table
color: trueTroubleshooting
故障排除
Common Issues
常见问题
Issue: "command not found"
- Ensure tool is installed and in PATH
- Check:
which tool
Issue: Permission denied
- Make script executable:
chmod +x tool
问题:"command not found"
- 确保工具已安装且在PATH中
- 检查:
which tool
问题:Permission denied
- 使脚本可执行:
chmod +x tool
Development
开发
Build and test instructions.
undefined构建和测试说明。
undefinedMan Pages
手册页
Create man pages for Unix systems:
TOOL(1) User Commands TOOL(1)
NAME
tool - A tool that does one thing well
SYNOPSIS
tool [OPTIONS] COMMAND [ARGS]...
DESCRIPTION
Detailed description of what the tool does.
COMMANDS
add NAME Add a new item
list List all items
delete NAME Delete an item
OPTIONS
-v, --verbose Increase output verbosity
--format FMT Output format (json, yaml, csv)
-h, --help Show help message
--version Show version
EXAMPLES
Add a new item:
tool add "My Item"
List items as JSON:
tool list --format json
EXIT STATUS
0 Success
1 General error
2 Invalid arguments
FILES
~/.config/tool/config.yaml
Configuration file
SEE ALSO
tool-add(1), tool-delete(1)为Unix系统创建手册页:
TOOL(1) 用户命令 TOOL(1)
名称
tool - 专注做好一件事的工具
概要
tool [OPTIONS] COMMAND [ARGS]...
描述
工具的详细功能描述。
命令
add NAME 添加新条目
list 列出所有条目
delete NAME 删除条目
选项
-v, --verbose 增加输出详细程度
--format FMT 输出格式(json、yaml、csv)
-h, --help 显示帮助信息
--version 显示版本
示例
添加新条目:
tool add "我的条目"
以JSON格式列出条目:
tool list --format json
退出状态
0 成功
1 通用错误
2 参数无效
文件
~/.config/tool/config.yaml
配置文件
另请参阅
tool-add(1), tool-delete(1)Built-in Help
内置帮助
Make help accessible and comprehensive:
python
@click.group()
def main():
"""Main CLI tool."""
pass
@main.command()
def help():
"""Show detailed help information."""
click.echo(click.get_current_context().get_help())使帮助信息易于访问且全面:
python
@click.group()
def main():
"""主CLI工具。"""
pass
@main.command()
def help():
"""显示详细帮助信息。"""
click.echo(click.get_current_context().get_help())Common CLI Libraries
常见CLI库
Python
Python
Click - Most popular, decorator-based
python
import click
@click.command()
@click.option('--name', prompt='Your name', help='Name of person')
@click.option('--count', default=1, help='Number of greetings')
def hello(name, count):
"""Simple program that greets NAME COUNT times."""
for _ in range(count):
click.echo(f'Hello {name}!')
if __name__ == '__main__':
hello()Typer - Built on Click, modern with async support
python
import typer
app = typer.Typer()
@app.command()
def add(name: str, priority: int = typer.Option(5, min=1, max=10)):
"""Add a new item."""
print(f"Added {name} with priority {priority}")
if __name__ == "__main__":
app()Argparse - Built-in to Python, more verbose
python
import argparse
parser = argparse.ArgumentParser(description='Process some integers')
parser.add_argument('--name', required=True, help='Name')
parser.add_argument('--count', type=int, default=1, help='Count')
args = parser.parse_args()Click - 最受欢迎的装饰器式库
python
import click
@click.command()
@click.option('--name', prompt='您的姓名', help='用户姓名')
@click.option('--count', default=1, help='问候次数')
def hello(name, count):
"""简单的问候程序,向NAME问候COUNT次。"""
for _ in range(count):
click.echo(f'你好 {name}!')
if __name__ == '__main__':
hello()Typer - 基于Click构建,支持异步的现代库
python
import typer
app = typer.Typer()
@app.command()
def add(name: str, priority: int = typer.Option(5, min=1, max=10)):
"""添加新条目。"""
print(f"已添加 {name},优先级为 {priority}")
if __name__ == "__main__":
app()Argparse - Python内置库,语法较冗长
python
import argparse
parser = argparse.ArgumentParser(description='处理一些整数')
parser.add_argument('--name', required=True, help='姓名')
parser.add_argument('--count', type=int, default=1, help='次数')
args = parser.parse_args()Node.js / JavaScript
Node.js / JavaScript
Commander - Minimal and clean
javascript
const { Command } = require('commander');
const program = new Command();
program
.command('add <name>')
.option('--priority <number>', 'Item priority', '5')
.action((name, options) => {
console.log(`Added ${name} with priority ${options.priority}`);
});
program.parse(process.argv);Yargs - Feature-rich
javascript
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
yargs(hideBin(process.argv))
.command('add <name>', 'Add item', (yargs) => {
return yargs.option('priority', { type: 'number', default: 5 });
}, (argv) => {
console.log(`Added ${argv.name} with priority ${argv.priority}`);
})
.argv;Oclif - Full-featured framework for complex CLIs
javascript
const {Command, flags} = require('@oclif/command');
class AddCommand extends Command {
static description = 'Add a new item';
static args = [{ name: 'name' }];
static flags = {
priority: flags.integer({ default: 5 })
};
async run() {
const { args, flags } = this.parse(AddCommand);
this.log(`Added ${args.name} with priority ${flags.priority}`);
}
}
module.exports = AddCommand;Commander - 轻量简洁
javascript
const { Command } = require('commander');
const program = new Command();
program
.command('add <name>')
.option('--priority <number>', '条目优先级', '5')
.action((name, options) => {
console.log(`已添加 ${name},优先级为 ${options.priority}`);
});
program.parse(process.argv);Yargs - 功能丰富
javascript
const yargs = require('yargs/yargs');
const { hideBin } = require('yargs/helpers');
yargs(hideBin(process.argv))
.command('add <name>', '添加条目', (yargs) => {
return yargs.option('priority', { type: 'number', default: 5 });
}, (argv) => {
console.log(`已添加 ${argv.name},优先级为 ${argv.priority}`);
})
.argv;Oclif - 适用于复杂CLI的全功能框架
javascript
const {Command, flags} = require('@oclif/command');
class AddCommand extends Command {
static description = '添加新条目';
static args = [{ name: 'name' }];
static flags = {
priority: flags.integer({ default: 5 })
};
async run() {
const { args, flags } = this.parse(AddCommand);
this.log(`已添加 ${args.name},优先级为 ${flags.priority}`);
}
}
module.exports = AddCommand;Go
Go
Cobra - Popular Go CLI framework
go
var rootCmd = &cobra.Command{
Use: "tool",
Short: "A brief description",
Long: "A longer description...",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello World!")
},
}
var addCmd = &cobra.Command{
Use: "add [name]",
Short: "Add a new item",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Added %s\n", args[0])
},
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().IntP("priority", "p", 5, "Priority level")
}Cobra - 流行的Go CLI框架
go
var rootCmd = &cobra.Command{
Use: "tool",
Short: "简要描述",
Long: "更详细的描述...",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("你好,世界!")
},
}
var addCmd = &cobra.Command{
Use: "add [name]",
Short: "添加新条目",
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("已添加 %s\n", args[0])
},
}
func init() {
rootCmd.AddCommand(addCmd)
addCmd.Flags().IntP("priority", "p", 5, "优先级")
}Rust
Rust
Clap - Powerful and flexible
rust
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[clap(name = "tool")]
#[clap(about = "A tool that does one thing well")]
struct Cli {
#[clap(subcommand)]
command: Option<Commands>,
#[clap(short, long)]
debug: bool,
}
#[derive(Subcommand)]
enum Commands {
Add {
name: String,
#[clap(short, long, default_value = "5")]
priority: u8,
},
List,
}
fn main() {
let cli = Cli::parse();
// Handle commands...
}Clap - 强大灵活的库
rust
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[clap(name = "tool")]
#[clap(about = "专注做好一件事的工具")]
struct Cli {
#[clap(subcommand)]
command: Option<Commands>,
#[clap(short, long)]
debug: bool,
}
#[derive(Subcommand)]
enum Commands {
Add {
name: String,
#[clap(short, long, default_value = "5")]
priority: u8,
},
List,
}
fn main() {
let cli = Cli::parse();
// 处理命令...
}Common Patterns and Examples
常见模式与示例
Pattern: Global Options vs Subcommand Options
模式:全局选项与子命令选项
undefinedundefinedGlobal options apply to tool
全局选项应用于整个工具
tool --verbose add item
tool --verbose add item
Subcommand options apply to subcommand
子命令选项仅应用于子命令
tool add item --priority 5
tool add item --priority 5
Mix of both
混合使用两者
tool --verbose add item --priority 5
undefinedtool --verbose add item --priority 5
undefinedPattern: Piping and Composition
模式:管道与组合
bash
undefinedbash
undefinedPipe output to other tools
将输出管道到其他工具
tool list --format json | jq '.[] | select(.status == "active")'
tool list --format json | jq '.[] | select(.status == "active")'
Use as input to other commands
作为其他命令的输入
tool export > backup.json
tool import < backup.json
tool export > backup.json
tool import < backup.json
Chain commands
链式命令
tool list | grep important | while read item; do tool process "$item"; done
undefinedtool list | grep important | while read item; do tool process "$item"; done
undefinedPattern: Interactive vs Non-interactive
模式:交互式与非交互式
python
import sys
import click
@click.command()
@click.option('--force', is_flag=True, help='Skip confirmations')
def delete_item(force):
if sys.stdin.isatty() and not force:
# Interactive - prompt for confirmation
if not click.confirm('Delete this item?'):
click.echo('Cancelled')
return
# Non-interactive or confirmed
perform_deletion()python
import sys
import click
@click.command()
@click.option('--force', is_flag=True, help='跳过确认步骤')
def delete_item(force):
if sys.stdin.isatty() and not force:
# 交互式 - 提示确认
if not click.confirm('确定要删除此条目吗?'):
click.echo('已取消')
return
# 非交互式或已确认
perform_deletion()Pattern: Timeout Handling
模式:超时处理
python
import signal
import sys
def timeout_handler(signum, frame):
print("Operation timed out")
sys.exit(124)
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(30) # 30 second timeout
try:
result = long_running_operation()
finally:
signal.alarm(0) # Cancel alarmNote: For project-specific CLI patterns, check in the project directory.
.claude/CLAUDE.mdpython
import signal
import sys
def timeout_handler(signum, frame):
print("操作超时")
sys.exit(124)
signal.signal(signal.SIGALRM, timeout_handler)
signal.alarm(30) # 30秒超时
try:
result = long_running_operation()
finally:
signal.alarm(0) # 取消超时注意: 项目专属的CLI模式,请查看项目目录中的。
.claude/CLAUDE.md