rust-cli

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

rust-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
oss-publish
skill.
当你需要设计或实现具备生产级易用性和自动化能力的Rust CLI时,可以参考本技能。
若需了解与语言无关的开源项目发布/维护规范(LICENSE/SECURITY.md、发布说明、CI策略、仓库初始化约定),请参考
oss-publish
技能。

Defaults (Agent-Friendly)

适配Agent的默认配置

  • CLI parsing:
    clap
    derive.
  • Error handling:
    anyhow
    (or
    thiserror
    for library-style errors).
  • Logging/diagnostics:
    tracing
    +
    tracing-subscriber
    (write logs to stderr).
  • Structured output:
    serde
    +
    serde_json
    .
  • CLI解析:
    clap
    派生宏
  • 错误处理:
    anyhow
    (若为库级错误可使用
    thiserror
  • 日志/诊断:
    tracing
    +
    tracing-subscriber
    (日志输出至stderr)
  • 结构化输出:
    serde
    +
    serde_json

Companion Skill: agentic-cli-design

配套技能:agentic-cli-design

If this CLI will be operated by AI agents and/or automation, also consult the
agentic-cli-design
skill. Borrow these concepts: machine-readable output, non-interactive operation, idempotent commands, safe-by-default behavior, observability, and introspection.
如果该CLI将由AI Agent或自动化系统操作,还需参考
agentic-cli-design
技能。可借鉴以下理念:机器可读输出、非交互式操作、幂等命令、默认安全行为、可观测性与自省能力。

Cross-platform testing pitfalls

跨平台测试陷阱

Home directory override (Windows limitation)

主目录覆盖限制(Windows平台局限)

On Windows,
directories::BaseDirs
calls Windows API (
SHGetKnownFolderPath
) directly; setting
HOME
or
USERPROFILE
environment variables does not override the returned path. This differs from Unix/Linux/macOS, where
directories
typically reads
$HOME
.
PlatformBehaviorOverride via env?
Unix/Linux/macOSReads
$HOME
environment variable
✅ Yes
WindowsCalls Win32 API (
SHGetKnownFolderPath
)
❌ 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:
FunctionPlatform behaviorOverride via env?
std::env::temp_dir()
Calls OS API❌ No
std::env::current_exe()
Calls OS API❌ No
directories::ProjectDirs
(config/cache)
Windows: API, Unix: XDG varsPartial (Unix only)
Recommendation: Design functions to accept explicit paths where testability matters; use
BaseDirs
/
ProjectDirs
only in top-level CLI entrypoint or well-isolated modules.
在Windows系统中,
directories::BaseDirs
直接调用Windows API(
SHGetKnownFolderPath
);设置
HOME
USERPROFILE
环境变量无法覆盖其返回路径。这与Unix/Linux/macOS平台不同,后者的
directories
库通常读取
$HOME
变量。
平台行为可通过环境变量覆盖?
Unix/Linux/macOS读取
$HOME
环境变量
✅ 是
Windows调用Win32 API(
SHGetKnownFolderPath
❌ 否
测试设计模式:
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");
    // 跨所有平台兼容
}
其他存在类似问题的常见场景:
函数平台行为可通过环境变量覆盖?
std::env::temp_dir()
调用系统API❌ 否
std::env::current_exe()
调用系统API❌ 否
directories::ProjectDirs
(配置/缓存目录)
Windows:调用API;Unix:读取XDG变量部分支持(仅Unix)
建议: 在对可测试性有要求的场景中,设计函数时直接接收显式路径;仅在顶层CLI入口或隔离模块中使用
BaseDirs
/
ProjectDirs

Flaky tests: time-based cutoffs (SystemTime + second precision)

不稳定测试:基于时间的截止条件(SystemTime + 秒级精度)

If you store timestamps as Unix seconds (
i64
) and compute a cleanup cutoff using
SystemTime::now()
, tests can become flaky across platforms. This often passes on Linux/macOS and fails on Windows due to timing/resolution differences.
Typical failure mode (SQLite example):
  • record()
    inserts
    created_at = now_secs()
    (e.g.
    1707408142
    ).
  • a moment later,
    cleanup_old_entries(0)
    computes
    cutoff = now_secs()
    (e.g.
    1707408143
    ).
  • SQL uses
    WHERE created_at < cutoff
    which matches the freshly inserted row (
    1707408142 < 1707408143
    ).
Recommended fixes (pick one that matches intended semantics):
  • Tests: avoid boundary conditions; use a large margin (e.g.
    cleanup_old_entries(1)
    instead of
    0
    ), and optionally insert a small sleep between operations to avoid same-second edges.
  • Implementation: define
    days <= 0
    semantics explicitly (often a no-op), or inject a clock so tests can be deterministic. If you keep second-precision storage, be careful with
    <
    vs
    <=
    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时间秒(
i64
)存储时间戳,并使用
SystemTime::now()
计算清理截止时间,跨平台测试可能会出现不稳定的情况。此类测试通常在Linux/macOS上通过,但在Windows上因时间精度差异而失败。
典型失败场景(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

其他常见跨平台差异

FeatureUnix/macOSWindowsTesting approach
Path separator
/
\
Always use
std::path::Path
Line endings
\n
\r\n
Explicitly specify in tests or use
.replace()
Executable extensionnone
.exe
Use
env!("CARGO_BIN_EXE_<name>")
Case sensitivityYesNoTest with varied cases on Windows
Temp directory
/tmp
or
/var/tmp
%TEMP%
Use
std::env::temp_dir()
or
tempfile
crate
特性Unix/macOSWindows测试方案
路径分隔符
/
\
始终使用
std::path::Path
行结束符
\n
\r\n
测试中显式指定或使用
.replace()
处理
可执行文件扩展名
.exe
使用
env!("CARGO_BIN_EXE_<name>")
大小写敏感性在Windows平台测试不同大小写的情况
临时目录
/tmp
/var/tmp
%TEMP%
使用
std::env::temp_dir()
tempfile
crate

Rust-specific workflow (minimal)

Rust专属极简工作流

  1. Implement a JSON output mode (e.g.
    --json
    ) with stdout reserved for the result.
  2. Send logs/diagnostics to stderr (e.g. via
    tracing
    ).
  3. Use predictable exit codes and integration tests that execute the compiled binary.
  4. For repo-wide release/quality gate conventions, see
    oss-publish
    .
  1. 实现JSON输出模式(例如
    --json
    参数),标准输出仅用于返回结果
  2. 将日志/诊断信息输出至stderr(例如通过
    tracing
  3. 使用可预测的退出码,并编写执行编译后二进制文件的集成测试
  4. 若需仓库级的发布/质量门禁约定,请参考
    oss-publish
    技能

Release workflow (cargo-release)

基于cargo-release的发布工作流

Use
cargo-release
to bump versions, create git tags, and (optionally) publish to crates.io.
Install:
bash
cargo install cargo-release
使用
cargo-release
工具来升级版本、创建Git标签,以及(可选)发布至crates.io。
安装命令:
bash
cargo install cargo-release

Bump version + tag (no publish)

升级版本+创建标签(不发布)

This is a safe default when you want to control publishing manually:
bash
undefined
当你希望手动控制发布流程时,这是安全的默认操作:
bash
undefined

Patch: 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 publish
cargo release major --execute --no-confirm --no-publish

注意事项:

- `--no-confirm`使命令变为非交互式(适配Agent/CI环境);若需安全确认可省略该参数
- `--no-publish`将crates.io发布保留为显式操作步骤

升级版本并创建标签后,可执行显式发布:

```bash
cargo publish

Publish 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 test

Ensures the package can be built as it will be uploaded

确保包可按上传要求构建

cargo publish --dry-run
undefined
cargo publish --dry-run
undefined

Optional: publish from a tag

可选:从已有标签发布

If you need to publish an already-created tag:
bash
git checkout <tag>
cargo publish
Recommendation: 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