前言
在前面的几篇文章里我们简单的介绍如何使用Rust标准库和第三方库来编写命令行工具,在本篇文章中,我们将从零开始构建一个命令行 Todo 管理器,允许用户添加、查看、标记完成以及删除待办事项。本项目将使用 clap 作为命令行参数解析工具,并结合标准库完成核心逻辑。
项目目标
需求分析
1.管理任务列表
支持以下操作:
- 添加任务
- 删除任务
- 列出任务
- 标记任务为完成
2.支持持久化到文件
- 数据将保存到本地文件,以 JSON 格式存储,后续可以扩展为 TOML 或 YAML 格式。
3.美观的命令行界面
- 使用颜色和表格格式化输出,提高用户体验。
4.测试与优化
- 包括单元测试和集成测试,确保项目稳定性。
创建并初始化项目结构
使用cargo new创建一个项目,具体如下所示:
carog new cli_demo
在Cargo.toml文件中添加项目依赖,具体如下所示:
[package]
name = "cli_demo"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.5.23", features = ["derive"] }
colored = "2.2.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"
tabwriter = "1.4.0"
在src目录下分别创建ui.rs,storage.rs和todo.rs文件,项目目录结构如下所示:
? cli_demo git:(master) ? tree . -I target
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── main.rs
│ ├── storage.rs
│ ├── todo.rs
│ └── ui.rs
└── tasks.json
2 directories, 7 files
实现具体代码逻辑
打开并编辑todo.rs文件,这里是项目的核心逻辑,具体实现如下所示:
use serde::{Deserialize, Serialize};
use crate::storage;
use crate::ui;
#[derive(Debug, Deserialize, Serialize)]
pub struct Task {
pub id: usize,
pub description: String,
pub done: bool,
}
pub struct TodoManager {
pub tasks: Vec,
file_path: String,
}
impl TodoManager {
pub fn new(file_path: &str) -> Self {
let tasks = storage::load_tasks(file_path).unwrap_or_default();
Self {
tasks,
file_path: file_path.to_string(),
}
}
pub fn save(&self) {
storage::save_tasks(&self.file_path, &self.tasks);
}
pub fn add(&mut self, description: String) {
let id = self.tasks.len() + 1;
self.tasks.push(Task{
id,
description,
done: false,
});
self.save();
println!("Added task with id {}", id);
}
pub fn list_tasks(&self) {
ui::print_tasks(&self.tasks);
}
pub fn mark_task(&mut self, id: usize) {
if let Some(task) = self.tasks.iter_mut().find(|t| t.id == id) {
task.done = true;
self.save();
println!("Marking task");
} else {
println!("Task with id {} not found", id);
}
}
pub fn delete(&mut self, id: usize) {
if self.tasks.iter().any(|t| t.id == id) {
self.tasks.retain(|t| t.id != id);
self.save();
println!("Removed task");
} else {
println!("Task with id {} not found", id);
}
}
}
打开并编辑storage.rs文件,这里是项目实现持久化的地方,具体实现如下所示:
use crate::todo::Task;
use std::io::Error;
pub fn load_tasks(file_path: &str) -> Result, Error> {
let data = std::fs::read_to_string(file_path).unwrap_or_else(|_err| "[]".to_string());
let tasks: Vec = serde_json::from_str(&data).unwrap_or_default();
Ok(tasks)
}
pub fn save_tasks(file_path: &str, tasks: &Vec) {
let data = serde_json::to_string_pretty(tasks).expect("failed to serialize tasks");
std::fs::write(file_path, data).expect("failed to write data to file");
}
打开并编辑ui.rs文件,这里的逻辑主要负责美化输出,具体实现如下所示:
use std::io::Write;
use colored::Colorize;
use tabwriter::TabWriter;
use crate::todo::Task;
pub fn print_tasks(tasks: &[Task]) {
if tasks.is_empty() {
println!("{}", " No tasks selected".yellow());
return;
}
let mut tw = TabWriter::new(Vec::new());
writeln!(&mut tw, "ID\tDescription\tStatus").unwrap();
for task in tasks {
let status = if task.done {
"Done".green()
} else {
"Pending".red()
};
writeln!(&mut tw, "{}\t{}\t{}", task.id, task.description, status).unwrap();
}
tw.flush().unwrap();
println!("{}", String::from_utf8(tw.into_inner().unwrap()).unwrap());
}
打开并编辑main.rs文件,作为入口文件主要负责命令行参数的创建和主要逻辑,这里我选用clap宏特性来创建和解析命令行,具体实现如下所示:
mod todo;
mod ui;
mod storage;
#[derive(Debug, Parser)]
#[command(name="todo manager", version="0.0.1", about="manage your tasks effortlessly!")]
struct Cli {
#[command(subcommand)]
commands: Commands
}
#[derive(Debug, Subcommand)]
enum Commands {
Add {
#[arg(short, long)]
description: String,
},
List,
Done {
#[arg(short, long)]
id: usize
},
Delete {
#[arg(short, long)]
id: usize
},
}
fn main() {
let args = Cli::parse();
let mut manager = TodoManager::new("tasks.json");
match args.commands {
Commands::Add { description } => manager.add(description),
Commands::List => manager.list_tasks(),
Commands::Done { id } => manager.mark_task(id),
Commands::Delete { id } => manager.delete(id),
}
}
单元测试我这里做得比较简单,在 src/todo.rs 中添加如下代码:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let mut manager = TodoManager::new("test.json");
manager.add(String::from("test"));
assert_eq!(manager.tasks.len(), 1);
}
#[test]
fn test_mar_done() {
let mut manager = TodoManager::new("test.json");
manager.add(String::from("test"));
manager.mark_task(1);
assert!(manager.tasks[0].done);
}
}
编译并运行,结果如下所示:
? cli_demo git:(master) ? cargo run -- add --description "learn rust"
Compiling cli_demo v0.1.0 (/Users/Alen/Workspaces/Rust/cli_demo)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cli_demo add --description 'learn rust'`
Added task with id 1
? cli_demo git:(master) ? cargo run -- add --description "learn rust-lang"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s
Running `target/debug/cli_demo add --description 'learn rust-lang'`
Added task with id 2
? cli_demo git:(master) ? cargo run -- add --description "learn rust-lang-web"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/cli_demo add --description 'learn rust-lang-web'`
Added task with id 3
? cli_demo git:(master) ? cargo run -- list
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/cli_demo list`
ID Description Status
1 learn rust Pending
2 learn rust-lang Pending
3 learn rust-lang-web Pending
? cli_demo git:(master) ? cargo run -- done --id 1
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/cli_demo done --id 1`
Marking task
? cli_demo git:(master) ? cargo run -- list
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/cli_demo list`
ID Description Status
1 learn rust Done
2 learn rust-l Pending
3 learn rust-lang-web Pending
? cli_demo git:(master) ? cargo run -- delete --id 1
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/cli_demo delete --id 1`
Removed task
? cli_demo git:(master) ? cargo run -- list
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/cli_demo list`
ID Description Status
2 learn rust-lang Pending
3 learn rust-lang-web Pending
项目总结
学到的关键知识点
1.命令行工具开发
- 学会了使用 clap 库定义命令行参数和子命令,构建功能强大的 CLI 工具。
2.数据持久化
- 使用 serde 和 serde_json 库完成数据的序列化和反序列化,实现了任务的本地存储和加载。
3.美观的输出
- 通过 colored 和 tabwriter 库,学会了如何增强命令行工具的用户体验。
4.代码组织与模块化
- 理解了如何分离核心逻辑、存储逻辑和用户界面模块,提升代码的可维护性。
5.测试与优化
- 掌握了基础的单元测试,确保项目功能正常。
项目可扩展方向
1.支持多种数据格式
- 增加对 TOML、YAML 等格式的支持,让用户可以选择自己的数据存储方式。
2.支持多用户
- 增加多用户管理功能,为每个用户维护独立的任务列表。
3.任务分类与标签
- 为任务添加分类和标签功能,实现更高效的任务管理。
4.云同步功能
- 通过 API 与云服务集成,实现任务的实时同步。
5.多语言支持
- 提供国际化支持,让更多用户可以方便地使用工具。
总结
通过本章,我们完成了一个功能完整的 Todo 管理器,学会了以下技能:
- 使用 clap 构建复杂命令行工具。
- 使用 serde 和 serde_json 管理数据的序列化和反序列化。
- 使用标准库进行文件操作。
本项目为实际应用场景提供了一个良好的示例,今后可以扩展更多功能,如支持分类、搜索或网络同步等。希望通过这个系列的文章能够帮助到你吧,感谢收到,欢迎点赞,转发让更多的rust看到!!!
Tags:yaml 格式化