Cargo 自定义子命令把 AI 工具的功能拆成 cargo xxx 一键调用一、Cargo 子命令的工作原理Cargo 的子命令机制设计得极其简单只要你的系统 PATH 里有一个叫cargo-xxx的可执行文件就能用cargo xxx调用它。flowchart LR A[用户输入: cargo review] -- B{cargo 查找} B --|内置命令| C[执行内置逻辑] B --|非内置| D[搜索 PATH 中的 cargo-review] D --|找到| E[执行 cargo-review] D --|没找到| F[提示: no such subcommand] E -- G[cargo-review 接收原样参数] G -- H[可以解析 Cargo.toml / 调用 cargo metadata] style E fill:#6bcb77,stroke:#333 style F fill:#ff6b6b,stroke:#333具体细节cargo review→ 找cargo-review可执行文件cargo review --fix→cargo-review收到参数--fixcargo review ./src/→cargo-review收到参数./src/内置命令build、test、run等优先级高于自定义子命令一个真实踩坑我第一次做cargo review时装了工具在项目目录里敲cargo reviewCargo 却告诉我no such subcommand。排查了半小时发现是可执行文件叫cargo_review用下划线而 Cargo 只认连字符cargo-review。Rust 编译器会自动把 crate 名字里的连字符变成下划线但操作系统找的是连字符版本的文件名。这个细节不大但很容易踩到。二、实现第一个 AI 代码审查子命令cargo-review先从一个完整的例子开始。我们要做一个cargo review命令它会读取当前项目的Cargo.toml获取项目信息收集待审查的 Rust 源文件调用 AI API 做代码审查输出审查报告// cargo-review 主程序 // 编译后的可执行文件命名为 cargo-review放到 PATH 中 use clap::Parser; use std::path::PathBuf; use std::process::Command; /// cargo review —— AI 代码审查工具 /// /// 用法: cargo review [OPTIONS] [PATH] #[derive(Parser, Debug)] #[command(name cargo review, about AI 代码审查工具)] struct Cli { /// 要审查的文件或目录默认为当前目录下的 src/ #[arg(default_value .)] path: PathBuf, /// 自动修复简单问题 #[arg(long, short f)] fix: bool, /// 审查严格程度: loose | normal | strict #[arg(long, default_value normal)] level: String, /// 输出格式: text | json | markdown #[arg(long, default_value text)] output: String, } fn main() - Result(), String { let cli Cli::parse(); // 1️⃣ 获取 Rust 项目根目录向上寻找 Cargo.toml let project_root find_project_root(cli.path) .map_err(|e| format!(找不到 Cargo.toml: {e}))?; println!( 项目目录: {}, project_root.display()); // 2️⃣ 收集要审查的文件 let target if cli.path.is_dir() { cli.path.join(src) } else { cli.path.clone() }; let rust_files collect_rust_files(target)?; if rust_files.is_empty() { return Err(没有找到 Rust 源文件.to_string()); } println!( 找到 {} 个 Rust 文件待审查, rust_files.len()); // 3️⃣ 逐文件审查 let mut report String::new(); for file in rust_files { let content std::fs::read_to_string(file) .map_err(|e| format!(读取 {} 失败: {e}, file.display()))?; println!( 正在审查: {}, file.display()); let review ai_review_code( content, file.display().to_string(), cli.level, )?; report.push_str(review); } // 4️⃣ 输出报告 match cli.output.as_str() { json println!({}, format_as_json(report)), markdown { let md_path project_root.join(review-report.md); std::fs::write(md_path, report) .map_err(|e| format!(写入报告失败: {e}))?; println!( 报告已保存到: {}, md_path.display()); } _ println!({}, report), // 默认文本输出 } // 5️⃣ 可选自动修复 if cli.fix { println!( 自动修复模式已启用此功能待实现); } Ok(()) }找到项目根目录这是 cargo 子命令最重要的能力——自动识别当前 Cargo 项目/// 向上遍历目录树找到包含 Cargo.toml 的目录 fn find_project_root(start: PathBuf) - ResultPathBuf, String { let mut current if start.is_file() { start.parent() .unwrap_or(start) .to_path_buf() } else { start.clone() }; // 一直向上找直到找到 Cargo.toml 或到达文件系统根 loop { let cargo_toml current.join(Cargo.toml); if cargo_toml.exists() { return Ok(current); } match current.parent() { Some(parent) current parent.to_path_buf(), None break, } } Err(未找到 Cargo.toml请确保在 Rust 项目内运行.to_string()) } /// 收集指定目录下所有 .rs 文件 fn collect_rust_files(dir: PathBuf) - ResultVecPathBuf, String { if !dir.exists() { return Err(format!(目录不存在: {}, dir.display())); } let mut files Vec::new(); // 使用 walkdir 遍历目录 for entry in walkdir::WalkDir::new(dir) .into_iter() .filter_map(|e| e.ok()) { let path entry.path(); if path.extension().map_or(false, |ext| ext rs) { files.push(path.to_path_buf()); } } Ok(files) }调用 AI 做代码审查/// AI 代码审查 —— 调用 LLM API fn ai_review_code(code: str, filename: str, level: str) - ResultString, String { // 构建审查提示词 let strictness match level { loose 只关注明显的逻辑错误和安全漏洞, strict 严格审查包括命名规范、代码风格、潜在性能问题, _ 常规审查关注正确性、安全性和可读性, }; let prompt format!( r#你是一个 Rust 代码审查助手。{strictness}。 请审查以下文件 {filename}按照以下格式输出 ### 严重问题 影响正确性或安全性的问题 ### 改进建议 代码风格、性能、可读性方面的建议 ### 学习建议 如果发现可以优化的地方给出 Rust 最佳实践说明 rust {code} #, ); // 调用 AI API这里用模拟实现实际替换成你的 API 调用 call_ai_api(prompt) } fn call_ai_api(prompt: str) - ResultString, String { // 实际项目中替换为 // let client reqwest::blocking::Client::new(); // let resp client.post(https://api.openai.com/v1/...) // .json(request_body) // .send()?; // let body resp.text()?; // 模拟返回 Ok(format!( r#### 严重问题 暂无发现严重问题。 ### 改进建议 - 建议添加更多文档注释 ### 学习建议 - 可以使用 clippy 检查 Rust 代码风格 --- 共审查 {} 行代码 #, prompt.lines().count() )) }格式化输出/// 将报告转为 JSON 格式 fn format_as_json(report: str) - String { // 简化实现 —— 实际应该解析报告结构 format!(r#{{report: {}}}#, report.replace(\n, \\n)) }三、安装和使用cargo install一键搞定编译并安装到系统 PATH 中最方便的方式就是cargo install# Cargo.toml [package] name cargo-review version 0.1.0 edition 2021 description AI 代码审查工具 [dependencies] clap { version 4, features [derive] } walkdir 2 serde { version 1, features [derive] } serde_json 1 [[bin]] name cargo-review # ← 关键可执行文件叫 cargo-review path src/main.rs安装步骤# 在项目目录下 cargo install --path . # 安装完成后cargo-review 会在 ~/.cargo/bin/ 下 # 确保这个目录在 PATH 中rustup 通常已经配好了 # 使用 cargo review # 审查当前项目 cargo review ./src/main.rs # 审查单个文件 cargo review --level strict # 严格审查模式 cargo review --output markdown # 输出 Markdown 报告 cargo review --fix # 审查并修复生产环境实战经验cargo install --path .有个细节如果你改完代码想更新安装版本直接重新cargo install --path .不会重新编译除非加--force。我第一次更新时改了参数结构装完还是旧的。后来嫌麻烦改成了在 CI 里cargo install --path . --force --debug开发阶段用 debug 构建速度快。但注意 debug 构建性能差很多分发给别人还是要用 release 构建。四、更完整的体系cargo ai命令家族如果把多个 AI 工具组织在一起可以设计成cargo ai命令家族flowchart TD A[cargo ai] -- B[子命令] B -- C[review: 代码审查] B -- D[gen: 生成测试代码] B -- E[doc: 生成文档注释] B -- F[explain: 解释代码逻辑] B -- G[refactor: 重构建议] C -- C1[cargo review 已有] D -- D1[cargo ai-gen 待实现] E -- E1[cargo ai-doc 待实现] F -- F1[cargo ai-explain 待实现] G -- G1[cargo ai-refactor 待实现]实现cargo ai作为入口// cargo-ai 主程序 // 用法: cargo ai SUBCOMMAND use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name cargo ai, about AI 辅助开发工具集)] struct Cli { #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// AI 代码审查 Review { #[arg(default_value .)] path: PathBuf, #[arg(long, default_value normal)] level: String, }, /// AI 生成测试代码 GenTest { #[arg(default_value .)] path: PathBuf, }, /// AI 生成文档注释 GenDoc { #[arg(default_value .)] path: PathBuf, }, /// AI 解释代码逻辑 Explain { file: PathBuf, /// 解释的详细程度: brief | normal | detailed #[arg(long, default_value normal)] detail: String, }, } fn main() - Result(), String { let cli Cli::parse(); match cli.command { Commands::Review { path, level } { // 委托给 review 逻辑 run_review(path, level) } Commands::GenTest { path } { println!( 生成测试功能开发中... path{}, path.display()); Ok(()) } Commands::GenDoc { path } { println!( 文档生成功能开发中... path{}, path.display()); Ok(()) } Commands::Explain { file, detail } { run_explain(file, detail) } } }使用示例# 安装后以下命令都可用 cargo ai review # 代码审查 cargo ai review --level strict # 严格审查 cargo ai gen-test --path ./src/ # 生成测试 cargo ai gen-doc # 生成文档 cargo ai explain ./src/main.rs --detail detailed # 解释代码命名冲突的边界案例设计cargo ai命令家族时需要考虑命名冲突。如果其他人也写了一个cargo-ai并且 bin 名字也注册成cargo-ai两个工具安装到同一个 PATH 就冲突了。目前 Cargo 没有命名空间机制只能靠文档说明如果你是 cargo-ai 作者请在文档里声明。团队内部用的话风险不大但如果发布到 crates.io建议调研一下是否有同名工具。我在一次实际开发中cargo review --fix不小心覆盖了一个正在编辑的文件因为没加--dry-run预览。后来加了一个--confirm参数默认预览、人工确认后再修改。五、总结这篇文章梳理了 Cargo 自定义子命令的三个要点约定大于配置只要可执行文件以cargo-开头放进 PATH就能用cargo xxx调用。这是 Unix 哲学在 Rust 工具链中的体现。clap 做参数解析cargo xxx后面的所有参数都会原样传给cargo-xxx用 clap 的#[derive(Parser)]可以像普通 CLI 工具一样处理。cargo install 分发cargo install --path .自动编译并安装到~/.cargo/bin/用户一行命令就能用上。配合 crates.io 发布别人cargo install cargo-review就能用。作为独立开发者Cargo 的设计让我学会了一件事好工具的设计应该让人猜得出来。不需要读文档只要知道cargo test存在自然就能想到cargo review也应该是类似的工作方式。如果你也在写 Rust 工具强烈建议做成 cargo 子命令。不仅能复用 Cargo 的项目上下文自动找Cargo.toml还能让用户零学习成本上手。有问题欢迎评论区交流