rust-cli
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineserust-cli - Build Maintainable Rust CLIs
rust-cli - 构建可维护的Rust CLI程序
Use this skill when you need to design or implement a Rust CLI with production-grade ergonomics and automation.
For language-agnostic OSS publication/release hygiene (LICENSE/SECURITY.md, release notes, CI policy, repo bootstrap conventions), consult the skill.
oss-publish当你需要设计或实现具备生产级易用性和自动化能力的Rust CLI时,可以参考本技能。
若需了解与语言无关的开源项目发布/维护规范(LICENSE/SECURITY.md、发布说明、CI策略、仓库初始化约定),请参考技能。
oss-publishDefaults (Agent-Friendly)
适配Agent的默认配置
- CLI parsing: derive.
clap - Error handling: (or
anyhowfor library-style errors).thiserror - Logging/diagnostics: +
tracing(write logs to stderr).tracing-subscriber - Structured output: +
serde.serde_json
- CLI解析:派生宏
clap - 错误处理:(若为库级错误可使用
anyhow)thiserror - 日志/诊断:+
tracing(日志输出至stderr)tracing-subscriber - 结构化输出:+
serdeserde_json
Companion Skill: agentic-cli-design
配套技能:agentic-cli-design
If this CLI will be operated by AI agents and/or automation, also consult the skill.
Borrow these concepts: machine-readable output, non-interactive operation, idempotent commands, safe-by-default behavior, observability, and introspection.
agentic-cli-design如果该CLI将由AI Agent或自动化系统操作,还需参考技能。可借鉴以下理念:机器可读输出、非交互式操作、幂等命令、默认安全行为、可观测性与自省能力。
agentic-cli-designCross-platform testing pitfalls
跨平台测试陷阱
Home directory override (Windows limitation)
主目录覆盖限制(Windows平台局限)
On Windows, calls Windows API () directly; setting or environment variables does not override the returned path. This differs from Unix/Linux/macOS, where typically reads .
directories::BaseDirsSHGetKnownFolderPathHOMEUSERPROFILEdirectories$HOME| Platform | Behavior | Override via env? |
|---|---|---|
| Unix/Linux/macOS | Reads | ✅ Yes |
| Windows | Calls Win32 API ( | ❌ No |
Test design patterns:
rust
// Pattern 1: Unix-only test (recommended for HOME override)
#[test]
#[cfg(not(target_os = "windows"))]
fn test_home_override() {
let temp = TempDir::new().unwrap();
env::set_var("HOME", temp.path());
// Test code that uses directories::BaseDirs
}
// Pattern 2: Platform-specific logic
#[test]
fn test_cross_platform_home() {
#[cfg(unix)]
{
env::set_var("HOME", "/tmp/test");
// Unix-specific test
}
#[cfg(windows)]
{
// Windows: use explicit paths or dependency injection
let test_dir = PathBuf::from("C:\\temp\\test");
// Windows-specific test
}
}
// Pattern 3: Dependency injection (best for portability)
fn install_skill_to_path(base_dir: &Path, skill_name: &str) {
// Accept path directly, avoid BaseDirs in implementation
}
#[test]
fn test_install_with_explicit_path() {
let temp = TempDir::new().unwrap();
install_skill_to_path(temp.path(), "my-skill");
// Portable across all platforms
}Other common cases with similar issues:
| Function | Platform behavior | Override via env? |
|---|---|---|
| Calls OS API | ❌ No |
| Calls OS API | ❌ No |
| Windows: API, Unix: XDG vars | Partial (Unix only) |
Recommendation: Design functions to accept explicit paths where testability matters; use / only in top-level CLI entrypoint or well-isolated modules.
BaseDirsProjectDirs在Windows系统中,直接调用Windows API();设置或环境变量无法覆盖其返回路径。这与Unix/Linux/macOS平台不同,后者的库通常读取变量。
directories::BaseDirsSHGetKnownFolderPathHOMEUSERPROFILEdirectories$HOME| 平台 | 行为 | 可通过环境变量覆盖? |
|---|---|---|
| Unix/Linux/macOS | 读取 | ✅ 是 |
| Windows | 调用Win32 API( | ❌ 否 |
测试设计模式:
rust
// 模式1:仅Unix平台测试(推荐用于主目录覆盖场景)
#[test]
#[cfg(not(target_os = "windows"))]
fn test_home_override() {
let temp = TempDir::new().unwrap();
env::set_var("HOME", temp.path());
// 测试使用directories::BaseDirs的代码
}
// 模式2:平台专属逻辑
#[test]
fn test_cross_platform_home() {
#[cfg(unix)]
{
env::set_var("HOME", "/tmp/test");
// Unix平台专属测试
}
#[cfg(windows)]
{
// Windows:使用显式路径或依赖注入
let test_dir = PathBuf::from("C:\\temp\\test");
// Windows平台专属测试
}
}
// 模式3:依赖注入(最佳可移植性方案)
fn install_skill_to_path(base_dir: &Path, skill_name: &str) {
// 直接接收路径,实现中避免使用BaseDirs
}
#[test]
fn test_install_with_explicit_path() {
let temp = TempDir::new().unwrap();
install_skill_to_path(temp.path(), "my-skill");
// 跨所有平台兼容
}其他存在类似问题的常见场景:
| 函数 | 平台行为 | 可通过环境变量覆盖? |
|---|---|---|
| 调用系统API | ❌ 否 |
| 调用系统API | ❌ 否 |
| Windows:调用API;Unix:读取XDG变量 | 部分支持(仅Unix) |
建议: 在对可测试性有要求的场景中,设计函数时直接接收显式路径;仅在顶层CLI入口或隔离模块中使用/。
BaseDirsProjectDirsFlaky tests: time-based cutoffs (SystemTime + second precision)
不稳定测试:基于时间的截止条件(SystemTime + 秒级精度)
If you store timestamps as Unix seconds () and compute a cleanup cutoff using , tests can become flaky across platforms.
This often passes on Linux/macOS and fails on Windows due to timing/resolution differences.
i64SystemTime::now()Typical failure mode (SQLite example):
- inserts
record()(e.g.created_at = now_secs()).1707408142 - a moment later, computes
cleanup_old_entries(0)(e.g.cutoff = now_secs()).1707408143 - SQL uses which matches the freshly inserted row (
WHERE created_at < cutoff).1707408142 < 1707408143
Recommended fixes (pick one that matches intended semantics):
- Tests: avoid boundary conditions; use a large margin (e.g. instead of
cleanup_old_entries(1)), and optionally insert a small sleep between operations to avoid same-second edges.0 - Implementation: define semantics explicitly (often a no-op), or inject a clock so tests can be deterministic. If you keep second-precision storage, be careful with
days <= 0vs<and how you define "older than N days".<=
Example test adjustment (stable across platforms):
rust
#[test]
fn test_cleanup_old_entries() {
let (ledger, _temp) = create_test_ledger();
ledger
.record("test-1", "hash-1", "tweet-1", "success")
.unwrap();
// Ensure the cutoff is not computed in the exact same instant.
std::thread::sleep(std::time::Duration::from_millis(10));
// Use a safe margin: a fresh entry should not be deleted.
let deleted = ledger.cleanup_old_entries(1).unwrap();
assert_eq!(deleted, 0);
let entry = ledger.lookup("test-1").unwrap();
assert!(entry.is_some());
}如果你以Unix时间秒()存储时间戳,并使用计算清理截止时间,跨平台测试可能会出现不稳定的情况。此类测试通常在Linux/macOS上通过,但在Windows上因时间精度差异而失败。
i64SystemTime::now()典型失败场景(SQLite示例):
- 插入
record()(例如created_at = now_secs())1707408142 - 片刻后,计算
cleanup_old_entries(0)(例如cutoff = now_secs())1707408143 - SQL语句使用,会匹配刚插入的记录(
WHERE created_at < cutoff)1707408142 < 1707408143
推荐修复方案(选择符合预期语义的一种):
- 测试层面: 避免边界条件;使用较大的时间余量(例如而非
cleanup_old_entries(1)),必要时在操作间插入短暂延迟以规避秒级边界问题。0 - 实现层面: 明确定义的语义(通常为无操作),或注入时钟以实现确定性测试。若保留秒级精度存储,需注意
days <= 0与<的区别,以及“早于N天”的定义方式。<=
跨平台稳定测试示例:
rust
#[test]
fn test_cleanup_old_entries() {
let (ledger, _temp) = create_test_ledger();
ledger
.record("test-1", "hash-1", "tweet-1", "success")
.unwrap();
// 确保截止时间不会与记录时间完全重合
std::thread::sleep(std::time::Duration::from_millis(10));
// 使用安全余量:新插入的记录不应被删除
let deleted = ledger.cleanup_old_entries(1).unwrap();
assert_eq!(deleted, 0);
let entry = ledger.lookup("test-1").unwrap();
assert!(entry.is_some());
}Other common cross-platform differences
其他常见跨平台差异
| Feature | Unix/macOS | Windows | Testing approach |
|---|---|---|---|
| Path separator | | | Always use |
| Line endings | | | Explicitly specify in tests or use |
| Executable extension | none | | Use |
| Case sensitivity | Yes | No | Test with varied cases on Windows |
| Temp directory | | | Use |
| 特性 | Unix/macOS | Windows | 测试方案 |
|---|---|---|---|
| 路径分隔符 | | | 始终使用 |
| 行结束符 | | | 测试中显式指定或使用 |
| 可执行文件扩展名 | 无 | | 使用 |
| 大小写敏感性 | 是 | 否 | 在Windows平台测试不同大小写的情况 |
| 临时目录 | | | 使用 |
Rust-specific workflow (minimal)
Rust专属极简工作流
- Implement a JSON output mode (e.g. ) with stdout reserved for the result.
--json - Send logs/diagnostics to stderr (e.g. via ).
tracing - Use predictable exit codes and integration tests that execute the compiled binary.
- For repo-wide release/quality gate conventions, see .
oss-publish
- 实现JSON输出模式(例如参数),标准输出仅用于返回结果
--json - 将日志/诊断信息输出至stderr(例如通过)
tracing - 使用可预测的退出码,并编写执行编译后二进制文件的集成测试
- 若需仓库级的发布/质量门禁约定,请参考技能
oss-publish
Release workflow (cargo-release)
基于cargo-release的发布工作流
Use to bump versions, create git tags, and (optionally) publish to crates.io.
cargo-releaseInstall:
bash
cargo install cargo-release使用工具来升级版本、创建Git标签,以及(可选)发布至crates.io。
cargo-release安装命令:
bash
cargo install cargo-releaseBump version + tag (no publish)
升级版本+创建标签(不发布)
This is a safe default when you want to control publishing manually:
bash
undefined当你希望手动控制发布流程时,这是安全的默认操作:
bash
undefinedPatch: 0.1.0 -> 0.1.1
补丁版本:0.1.0 -> 0.1.1
cargo release patch --execute --no-confirm --no-publish
cargo release patch --execute --no-confirm --no-publish
Minor: 0.1.0 -> 0.2.0
小版本:0.1.0 -> 0.2.0
cargo release minor --execute --no-confirm --no-publish
cargo release minor --execute --no-confirm --no-publish
Major: 0.1.0 -> 1.0.0
大版本:0.1.0 -> 1.0.0
cargo release major --execute --no-confirm --no-publish
Notes:
- `--no-confirm` makes the command non-interactive (agent/CI friendly). Use without it for a safety prompt.
- `--no-publish` keeps crates.io publishing as an explicit step.
After bumping/tagging, publish explicitly:
```bash
cargo publishcargo release major --execute --no-confirm --no-publish
注意事项:
- `--no-confirm`使命令变为非交互式(适配Agent/CI环境);若需安全确认可省略该参数
- `--no-publish`将crates.io发布保留为显式操作步骤
升级版本并创建标签后,可执行显式发布:
```bash
cargo publishPublish preflight checks
发布前预检
Before publishing (especially in automation), prefer these checks:
bash
cargo fmt
cargo clippy -- -D warnings
cargo test发布前(尤其是自动化场景),建议执行以下检查:
bash
cargo fmt
cargo clippy -- -D warnings
cargo testEnsures the package can be built as it will be uploaded
确保包可按上传要求构建
cargo publish --dry-run
undefinedcargo publish --dry-run
undefinedOptional: publish from a tag
可选:从已有标签发布
If you need to publish an already-created tag:
bash
git checkout <tag>
cargo publishRecommendation: keep the release workflow simple.
In most repos, publishing from a clean working tree on the release commit (the one that was tagged) is sufficient.
若需基于已创建的标签发布:
bash
git checkout <tag>
cargo publish建议:保持发布工作流简洁。在大多数仓库中,基于发布提交(即被打标签的提交)的干净工作区进行发布即可。
Templates
模板资源
- Crate selection:
rust-cli/references/crates.md - Copy/paste scaffolding (Cargo.toml, main.rs, tests, prek config):
rust-cli/references/templates.md
- 依赖 crate 选型参考:
rust-cli/references/crates.md - 可复制粘贴的脚手架代码(Cargo.toml、main.rs、测试代码、prek配置):
rust-cli/references/templates.md