cli-patterns by 0xdarkmatter/claude-mods
npx skills add https://github.com/0xdarkmatter/claude-mods --skill cli-patterns适用于构建 AI 助手和高级用户可以链式调用、解析输出并依赖的 CLI 工具的模式。
为智能体工作流构建 CLI —— 适用于链式执行命令、以编程方式解析输出并期望可预测行为的 AI 助手和高级用户。
| 原则 | 含义 | 重要性 |
|---|---|---|
| 自文档化 | --help 内容全面且始终保持最新 | LLM 无需外部文档即可发现功能 |
| 可预测 | 所有命令采用相同模式 | 一次学习,随处使用 |
| 可组合 | Unix 哲学 - 做好一件事 | 工具自然链式组合 |
| 可解析 | --json 始终可用,始终有效 | 无需解析技巧即可供机器消费 |
| 默认静默 | 仅数据,除非请求否则无装饰 | 脚本不会因意外输出而中断 |
Patterns for building CLI tools that AI assistants and power users can chain, parse, and rely on.
Build CLIs for agentic workflows - AI assistants and power users who chain commands, parse output programmatically, and expect predictable behavior.
| Principle | Meaning | Why It Matters |
|---|---|---|
| Self-documenting | --help is comprehensive and always current | LLMs discover capabilities without external docs |
| Predictable | Same patterns across all commands | Learn once, use everywhere |
| Composable | Unix philosophy - do one thing well | Tools chain together naturally |
| Parseable | --json always available, always valid |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 快速失败 | 无效输入 = 立即报错 | 无静默失败或部分结果 |
<工具名> [全局选项] <资源> <操作> [选项] [参数]
每个 CLI 都遵循此层次结构:
<工具名>
├── --version, --help # 全局标志
├── auth # 身份验证(如果需要)
│ ├── login
│ ├── status
│ └── logout
└── <资源> # 领域资源(复数名词)
├── list # 获取多个
├── get <id> # 按 ID 获取单个
├── create # 创建新项(如果支持)
├── update <id> # 修改现有项(如果支持)
├── delete <id> # 删除(如果支持)
└── <自定义操作> # 领域特定动词
| 元素 | 约定 | 有效示例 | 无效示例 |
|---|---|---|---|
| 工具名 | 小写,2-12 个字符 | mytool, datactl | MyTool, my-tool-cli |
| 资源 | 复数名词,小写 | invoices, users | Invoice, user |
| 操作 | 动词,小写 | list, get, sync | listing, getter |
| 长标志 | 短横线命名法 | --dry-run, --output-format | --dryRun, --output_format |
| 短标志 | 单个字母 | -n, -q, -v | -num, -quiet |
| 操作 | HTTP 等价 | 返回 | 幂等性 |
|---|---|---|---|
list | GET /resources | 数组 | 是 |
get <id> | GET /resources/:id | 对象 | 是 |
create | POST /resources | 创建的对象 | 否 |
update <id> | PATCH /resources/:id | 更新的对象 | 是 |
delete <id> | DELETE /resources/:id | 确认信息 | 是 |
search | GET /resources?q= | 数组 | 是 |
每个命令必须支持:
| 标志 | 短标志 | 行为 | 输出 |
|---|---|---|---|
--help | -h | 显示带示例的帮助信息 | 帮助文本到 stdout,退出码 0 |
--json | 机器可读输出 | JSON 到 stdout |
根命令必须额外支持:
| 标志 | 短标志 | 行为 | 输出 |
|---|---|---|---|
--version | -V | 显示版本 | <工具名> <版本> 到 stdout,退出码 0 |
| 标志 | 短标志 | 类型 | 目的 | 默认值 |
|---|---|---|---|---|
--quiet | -q | 布尔值 | 抑制非必要的 stderr 输出 | false |
--verbose | -v | 布尔值 | 增加详细级别 | false |
--dry-run | 布尔值 | 预览而不执行 | false | |
--limit | -n | 整数 | 返回的最大结果数 | 20 |
--output | -o | 路径 | 将输出写入文件 | stdout |
--format | -f | 枚举 | 输出格式 | 可变 |
--json 而不是 --json=true-vq 等于 -v -q这是最重要的规则:
| 流 | 内容 | 时机 |
|---|---|---|
| stdout | 仅数据 | 始终 |
| stderr | 其他所有内容 | 交互模式 |
stdout 接收:
--json 时的 JSONstderr 接收:
--verbose)import sys
def is_interactive() -> bool:
"""如果连接到终端而非管道,则返回 True。"""
return sys.stdout.isatty() and sys.stderr.isatty()
| 上下文 | stdout.isatty() | 行为 |
|---|---|---|
| 终端 | True | 丰富输出到 stderr,摘要到 stdout |
| 管道(` | jq`) | False |
重定向(> file) | False | 最小化到 stdout |
--json 标志 | 任意 | JSON 到 stdout,抑制 stderr 噪音 |
完整 JSON 响应模式请参阅 references/json-schemas.md。
关键约定:
{"data": [...], "meta": {...}}{"data": {...}}{"error": {"code": "...", "message": "..."}}脚本可以依赖的语义化退出码:
| 代码 | 名称 | 含义 | 时机 |
|---|---|---|---|
| 0 | 成功 | 操作完成 | 一切正常 |
| 1 | 错误 | 通用/未知错误 | 意外失败 |
| 2 | 需要认证 | 未认证 | 无令牌、令牌过期 |
| 3 | 未找到 | 资源缺失 | ID 不存在 |
| 4 | 验证错误 | 无效输入 | 参数错误、验证失败 |
| 5 | 禁止访问 | 权限不足 | 已认证但未授权 |
| 6 | 速率限制 | 请求过多 | API 限流 |
| 7 | 冲突 | 状态冲突 | 并发修改、重复 |
# 脚本可以根据退出码分支处理
mytool items get item-001 --json
case $? in
0) echo "成功" ;;
2) echo "需要认证" && mytool auth login ;;
3) echo "项目未找到" ;;
*) echo "发生错误" ;;
esac
# 常量
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_AUTH_REQUIRED = 2
EXIT_NOT_FOUND = 3
EXIT_VALIDATION = 4
EXIT_FORBIDDEN = 5
EXIT_RATE_LIMITED = 6
EXIT_CONFLICT = 7
# 用法
raise typer.Exit(EXIT_NOT_FOUND)
使用 --json 时,错误将结构化 JSON 输出到 stdout 并将消息输出到 stderr:
stderr:
Error: Item not found
stdout:
{
"error": {
"code": "NOT_FOUND",
"message": "Item not found",
"details": {
"item_id": "bad-id"
}
}
}
| 代码 | 退出码 | 含义 |
|---|---|---|
AUTH_REQUIRED | 2 | 必须先认证 |
TOKEN_EXPIRED | 2 | 令牌需要刷新 |
FORBIDDEN | 5 | 权限不足 |
NOT_FOUND | 3 | 资源不存在 |
VALIDATION_ERROR | 4 | 无效输入 |
INVALID_ARGUMENT | 4 | 参数值错误 |
MISSING_ARGUMENT | 4 | 缺少必需参数 |
RATE_LIMITED | 6 | 请求过多 |
CONFLICT | 7 | 状态冲突 |
ALREADY_EXISTS | 7 | 重复资源 |
INTERNAL_ERROR | 1 | 意外错误 |
API_ERROR | 1 | 上游 API 失败 |
NETWORK_ERROR | 1 | 连接失败 |
def _error(
message: str,
code: str = "ERROR",
exit_code: int = EXIT_ERROR,
details: dict = None,
as_json: bool = False,
):
"""输出错误并退出。"""
error_obj = {"error": {"code": code, "message": message}}
if details:
error_obj["error"]["details"] = details
if as_json:
print(json.dumps(error_obj, indent=2))
# 始终将人类可读消息打印到 stderr
console.print(f"[red]Error:[/red] {message}")
raise typer.Exit(exit_code)
每个 --help 输出必须包含:
<一行描述>
用法: <工具名> <资源> <操作> [选项] [参数]
参数:
<参数> 位置参数的描述
选项:
-s, --status TEXT 按状态筛选
-n, --limit INTEGER 最大结果数 [默认: 20]
--json 输出为 JSON
-h, --help 显示此帮助
示例:
<工具名> <资源> <操作>
<工具名> <资源> <操作> --status active
<工具名> <资源> <操作> --json | jq '.[0]'
示例应展示:
jq 链式使用需要身份验证的工具必须实现:
<工具名> auth login # 交互式认证
<工具名> auth status # 检查当前状态
<工具名> auth logout # 清除凭据
推荐: 操作系统密钥环配合后备方案以获得最大安全性
环境变量(CI/CD、测试)
MYTOOL_API_TOKEN 或类似操作系统密钥环(主要存储 - 安全)
.env 文件(开发后备方案)
.gitignore 中依赖项:
dependencies = [
"keyring>=24.0.0", # 操作系统密钥环访问
"python-dotenv>=1.0.0", # .env 文件支持
]
简单替代方案: 仅使用 ~/.config/<工具名>/ 中的配置文件
完整凭据存储实现请参阅 references/implementation.md。
当需要认证但缺失时:
$ mytool items list
Error: Not authenticated. Run: mytool auth login
# 退出码: 2
$ mytool items list --json
# stderr: Error: Not authenticated. Run: mytool auth login
{"error": {"code": "AUTH_REQUIRED", "message": "Not authenticated. Run: mytool auth login"}}
# 退出码: 2
输入(灵活): 为方便用户接受多种格式
| 格式 | 示例 | 解释 |
|---|---|---|
| ISO 日期 | 2025-01-15 | 精确日期 |
| ISO 日期时间 | 2025-01-15T10:30:00Z | 精确日期时间 |
| 相对日期 | today, yesterday, tomorrow | 当前/前一天/后一天 |
| 相对日期 | last, this(带上下文) | 前一个/当前周期 |
输出(严格): 始终输出 ISO 8601
{
"created_at": "2025-01-15T10:30:00Z",
"due_date": "2025-02-15",
"month": "2025-01"
}
存储为十进制数,而非分
有歧义时包含货币
永不格式化(JSON 中无 "$" 或 ",")
{ "total": 1250.50, "currency": "USD" }
始终为字符串(即使是数字)
保留来源的精确格式
{ "id": "abc_123", "legacy_id": "12345" }
JSON 中使用 UPPER_SNAKE_CASE
输入不区分大小写
--status DRAFT --status draft --status Draft
{"status": "IN_PROGRESS"}
# 按状态
--status DRAFT
--status active,pending # 多个值
# 按日期范围
--from 2025-01-01 --to 2025-01-31
--month 2025-01
--month last
# 按相关实体
--user "Alice"
--project "Project X"
# 文本搜索
--search "keyword"
-q "keyword"
# 布尔筛选器
--archived
--no-archived
--include-deleted
# 限制结果
--limit 50
-n 50
# 基于偏移量
--page 2
--offset 20
# 基于游标
--cursor "eyJpZCI6MTIzfQ=="
--after "item_123"
完整 Python 实现模板请参阅 references/implementation.md,包括:
# 错误:进度输出到 stdout
$ bad-tool items list --json
Fetching items...
[{"id": "1"}]
Done!
# 正确:仅 JSON 到 stdout
$ good-tool items list --json
[{"id": "1"}]
# 错误:非交互式上下文中的提示
$ bad-tool items create
Enter name: _
# 正确:使用必需标志快速失败
$ good-tool items create
Error: --name is required
# 错误:相同概念使用不同标志
$ tool1 list -j
$ tool2 list --format=json
# 正确:各处使用相同标志
$ tool1 list --json
$ tool2 list --json
# 错误:失败时返回成功退出码
$ bad-tool items delete bad-id
Item not found
$ echo $?
0
# 正确:语义化退出码
$ good-tool items delete bad-id
Error: Item not found: bad-id
$ echo $?
3
<工具名> --version<工具名> --help 带示例<工具名> <资源> list [--json]<工具名> <资源> get <id> [--json]--json 时输出有效 JSONauth login, auth status, auth logout)--quiet 和 --verbose 模式--dry-run--limit, --page)Typer(新工具首选):
Click(现有工具可接受):
Typer 基于 Click 构建(100% 兼容)
结构良好的 Click 代码无需迁移
两者都必须遵循相同的输出约定
import typer from rich.console import Console
app = typer.Typer() console = Console(stderr=True) # UI 到 stderr
import click from rich.console import Console
console = Console(stderr=True) # 相同模式
每周安装量
5
代码仓库
GitHub 星标数
8
首次出现
2026年2月5日
安全审计
安装于
opencode5
gemini-cli5
claude-code5
replit4
codex4
cursor4
| Machine consumption without parsing hacks |
| Quiet by default | Data only, no decoration unless requested | Scripts don't break on unexpected output |
| Fail fast | Invalid input = immediate error | No silent failures or partial results |
<tool> [global-options] <resource> <action> [options] [arguments]
Every CLI follows this hierarchy:
<tool>
├── --version, --help # Global flags
├── auth # Authentication (if required)
│ ├── login
│ ├── status
│ └── logout
└── <resource> # Domain resources (plural nouns)
├── list # Get many
├── get <id> # Get one by ID
├── create # Make new (if supported)
├── update <id> # Modify existing (if supported)
├── delete <id> # Remove (if supported)
└── <custom-action> # Domain-specific verbs
| Element | Convention | Valid Examples | Invalid Examples |
|---|---|---|---|
| Tool name | lowercase, 2-12 chars | mytool, datactl | MyTool, my-tool-cli |
| Resource | plural noun, lowercase | invoices, users | Invoice, user |
| Action | verb, lowercase | list, get, sync | listing, getter |
| Long flags | kebab-case | --dry-run, --output-format | --dryRun, --output_format |
| Short flags | single letter | -n, -q, -v | -num, -quiet |
| Action | HTTP Equiv | Returns | Idempotent |
|---|---|---|---|
list | GET /resources | Array | Yes |
get <id> | GET /resources/:id | Object | Yes |
create | POST /resources | Created object | No |
update <id> | PATCH /resources/:id | Updated object | Yes |
delete <id> | DELETE /resources/:id | Confirmation | Yes |
search | GET /resources?q= | Array | Yes |
Every command MUST support:
| Flag | Short | Behavior | Output |
|---|---|---|---|
--help | -h | Show help with examples | Help text to stdout, exit 0 |
--json | Machine-readable output | JSON to stdout |
Root command MUST additionally support:
| Flag | Short | Behavior | Output |
|---|---|---|---|
--version | -V | Show version | <tool> <version> to stdout, exit 0 |
| Flag | Short | Type | Purpose | Default |
|---|---|---|---|---|
--quiet | -q | bool | Suppress non-essential stderr | false |
--verbose | -v | bool | Increase detail level | false |
--dry-run | bool | Preview without executing | false | |
--limit | -n | int | Max results to return | 20 |
--output | -o | path | Write output to file | stdout |
--format | -f | enum | Output format | varies |
--json not --json=true-vq equals -v -qThis is the most critical rule:
| Stream | Content | When |
|---|---|---|
| stdout | Data only | Always |
| stderr | Everything else | Interactive mode |
stdout receives:
--json is setstderr receives:
--verbose)import sys
def is_interactive() -> bool:
"""True if connected to a terminal, not piped."""
return sys.stdout.isatty() and sys.stderr.isatty()
| Context | stdout.isatty() | Behavior |
|---|---|---|
| Terminal | True | Rich output to stderr, summary to stdout |
| Piped (` | jq`) | False |
Redirected (> file) | False | Minimal to stdout |
--json flag | Any | JSON to stdout, suppress stderr noise |
See references/json-schemas.md for complete JSON response patterns.
Key conventions:
{"data": [...], "meta": {...}}{"data": {...}}{"error": {"code": "...", "message": "..."}}Semantic exit codes that scripts can rely on:
| Code | Name | Meaning | When |
|---|---|---|---|
| 0 | SUCCESS | Operation completed | Everything worked |
| 1 | ERROR | General/unknown error | Unexpected failures |
| 2 | AUTH_REQUIRED | Not authenticated | No token, token expired |
| 3 | NOT_FOUND | Resource missing | ID doesn't exist |
| 4 | VALIDATION | Invalid input | Bad arguments, failed validation |
| 5 | FORBIDDEN | Permission denied | Authenticated but not authorized |
| 6 | RATE_LIMITED | Too many requests | API throttling |
| 7 | CONFLICT | State conflict | Concurrent modification, duplicate |
# Script can branch on exit code
mytool items get item-001 --json
case $? in
0) echo "Success" ;;
2) echo "Need to authenticate" && mytool auth login ;;
3) echo "Item not found" ;;
*) echo "Error occurred" ;;
esac
# Constants
EXIT_SUCCESS = 0
EXIT_ERROR = 1
EXIT_AUTH_REQUIRED = 2
EXIT_NOT_FOUND = 3
EXIT_VALIDATION = 4
EXIT_FORBIDDEN = 5
EXIT_RATE_LIMITED = 6
EXIT_CONFLICT = 7
# Usage
raise typer.Exit(EXIT_NOT_FOUND)
With --json, errors output structured JSON to stdout AND a message to stderr:
stderr:
Error: Item not found
stdout:
{
"error": {
"code": "NOT_FOUND",
"message": "Item not found",
"details": {
"item_id": "bad-id"
}
}
}
| Code | Exit | Meaning |
|---|---|---|
AUTH_REQUIRED | 2 | Must authenticate first |
TOKEN_EXPIRED | 2 | Token needs refresh |
FORBIDDEN | 5 | Insufficient permissions |
NOT_FOUND | 3 | Resource doesn't exist |
VALIDATION_ERROR | 4 | Invalid input |
INVALID_ARGUMENT | 4 | Bad argument value |
MISSING_ARGUMENT | 4 | Required argument missing |
RATE_LIMITED | 6 | Too many requests |
CONFLICT | 7 | State conflict |
ALREADY_EXISTS | 7 | Duplicate resource |
INTERNAL_ERROR | 1 | Unexpected error |
API_ERROR | 1 | Upstream API failed |
NETWORK_ERROR | 1 | Connection failed |
def _error(
message: str,
code: str = "ERROR",
exit_code: int = EXIT_ERROR,
details: dict = None,
as_json: bool = False,
):
"""Output error and exit."""
error_obj = {"error": {"code": code, "message": message}}
if details:
error_obj["error"]["details"] = details
if as_json:
print(json.dumps(error_obj, indent=2))
# Always print human message to stderr
console.print(f"[red]Error:[/red] {message}")
raise typer.Exit(exit_code)
Every --help output MUST include:
<one-line description>
Usage: <tool> <resource> <action> [OPTIONS] [ARGS]
Arguments:
<arg> Description of positional argument
Options:
-s, --status TEXT Filter by status
-n, --limit INTEGER Max results [default: 20]
--json Output as JSON
-h, --help Show this help
Examples:
<tool> <resource> <action>
<tool> <resource> <action> --status active
<tool> <resource> <action> --json | jq '.[0]'
Examples should show:
jqTools requiring authentication MUST implement:
<tool> auth login # Interactive authentication
<tool> auth status # Check current state
<tool> auth logout # Clear credentials
Recommended: OS keyring with fallbacks for maximum security
Environment variable (CI/CD, testing)
MYTOOL_API_TOKEN or similarOS Keyring (primary storage - secure)
.env file (development fallback)
.gitignoreDependencies:
dependencies = [
"keyring>=24.0.0", # OS keyring access
"python-dotenv>=1.0.0", # .env file support
]
Simple alternative: Just config file in ~/.config/<tool>/
See references/implementation.md for complete credential storage implementations.
When auth is required but missing:
$ mytool items list
Error: Not authenticated. Run: mytool auth login
# exit code: 2
$ mytool items list --json
# stderr: Error: Not authenticated. Run: mytool auth login
{"error": {"code": "AUTH_REQUIRED", "message": "Not authenticated. Run: mytool auth login"}}
# exit code: 2
Input (Flexible): Accept multiple formats for user convenience
| Format | Example | Interpretation |
|---|---|---|
| ISO date | 2025-01-15 | Exact date |
| ISO datetime | 2025-01-15T10:30:00Z | Exact datetime |
| Relative | today, yesterday, tomorrow | Current/previous/next day |
| Relative | last, this (with context) | Previous/current period |
Output (Strict): Always output ISO 8601
{
"created_at": "2025-01-15T10:30:00Z",
"due_date": "2025-02-15",
"month": "2025-01"
}
Store as decimal number, not cents
Include currency when ambiguous
Never format (no "$" or "," in JSON)
{ "total": 1250.50, "currency": "USD" }
Always strings (even if numeric)
Preserve exact format from source
{ "id": "abc_123", "legacy_id": "12345" }
UPPER_SNAKE_CASE in JSON
Case-insensitive input
--status DRAFT --status draft --status Draft
{"status": "IN_PROGRESS"}
# By status
--status DRAFT
--status active,pending # Multiple values
# By date range
--from 2025-01-01 --to 2025-01-31
--month 2025-01
--month last
# By related entity
--user "Alice"
--project "Project X"
# Text search
--search "keyword"
-q "keyword"
# Boolean filters
--archived
--no-archived
--include-deleted
# Limit results
--limit 50
-n 50
# Offset-based
--page 2
--offset 20
# Cursor-based
--cursor "eyJpZCI6MTIzfQ=="
--after "item_123"
See references/implementation.md for complete Python implementation templates including:
# BAD: Progress to stdout
$ bad-tool items list --json
Fetching items...
[{"id": "1"}]
Done!
# GOOD: Only JSON to stdout
$ good-tool items list --json
[{"id": "1"}]
# BAD: Prompts in non-interactive context
$ bad-tool items create
Enter name: _
# GOOD: Fail fast with required flags
$ good-tool items create
Error: --name is required
# BAD: Different flags for same concept
$ tool1 list -j
$ tool2 list --format=json
# GOOD: Same flags everywhere
$ tool1 list --json
$ tool2 list --json
# BAD: Success exit code on failure
$ bad-tool items delete bad-id
Item not found
$ echo $?
0
# GOOD: Semantic exit code
$ good-tool items delete bad-id
Error: Item not found: bad-id
$ echo $?
3
<tool> --version<tool> --help with examples<tool> <resource> list [--json]<tool> <resource> get <id> [--json]--jsonauth login, auth status, auth logout)--quiet and --verbose modes--dry-run for mutations--limit, --page)Typer (preferred for new tools):
Click (acceptable for existing tools):
Typer is built on Click (100% compatible)
Well-structured Click code doesn't need migration
Both must follow same output conventions
import typer from rich.console import Console
app = typer.Typer() console = Console(stderr=True) # UI to stderr
import click from rich.console import Console
console = Console(stderr=True) # Same pattern
Weekly Installs
5
Repository
GitHub Stars
8
First Seen
Feb 5, 2026
Security Audits
Installed on
opencode5
gemini-cli5
claude-code5
replit4
codex4
cursor4
AI新闻播客制作技能:实时新闻转对话式播客脚本与音频生成
1,200 周安装