为什么用 Rust 写 CLI 工具
Rust 编译为原生二进制,无运行时依赖,启动快,适合做命令行工具。本文从零实现一个 todo 命令,支持添加、列表、完成、删除任务,数据保存到本地 JSON 文件。
项目初始化
cargo new todo-cli
cd todo-cli
在 Cargo.toml 中添加依赖:
[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
clap 用于参数解析,serde 和 serde_json 负责序列化与持久化。
定义数据结构
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Task {
pub id: u32,
pub title: String,
pub done: bool,
}
参数解析
用 clap 的 derive 模式定义子命令:
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(name = "todo", version = "0.1.0")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Add { title: String },
List,
Done { id: u32 },
Remove { id: u32 },
}
入口函数中匹配命令并调用对应逻辑:
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Add { title } => add_task(&title),
Commands::List => list_tasks(),
Commands::Done { id } => mark_done(id),
Commands::Remove { id } => remove_task(id),
}
}
本地持久化
选择用户主目录下的 .todo.json 作为存储位置。读取时若文件不存在则返回空列表:
use std::fs;
use std::path::PathBuf;
fn data_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".todo.json")
}
fn load_tasks() -> Vec<Task> {
let path = data_path();
if !path.exists() {
return Vec::new();
}
let content = fs::read_to_string(path).unwrap_or_default();
serde_json::from_str(&content).unwrap_or_default()
}
fn save_tasks(tasks: &[Task]) {
let json = serde_json::to_string_pretty(tasks).expect("序列化失败");
fs::write(data_path(), json).expect("写入失败");
}
实现各子命令
添加任务时,用已有任务的最大 id 加一保证唯一:
fn add_task(title: &str) {
let mut tasks = load_tasks();
let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
tasks.push(Task { id: next_id, title: title.to_string(), done: false });
save_tasks(&tasks);
println!("已添加任务 #{}: {}", next_id, title);
}
列表打印状态标记:
fn list_tasks() {
let tasks = load_tasks();
if tasks.is_empty() {
println!("暂无任务");
return;
}
for t in tasks {
let mark = if t.done { "[x]" } else { "[ ]" };
println!("{} #{} {}", mark, t.id, t.title);
}
}
完成与删除任务,通过 id 查找并修改:
fn mark_done(id: u32) {
let mut tasks = load_tasks();
if let Some(t) = tasks.iter_mut().find(|t| t.id == id) {
t.done = true;
save_tasks(&tasks);
println!("任务 #{} 已完成", id);
} else {
println!("未找到任务 #{}", id);
}
}
fn remove_task(id: u32) {
let mut tasks = load_tasks();
let len = tasks.len();
tasks.retain(|t| t.id != id);
if tasks.len() < len {
save_tasks(&tasks);
println!("已删除任务 #{}", id);
} else {
println!("未找到任务 #{}", id);
}
}
编译与使用
cargo build --release
./target/release/todo add "写周报"
./target/release/todo list
./target/release/todo done 1
./target/release/todo remove 1
可扩展方向
- 用
--data参数自定义存储路径,便于测试。 - 增加
priority字段并按优先级排序输出。 - 引入
anyhow统一错误处理,替换unwrap。 - 用
chrono记录创建时间。
这个项目覆盖了 Rust 命令行开发的典型链路:参数解析、文件读写、错误处理与序列化,适合作为入门练手。