我們每天都會在終端機或命令提示字元上使用git、npm或cargo之類的CLI工具。作為一個工程師我們經常和這些工具打交道。那麽,你有沒有想過可以自己打造一個CLI工具?
接下來,我們將使用Clap框架,從零開始完成一個實用的待辦事項管理器。
現在,我們從建立新的Rust專案開始:
cargo new todo-cli
cd todo-cli
接下來,添加一些依賴的套件。
在沒有要求特定版本的情況下,可以使用以下的方式添加:
cargo add clap --features derive
cargo add serde --features derive
cargo add serde_json
cargo add chrono --features serde
cargo add colored
cargo add dirs
當然也可以選擇手動修改Cargo.toml,它可以指定特定的版本的套件。
我們來簡單說明這些套件的作用
我們在src/main.rs中,添加關於待辦事項的資料格式的程式碼。
use chrono::{DateTime, Local}; // 處理日期時間和時區
use clap::{Parser, Subcommand}; // 解讀傳入參數和子命令
use colored::*; // 在畫面輸出有顏色的文字
use serde::{Deserialize, Serialize}; // 序列化和反序列化資料結構
use std::fs; // 管理檔案讀取儲存
use std::path::PathBuf; // 調整各個系統(MacOS,Linux,Windows)的路徑格式,如\跟/的差異
use dirs::home_dir; // 取得家目錄路徑
// 待辦事項的結構
// derive可以自動產生對應的Debug, Serialize, Deserialize, Clone的程式碼,不必自己實做
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Todo {
id: usize,
title: String,
description: Option<String>,
completed: bool,
created_at: DateTime<Local>,
completed_at: Option<DateTime<Local>>,
priority: Priority,
}
// 待辦事項的優先度
#[derive(Debug, Serialize, Deserialize, Clone)]
enum Priority {
Low,
Medium,
High,
}
// 實做待辦事項的方法
impl Todo {
// 新增待辦事項
fn new(id: usize, title: String, description: Option<String>, priority: Priority) -> Self {
Self {
id,
title,
description,
completed: false,
created_at: Local::now(),
completed_at: None,
priority,
}
}
// 將待辦事項設定為完成的狀態
fn complete(&mut self) {
self.completed = true;
self.completed_at = Some(Local::now());
}
}
我相信大家可能有些疑惑,為什麼要這樣設計,我來解釋一下想法:
接下來就是最重要的部分,使用Clap框架讓我們的程式可以透過命令提示字元與使用者互動,同樣在src/main.rs中,添加底下的程式碼:
#[derive(Parser)]
#[command(name = "todo-list")]
#[command(author = "96gen")]
#[command(version = "1.0")]
#[command(about = "待辦事項管理器", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// 新增待辦事項,與傳入參數
Add {
/// 標題
title: String,
/// 描述(可留空)
#[arg(short, long)]
description: Option<String>,
/// 優先度,預設是中
#[arg(short, long, default_value = "medium")]
priority: String,
},
/// 列出所有待辦事項
List {
/// 只顯示未完成的待辦事項
#[arg(short, long)]
pending: bool,
/// 只顯示已完成的待辦事項
#[arg(short, long)]
completed: bool,
},
/// 標記待辦事項為完成
Complete {
/// 待辦事項id
id: usize,
},
/// 刪除待辦事項
Delete {
/// 待辦事項id
id: usize,
},
/// 清除已完成的待辦事項
Clean,
}
需要特別注意是三條的///,不是一般的註解//,這些內容將顯示在Clap框架自動產生的help中。
我們剛才只是定義的Clap的界面,並沒有完成對應的實作,在src/main.rs繼續完成它們:
struct TodoController {
todos: Vec<Todo>,
storage_path: PathBuf,
}
impl TodoController {
// 初始化待辦事項管理器
fn new() -> Self {
// 設定儲存的路徑,待辦事項儲存在家目錄下的.todo-cli/todos.json
let storage_path = dirs::home_dir()
.unwrap()
.join(".todo-cli")
.join("todos.json");
// 檢查目錄是否存在,不存在就建立
if let Some(parent) = storage_path.parent() {
fs::create_dir_all(parent).ok();
}
// 從todos.json檔案載入之前儲存的待辦事項
let todos = Self::load_todos(&storage_path);
// 回傳初始化完成的TodoController
Self {
todos,
storage_path,
}
}
fn load_todos(path: &PathBuf) -> Vec<Todo> {
// 如果檔案存在,就解析後回傳
if path.exists() {
let data = fs::read_to_string(path).unwrap_or_default();
serde_json::from_str(&data).unwrap_or_default()
} else {
Vec::new() // 不存在todos.json,回傳空Vec
}
}
// 將待辦事項儲存到檔案中
fn save_todos(&self) -> Result<(), Box<dyn std::error::Error>> {
let data = serde_json::to_string_pretty(&self.todos)?; // 轉換成易讀性高的JSON
fs::write(&self.storage_path, data)?;// 寫入檔案中
Ok(())
}
// 新增待辦事項
fn add_todo(&mut self, title: String, description: Option<String>, priority: Priority) {
let id = self.todos.len() + 1; // 用自增id
let todo = Todo::new(id, title, description, priority);
println!("{} 新增待辦事項: {}", "V".green().bold(), todo.title.cyan());
self.todos.push(todo); // 將剛新增的內容,放入待辦事項管理器中
self.save_todos().ok(); // 儲存新增的內容
}
// 顯示待辦事項
fn list_todos(&self, pending_only: bool, completed_only: bool) {
// 根據傳入的參數,選擇只顯示 未完成/已完成,未指定時顯示全部的待辦事項
let filtered_todos: Vec<&Todo> = self.todos
.iter()
.filter(|t| {
if pending_only { !t.completed }
else if completed_only { t.completed }
else { true }
})
.collect();
if filtered_todos.is_empty() {
println!("{}", "沒有找到待辦事項".yellow());
return;
}
println!("\n{}", "=== 待辦事項 ===".bold().blue());
for todo in filtered_todos {
// 完成的顯示V,未完成則是X
let status = if todo.completed {
"V".green()
} else {
"X".red()
};
// 將優先度轉換成清楚的標識
let priority_str = match todo.priority {
Priority::High => "高".red().bold(),
Priority::Medium => "中".yellow(),
Priority::Low => "低".white(),
};
// 顯示待辦事項的狀態、id、標題、優先度、建立日期
println!(
"{} [{}] {} [{}] {}",
status,
todo.id.to_string().dimmed(),
todo.title.bold(),
priority_str,
todo.created_at.format("%Y-%m-%d").to_string().dimmed()
);
// 如果有待辦事項描述就顯示
if let Some(desc) = &todo.description {
println!(" {}", desc.italic());
}
}
}
// 將指定id的待辦事項設定為完成
fn complete_todo(&mut self, id: usize) -> Result<(), String> {
match self.todos.iter_mut().find(|t| t.id == id) {
Some(todo) => {
if todo.completed {
return Err(format!("待辦事項 {} 已經完成", id));
}
todo.complete();
println!("{} 完成待辦事項: {}", "V".green().bold(), todo.title.cyan());
self.save_todos().ok();
Ok(())
}
None => Err(format!("找不到id為 {} 的待辦事項", id))
}
}
// 刪除指定id的待辦事項
fn delete_todo(&mut self, id: usize) -> Result<(), String> {
if let Some(pos) = self.todos.iter().position(|t| t.id == id) {
let todo = self.todos.remove(pos);
println!("{} 刪除待辦事項: {}", "X".red().bold(), todo.title);
// 重新編號後面的待辦事項,保持id編號連續
for (index, todo) in self.todos.iter_mut().enumerate() {
todo.id = index + 1;
}
self.save_todos().ok();
Ok(())
} else {
Err(format!("找不到id為 {} 的待辦事項", id))
}
}
// 清理已完成的待辦事項
fn clean_completed(&mut self) {
let completed_count = self.todos.iter().filter(|t| t.completed).count();
self.todos.retain(|t| !t.completed);
// 重新編號
for (idx, todo) in self.todos.iter_mut().enumerate() {
todo.id = idx + 1;
}
println!("{} 清除了 {} 個已完成的待辦事項", "V".green().bold(), completed_count);
self.save_todos().ok();
}
}
我們的程式快要完成了,只要把之前寫的程式連接起來,並加入CLI處理邏輯的程式碼即可。
fn main() {
// 設定windows系統顯示顏色,而不是控制碼
#[cfg(windows)]
let _ = colored::control::set_virtual_terminal(true);
// MacOS/Linux用戶不需要添加以上兩行程式碼
let cli = Cli::parse();
let mut manager = TodoController::new();
match cli.command {
Commands::Add { title, description, priority } => {
let priority = match priority.to_lowercase().as_str() {
"high" | "h" => Priority::High,
"low" | "l" => Priority::Low,
_ => Priority::Medium,
};
manager.add_todo(title, description, priority);
}
Commands::List { pending, completed } => {
manager.list_todos(pending, completed);
}
Commands::Complete { id } => {
if let Err(e) = manager.complete_todo(id) {
eprintln!("{} {}", "錯誤:".red().bold(), e);
}
}
Commands::Delete { id } => {
if let Err(e) = manager.delete_todo(id) {
eprintln!("{} {}", "錯誤:".red().bold(), e);
}
}
Commands::Clean => {
manager.clean_completed();
}
}
}
在編寫結束後,我們來編譯程式,讓我們實際的使用它。
# 在專案目錄下執行,來編譯專案
cargo build --release
# 可跳過,執行後可以在任何地方執行todo-cli,否則我們要在target\release下使用todo-cli
cargo install --path .
# 顯示說明,這是我們在使用Clap完成CLI界面章節設定的內容
todo-cli help
# 查詢副指令add的使用方式與參數
todo-cli help add
# 新增待辦事項,-d添加描述,-p設定優先度,high高,medium中,low是低
todo-cli add "學習Rust" -d "完成待辦事項管理器" -p high
todo-cli add "寫文章" -p medium
todo-cli add "看Youtube" -p low
# 無參數,顯示全部的待辦事項
todo-cli list
# 標示待辦事項完成
todo-cli complete 1
# 帶上pending參數,顯示未完成的待辦事項
todo-cli list --pending
# 清除已完成的待辦事項
todo-cli clean
# 刪除id是1的待辦事項
todo-cli delete 1
我們完成了待辦事項管理器,希望這個教學能大家能夠更加熟悉如何使用Rust。