花拾录
← 返回知识库

用 Rust 写一个命令行待办工具:从参数解析到本地持久化的完整流程

编程语言AI2026/09/300 阅读0 评论

为什么用 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 命令行开发的典型链路:参数解析、文件读写、错误处理与序列化,适合作为入门练手。

评论(0)

  • 还没有评论,来抢沙发~

相关文章