用Rust开发一个命令行Todo应用

225 阅读7分钟

项目背景

Todo应用是日常开发中非常常见的工具,通常用于记录和管理待办事项。通过Todo应用,用户可以方便地记录自己的任务,跟踪任务的完成情况,提高工作和生活效率。在实际开发过程中,Todo应用通常包括任务的增、删、改、查(CRUD)操作,并且常常涉及数据的持久化存储(例如:数据库、文件系统等)。

命令行Todo应用则是通过命令行界面(CLI)进行交互的应用程序,不依赖于图形用户界面(GUI)。这种类型的应用不仅有助于学习如何使用命令行进行交互,还能加深对应用程序内部逻辑的理解。在Rust中实现一个命令行Todo应用,不仅能帮助我们熟悉Rust的基本编程概念(如:结构体、枚举、错误处理、模块化等),还能够通过实践掌握Rust的文件读写、命令行参数处理、以及如何将数据存储到本地文件。

通过构建这个Todo应用,我们将能获得一些关于如何使用Rust构建命令行应用的实际经验,并了解如何设计一个小型应用的架构,从而帮助我们在未来的开发工作中更高效地运用Rust。

项目目标

1. 理解Rust的基础应用

这个项目的一个主要目标是帮助我们深入理解Rust语言的基础应用,具体包括:

  • 语法基础:Rust的基本语法(例如:变量声明、控制结构、函数定义等)。
  • 模块和结构体:如何将代码组织为不同模块,并定义结构体来表示数据。
  • 错误处理:如何处理可能出现的错误(例如:文件操作错误、JSON解析错误等),以及Rust的ResultOption类型的应用。
  • 集合类型:如何使用Rust的集合类型(例如:VecHashMap)来存储和操作数据。

通过实现这个简单的命令行Todo应用,我们能够在实际编程中应用这些基础概念,提升对Rust语言的理解和掌握。

2. 实现命令行交互

通过命令行进行任务管理是命令行应用的核心特性之一。我们将通过Rust中的clap库来实现命令行参数的解析。clap库使得我们能够轻松地定义命令行参数,并根据用户输入执行不同的操作。具体来说,我们的命令行应用将支持以下功能:

  • 添加任务:用户通过命令行输入新的任务。
  • 列出任务:列出所有当前的任务。
  • 标记任务为完成:标记某个任务为完成状态。
  • 删除任务:根据任务ID删除某个任务。

clap库将帮助我们设计清晰的命令行接口,使得用户可以方便地通过命令来执行这些功能。

3. 数据持久化

数据持久化是任何应用程序中都非常重要的一部分,尤其是在我们需要在不同的会话中保存用户数据时。我们将使用Rust的文件操作API来实现任务数据的持久化。具体来说:

  • 我们将使用serdeserde_json库将任务数据序列化为JSON格式,以便将其保存到本地文件中。
  • 每次启动应用时,我们将加载存储在文件中的任务数据;而每次执行增、删、改操作时,都会将修改后的任务数据写回到文件。
  • 这样,用户就可以在不同的启动会话之间保存任务列表,并随时查看和更新任务数据。

4. 增删改查功能

为了实现一个完整的Todo应用,我们将实现四个基本的功能:

  • 添加任务:用户通过命令行输入任务的内容,应用会将该任务添加到任务列表中,并保存到本地文件。
  • 列出任务:用户可以查看当前所有的任务,包括任务的ID、描述、完成状态等信息。
  • 标记任务为完成:用户可以通过任务ID将任务标记为完成,完成的任务会在输出中显示为“Done”状态。
  • 删除任务:用户可以通过任务ID删除某个任务,删除后的任务将从任务列表中移除并同步更新到本地文件。

每个功能都将对应一个命令行子命令(例如:addlistdonedelete),用户通过输入相应的命令来操作任务列表。

通过实现这些基本的CRUD功能,我们将能展示Rust在处理命令行交互、文件存储以及数据操作方面的能力,同时也为进一步扩展应用(例如:支持标签、优先级、到期日期等任务属性)打下基础。

项目设置

1. 初始化项目

创建一个新的Rust项目:

cargo new todo_cli
cd todo_cli

2. 添加依赖

为了便于处理命令行参数和文件IO操作,我们将添加以下依赖:

  • clap:一个用于解析命令行参数的库。
  • serdeserde_json:用于将任务序列化为JSON格式,以便存储在文件中。

Cargo.toml文件中添加以下内容:

[dependencies]
clap = "3.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

3. 设计Todo结构体

我们将定义一个Todo结构体来表示一个待办任务。每个任务将包含以下字段:

  • id: 任务的唯一标识符。
  • task: 任务的描述。
  • done: 标识任务是否完成。

src/main.rs文件中定义Todo结构体:

use serde::{Serialize, Deserialize};
​
#[derive(Serialize, Deserialize, Debug)]
struct Todo {
    id: u32,
    task: String,
    done: bool,
}
​
impl Todo {
    fn new(id: u32, task: String) -> Todo {
        Todo {
            id,
            task,
            done: false,
        }
    }
}

实现增、删、改、查功能

我们将为命令行应用实现以下功能:

  • 添加任务:添加一个新的待办任务。
  • 列出任务:查看所有待办任务及其状态。
  • 标记任务为完成:将指定任务标记为已完成。
  • 删除任务:删除指定的任务。

1. 保存和加载任务

为了持久化存储,我们将使用serde_json将任务序列化到文件中,方便之后的加载。

我们需要实现一个函数来加载任务:

use std::fs::File;
use std::io::{self, Read};
use std::path::Path;
​
fn load_todos() -> Vec<Todo> {
    if !Path::new("todos.json").exists() {
        return Vec::new();
    }
​
    let mut file = File::open("todos.json").expect("Unable to open file");
    let mut contents = String::new();
    file.read_to_string(&mut contents).expect("Unable to read file");
​
    serde_json::from_str(&contents).unwrap_or_else(|_| Vec::new())
}

接着,我们需要实现保存任务的函数:

use std::fs::OpenOptions;
use std::io::Write;
​
fn save_todos(todos: &Vec<Todo>) {
    let file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open("todos.json")
        .expect("Unable to open file");
​
    let json = serde_json::to_string(todos).expect("Unable to serialize tasks");
    file.write_all(json.as_bytes()).expect("Unable to write data");
}

2. 添加任务

我们可以通过命令行参数添加任务:

use clap::{App, Arg};
​
fn add_task(todos: &mut Vec<Todo>, task: String) {
    let id = todos.len() as u32 + 1; // 简单的ID生成方式
    let new_task = Todo::new(id, task);
    todos.push(new_task);
    save_todos(todos);
}

3. 列出任务

列出所有任务,并显示其完成状态:

fn list_tasks(todos: &Vec<Todo>) {
    for todo in todos {
        println!("{} - {} [{}]", todo.id, todo.task, if todo.done { "Done" } else { "Not Done" });
    }
}

4. 标记任务为完成

根据任务ID,标记任务为完成:

fn mark_done(todos: &mut Vec<Todo>, id: u32) {
    if let Some(task) = todos.iter_mut().find(|todo| todo.id == id) {
        task.done = true;
        save_todos(todos);
    } else {
        println!("Task not found");
    }
}

5. 删除任务

根据任务ID,删除指定任务:

fn delete_task(todos: &mut Vec<Todo>, id: u32) {
    if let Some(index) = todos.iter().position(|todo| todo.id == id) {
        todos.remove(index);
        save_todos(todos);
    } else {
        println!("Task not found");
    }
}

处理命令行参数

接下来,我们使用clap库来解析命令行参数,实现不同的命令。

fn main() {
    let matches = App::new("Todo CLI")
        .version("1.0")
        .author("Your Name <your.email@example.com>")
        .about("A simple command-line Todo application written in Rust")
        .subcommand(
            App::new("add")
                .about("Add a new task")
                .arg(Arg::new("task").about("The task description").required(true)),
        )
        .subcommand(App::new("list").about("List all tasks"))
        .subcommand(
            App::new("done")
                .about("Mark a task as done")
                .arg(Arg::new("id").about("The task ID").required(true)),
        )
        .subcommand(
            App::new("delete")
                .about("Delete a task")
                .arg(Arg::new("id").about("The task ID").required(true)),
        )
        .get_matches();
​
    let mut todos = load_todos();
​
    match matches.subcommand() {
        Some(("add", sub_matches)) => {
            let task = sub_matches.value_of("task").unwrap().to_string();
            add_task(&mut todos, task);
            println!("Task added");
        }
        Some(("list", _)) => {
            list_tasks(&todos);
        }
        Some(("done", sub_matches)) => {
            let id = sub_matches.value_of("id").unwrap().parse::<u32>().unwrap();
            mark_done(&mut todos, id);
            println!("Task marked as done");
        }
        Some(("delete", sub_matches)) => {
            let id = sub_matches.value_of("id").unwrap().parse::<u32>().unwrap();
            delete_task(&mut todos, id);
            println!("Task deleted");
        }
        _ => {}
    }
}

通过实现这个Rust命令行Todo应用,我们学到了如何在Rust中进行文件IO操作、如何使用命令行参数以及如何管理数据的增删改查操作。以下是该应用的关键点:

  • 使用clap解析命令行参数,执行不同的命令。
  • 使用serde_json将数据序列化为JSON格式,并存储到本地文件。
  • 实现了增、删、改、查功能,可以通过命令行交互管理Todo任务。