目录
- 为什么用 Rust 写 CLI
- Clap v4 参数解析
- 子命令与复杂参数
- TUI 交互界面 ratatui
- 日志与可观测性 tracing
- 配置文件管理
- CLI 错误处理
- 进度条与交互反馈
- 发布与分发 cargo-dist
- 速查表与经典工具拆解
1. 为什么用 Rust 写 CLI
CLI 工具是 Rust 最成功的应用场景之一。ripgrep(rg)、fd、bat、exa、zoxide、just、cargo 本身就是 Rust 写的。
| 优势 | 说明 |
|---|---|
| 单二进制 | 静态链接,零依赖分发,一条命令即用 |
| 启动快 | 无 VM 无解释器,毫秒级启动(对比 Python/Node) |
| 内存安全 | 处理恶意文件名/大文件也不会崩溃或越界 |
| 并发原生 | 并行扫描文件系统、多线程管线 |
| 无类型混乱 | 强类型让 CLI 状态机清晰可维护 |
一句话:任何需要「快、稳、零依赖安装」的工具都适合用 Rust。
2. Clap v4 参数解析
clap v4 是事实标准,支持 derive 宏(声明式)与 builder 两种方式。
2.1 derive 方式(推荐)
use clap::Parser;
/// 一个示例文件搜索工具
#[derive(Parser, Debug)]
#[command(name = "rgx", version, about, author)]
struct Args {
/// 搜索的目录(默认当前目录)
#[arg(default_value = ".")]
path: String,
/// 要搜索的关键词
query: String,
/// 忽略大小写
#[arg(short, long)]
ignore_case: bool,
/// 显示行号
#[arg(short = 'n', long = "line-number")]
line_numbers: bool,
/// 最大深度
#[arg(short, long, default_value_t = 5)]
max_depth: usize,
}
fn main() {
let args = Args::parse();
println!("搜索 {} 在 {},忽略大小写={}", args.query, args.path, args.ignore_case);
}
运行 cargo run -- README . -i -n 即可,--help 自动生成:
Usage: rgx [OPTIONS] <PATH> <QUERY>
Arguments:
<PATH> 搜索的目录(默认当前目录)
<QUERY> 要搜索的关键词
Options:
-i, --ignore-case 忽略大小写
-n, --line-number 显示行号
--max-depth <MAX_DEPTH> 最大深度 [default: 5]
-h, --help Print help
2.2 参数类型自动解析
#[derive(Parser, Debug)]
struct Args {
/// 端口号(自动校验 u16 范围)
#[arg(short, long, default_value_t = 8080)]
port: u16,
/// 日志级别
#[arg(short, long, value_enum, default_value_t = LogLevel::Info)]
level: LogLevel,
/// 多次出现的参数 → Vec
#[arg(long)]
tag: Vec<String>,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, Default)]
enum LogLevel { #[default] Info, Debug, Warn, Error }
3. 子命令与复杂参数
大型 CLI(如 cargo build/cargo run)用子命令组织:
use clap::{Parser, Subcommand};
#[derive(Parser, Debug)]
#[command(name = "pm", about = "项目包管理器")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
/// 初始化新项目
Init {
/// 项目名称
name: String,
#[arg(long, default_value = "false")]
git: bool,
},
/// 构建项目
Build {
#[arg(long, default_value_t = false)]
release: bool,
},
/// 发布项目
Publish {
/// 只检查,不实际发布
#[arg(long)]
dry_run: bool,
},
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Init { name, git } => {
println!("初始化 {name},git={git}");
}
Commands::Build { release } => {
println!("构建,release={release}");
}
Commands::Publish { dry_run } => {
println!("发布,dry_run={dry_run}");
}
}
}
3.1 组合值(逗号分隔)
/// 指定多个主机:--host a.com,b.com
#[arg(long, value_delimiter = ',')]
host: Vec<String>,
4. TUI 交互界面 ratatui
需要交互式终端界面(选择器、仪表盘、编辑器)时用 ratatui + crossterm。
use ratatui::{
backend::CrosstermBackend,
layout::{Constraint, Layout, Direction},
widgets::{Block, Borders, List, ListItem, Paragraph, Style},
Terminal,
};
use crossterm::event::{self, Event, KeyCode};
use crossterm::terminal::{enable_raw_mode, disable_raw_mode};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
// 模拟数据源
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(8);
tokio::spawn(async move {
for i in 1..=100 {
tx.send(format!("日志行 {i}")).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
});
let mut lines: Vec<String> = Vec::new();
loop {
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(80), Constraint::Percentage(20)])
.split(f.area());
let list = List::new(
lines.iter().map(|l| ListItem::new(l.clone())).collect::<Vec<_>>()
).block(Block::default().borders(Borders::ALL).title("实时日志"));
f.render_widget(list, chunks[0]);
f.render_widget(Paragraph::new("q: 退出"), chunks[1]);
})?;
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(k) = event::read()? {
if k.code == KeyCode::Char('q') { break; }
}
}
while let Ok(line) = rx.try_recv() {
lines.push(line);
}
}
disable_raw_mode()?;
Ok(())
}
TUI 设计要点:事件循环用
poll而非阻塞read;绘制用增量 diff(ratatui 自动处理);业务逻辑与渲染分离。
5. 日志与可观测性 tracing
tracing 是 Tokio 团队打造的日志/追踪库,与异步生态深度集成。
use tracing::{info, warn, error, debug, instrument};
use tracing_subscriber::{EnvFilter, fmt};
#[instrument]
async fn process_request(id: u64) {
debug!(id, "开始处理请求");
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
info!(id, "处理完成");
}
#[tokio::main]
async fn main() {
// 初始化:从环境变量 RUST_LOG 读取过滤级别
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.init();
process_request(1).await;
}
输出示例:
2026-09-28T10:00:00.000123Z INFO 处理完成: id=1
环境变量控制:RUST_LOG=debug ./mycli 看全部调试日志,RUST_LOG=warn 只看告警。
6. 配置文件管理
CLI 配置的三层优先级:命令行 > 环境变量 > 配置文件 > 默认值。
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Default)]
struct Config {
#[serde(default = "default_api_url")]
api_url: String,
#[serde(default = "default_timeout")]
timeout_secs: u64,
verbose: bool,
}
fn default_api_url() -> String { "https://api.example.com".into() }
fn default_timeout() -> u64 { 30 }
fn load_config(args: &Args) -> Config {
let mut cfg = Config::default();
// 1. 配置文件(~/.config/mycli/config.toml)
if let Some(path) = config_path() {
if path.exists() {
let text = std::fs::read_to_string(path).unwrap_or_default();
cfg = toml::from_str(&text).unwrap_or_default();
}
}
// 2. 环境变量覆盖
if let Ok(url) = std::env::var("MYCLI_API_URL") {
cfg.api_url = url;
}
// 3. 命令行参数优先
if let Some(url) = &args.api_url { cfg.api_url = url.clone(); }
if let Some(t) = args.timeout { cfg.timeout_secs = t; }
cfg
}
fn config_path() -> Option<PathBuf> {
std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.map(|p| p.join("mycli/config.toml"))
}
7. CLI 错误处理
CLI 的错误处理哲学:任何 Err 都要以人类可读的方式退出,且退出码语义化。
use std::process::ExitCode;
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("错误: {e}"); // 输出到 stderr,不污染 stdout 管道
ExitCode::FAILURE
}
}
}
fn run() -> anyhow::Result<()> {
let args = Args::parse();
let data = std::fs::read_to_string(&args.file)
.with_context(|| format!("读取文件 {} 失败", args.file))?;
// ... 业务逻辑
Ok(())
}
退出码约定:
| 码 | 含义 | 用途 |
|---|---|---|
| 0 | 成功 | 正常完成 |
| 1 | 一般错误 | 参数/业务错误 |
| 2 | 用法错误 | clap 默认参数错误 |
| 130 | Ctrl-C | 被中断 |
关键习惯:错误写
eprintln!(stderr),正常输出写println!(stdout)——否则mycli | jq会被日志污染。
8. 进度条与交互反馈
长任务需要进度反馈。indicatif 是标准选择。
use indicatif::{ProgressBar, ProgressStyle};
fn main() {
let pb = ProgressBar::new(100);
pb.set_style(
ProgressStyle::with_template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("#>-"),
);
for i in 0..100 {
std::thread::sleep(std::time::Duration::from_millis(30));
pb.inc(1);
pb.set_message(format!("处理第 {i} 项"));
}
pb.finish_with_message("完成 ✅");
}
9. 发布与分发 cargo-dist
cargo-dist 一键生成安装脚本 + GitHub Release 产物(Linux/macOS/Windows + npm/brew 集成)。
# 初始化
cargo dist init
# 构建发布产物
cargo dist build --tag v1.0.0
# GitHub Actions 自动化(生成 install.ps1 / install.sh / install.psh)
cargo dist plan
生成的安装脚本让用户一行安装:
curl -LsSf https://github.com/you/mycli/releases/latest/download/mycli-installer.sh | sh
发布清单:
| 项 | 做法 |
|---|---|
| 版本 | 语义化版本 + cargo-release |
| CHANGELOG | git-cliff 自动生成 |
| 二进制大小 | cargo build --release + strip + upx |
| 签名 | cosign 签名 + SBOM |
| 自动更新 | self_update crate |
10. 速查表与经典工具拆解
| 场景 | 推荐 crate |
|---|---|
| 参数解析 | clap / lexopt(极简) |
| 终端交互 | ratatui + crossterm |
| 进度条 | indicatif |
| 日志 | tracing + tracing-subscriber |
| 配置 | figment / config |
| 颜色输出 | colored / owo-colors / nu-ansi-term |
| shell 补全 | clap_complete |
| 文件搜索 | ignore(ripgrep 底层) |
| 目录遍历 | walkdir / jwalk(并行) |
| 发布 | cargo-dist / cargo-release |
经典工具拆解:
| 工具 | 用到的能力 |
|---|---|
| ripgrep | 并行扫描 + 内存映射 + SIMD 字节匹配 |
| fd | walkdir 遍历 + 颜色输出 + 交互 select |
| bat | 语法高亮 + 分页 + 主题 |
| zoxide | 频率数据库 + 模糊匹配 |
| just | 子命令 + 环境变量注入 |
一句话记忆:CLI = Clap 解析参数 + tracing 记日志 + anyhow 管错误 + indicatif 给反馈 + cargo-dist 分发,五个环节各配一个 crate,生产级工具手到擒来。
延伸阅读
- https://plumephp.com/rust-toolchain-guide/ — Cargo 高级特性与发布流程
- https://plumephp.com/rust-error-handling-testing/ — anyhow 错误处理设计
- https://plumephp.com/posts/github-actions/ — CI/CD 自动化发布
- https://plumephp.com/posts/linux/ — 系统运维与 shell 生态
- [[tools]] — 开发者工具链生态
命令行工具是每个开发者每天打交道的对象,用 Rust 写一个高质量的 CLI,是学习 Rust 最实用、回报最高的路径之一。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。