實戰:用 Rust 從頭實現一個 CLI 應用(2)

語言: CN / TW / HK

關注「 Rust程式設計指北 」,一起學習 Rust,給未來投資

大家好,我是螃蟹哥。

在本系列的第一部分中,我們建立了一個基本的 Rust CLI 程式,它允許我們 建立 筆記並將它們儲存在一個 sqlite 資料庫中。如果你還沒有讀過那篇文章,你應該先看看,因為這是從那篇文章開始的。

本文將涵蓋 CRUD 的其餘部分: 讀取更新刪除

本系列中描述的 Rust 應用程式已登陸 engram 的開源儲存庫 [1] 。歡迎點個 Star。

01 讀

建立內容後,應該讀取它。在命令列應用程式中,這裡的選項更加有限,這使我們可以跳過討論各種圖形使用者介面 (GUI) 相關主題。但這並不一定使它變得簡單。

關於這在我們的應用程式中如何工作有一些注意事項。即我們要如何觸發並向用戶顯示歷史記錄。

觸發器或命令

筆記應用程式啟動後將繼續執行,直到提交空字串。為了查詢某種資訊,我們需要一些功能,允許使用者區分簡單的筆記和更高階的命令。

為此,我們將實現 “/ 命令”。這些已經被 Slack 和 Notion 之類的東西所普及,並且應該可以很好地滿足我們的需求。如果筆記以 “/” 開頭,則後面的文字將被解釋為命令。更特別的是為了閱讀筆記,我們將新增一個 “/list” 命令。為簡單起見,它將轉儲資料庫中的所有筆記。我們將在稍後的帖子中對此進行講解,以改進其工作方式。每一項新功能都比你最初意識到的要多得多,因此推遲講解某些事情可以防止你過早糾纏在尚不重要的細節上。

/list

let mut running = true;
while running == true {
  let mut buffer = String::new();
  io::stdin().read_line(&mut buffer)?;

  let trimmed_body = buffer.trim();
  if trimmed_body == "" {
    running = false;
  } else if trimmed_body == "/list" {
    let mut stmt = conn.prepare("SELECT id, body from notes")?;
    let mut rows = stmt.query(rusqlite::params![])?;
    while let Some(row) = rows.next()? {
      let id: i32 = row.get(0)?;
      let body: String = row.get(1)?;
      println!("{} {}", id, body.to_string());
    }
  } else {
   conn.execute("INSERT INTO notes (body) values (?1)", [trimmed_body])?;
  }
}

每當提交筆記時,我們都會檢查是否輸入了 “/list”。如果是,我們進入該 else if 塊以打印出現有的筆記。

let mut stmt = conn.prepare("SELECT id, body from notes")?;

通過主函式開始時初始化的連線來預處理 SQL 語句。 SELECT id, body from notes 指定我們要從 notes 表中返回 id 和 body 列。

let mut rows = stmt.query(rusqlite::params![])?;

然後在預處理語句發出 query 查詢。由於我們在查詢所有筆記,因此沒有引數,這就是為什麼我們使用 rusqlite::params![] 傳遞空引數的原因。

while let Some(row) = rows.next()? {
    let id: i32 = row.get(0)?;
    let body: String = row.get(1)?;
    println!("{} {}", id, body.to_string());
}

上面的程式碼遍歷所有返回的行。每一行都是之前輸入到我們資料庫中的筆記。 row.get(0)row.get(1) 獲取關聯列索引的值。在我們的例子中,我們的查詢: SELECT id, body from notes ,這意味著 id 將在索引 0 處,body 將在索引 1 處。

Rust 無法推斷這些屬性的型別,這就是為什麼它們必須指定 let id: i32 = row.get(0)?;let body: String = row.get(1)?; 的原因。 i32 識別 ID 為一個 32 位整數,通過 String 識別 body 為一個字串。

最後,我們將它們傳遞給 println 函式,以便輸出到終端。

現在你可以執行該應用程式: cargo run ,如果你發出 /list 命令,現在應該看到你已提交的所有筆記回顯輸出。

02 刪除

我幾乎總是在實現更新之前實現刪除功能。這是一個簡單的操作,因此很快就能搞定。但它也可以作為編輯專案的臨時方式。在我們的筆記示例中,如果我想編輯一個筆記,我可以先將其刪除,然後建立一個具有正確內容的新筆記。如上所述,這在像這樣的小例子中似乎不太實用,但在更大的應用程式中,編輯 GUI 可能非常複雜。

...
let trimmed_body = buffer.trim();
let cmd_split = trimmed_body.split_once(" ");

let mut cmd = trimmed_body;
let mut msg = "";
if cmd_split != None {
    cmd = cmd_split.unwrap().0;
    msg = cmd_split.unwrap().1;
}

if cmd == "/del" {
    let id = msg;
    conn.execute("delete from notes where id = (?1)", [id])?;
}
...

/del 命令具有 /list 命令所沒有的東西——一個附加引數。我們需要指定要刪除的筆記。考慮了一會兒,我決定通過 /del 1 刪除 id 為 1 的筆記。

為了區分“命令”和“引數”,我決定使用 split_once 方法。

let cmd_split = trimmed_body.split_once(" ");

split_once 方法根據傳遞的分隔符拆分字串。在我們的示例中,“/del 1” 將返回為 Some(("/del", "1")) ,然後我繼續解開這些值並將它們儲存在 cmdmsg 變數中。

if cmd_split != None {

此相等性檢查涵蓋沒有空格的情況。在這種情況下, split_once 方法返回 None 以表示“ ”分隔符不存在。

我還是 Rust 的新手,發現這有點笨拙。我懷疑可能有更好的方法來實現,但現在它可以完成這項工作。我已經多次學會不要糾結於小細節,因為僅此一項就可能導致 30 分鐘的 Rust 文件陷入困境。如果你有任何建議,歡迎交流!

if cmd == "/del" {
    let id = msg;
    conn.execute("delete from notes where id = (?1)", [id])?;
}

我們現在檢查輸入文字的第一部分是否是 /del ,如果是,我們知道可以從 msg 變數中獲取要刪除的 id 。

"delete from notes where id = (?1)", [id]

這是 SQL 命令刪除 notes 表中的一個與指定 id 匹配的行。

你可以再次執行 cargo run ,現在嘗試輸出 /del 1 刪除你建立的第一條訊息。你可以通過執行 /list 來確認它是否有效,並且你不應該無法看到索引為 1 的筆記。

03 更新

有幾個關於更新如何工作的選項。為了繼續保持簡單,我決定將編輯作為一個命令全部發出: /edit 1 the new body I want to have 。與刪除類似,傳遞 id 來標識要編輯的筆記。 id 之後的所有內容都將被視為新 body 以覆蓋現有 body。

else if cmd == "/edit" {
    let msg_split = msg.split_once(" ").unwrap();
    let id = msg_split.0;
    let body = msg_split.1;

    conn.execute("update notes set body = (?1) where id = (?2)", [body, id])?;
}

/edit 命令的開頭類似於 /del ,主要區別是我們需要再次用空格分割 msgsplit_once 在第一個空白處分割,這可以讓 body 保持完整。

"update notes set body = (?1) where id = (?2)", [body, id]

此更新命令指定我們將設定 body 列為我們從具有 idid 指定匹配的任何行的輸入中解析的內容。

(?1) 和 (?2)

這些表示位置引數。我們之前所有的 SQL 語句都只有一個,但在裡有兩個。 (?1) 將被提供的引數中的第一個條目替換,即 body, (?2) 將被 id 變數替換。

再啟動一次並嘗試編輯你現有的某條筆記。你可以用 /list 命令檢視以前的,然後發出 /edit 命令,最後發出另一個 /list 命令來確認筆記是否被正確修改。

04 安裝 Notes 應用程式

cargo install --path .

執行上面的命令將編譯 rust 應用程式並將其新增到你的 PATH 系統路徑中。如果你在開始時使用了該命令 cargo new notes ,那麼你現在應該可以從終端訪問 notes 命令。如果你想更新可執行檔案的名稱,你可以修改 Cargo.toml 檔案中的 name 屬性,用你自己喜歡的名字替換。

我一直在研究筆記應用程式,一段時間叫 engram ,為了保持我的可執行檔名稱簡短,我將其縮短為 eg 。現在,無論何時我在終端中都可以輸入 eg 並立即訪問我的筆記。

05 總結

如果你按照整個教程進行了操作,你現在應該可以從終端訪問一個用 Rust 編寫的筆記應用程式。在下一篇文章中,我們將嚮應用程式新增一些附加功能並開始組織程式碼。

原文連結:https://devtails.xyz/how-to-build-a-note-taking-command-line-application-with-rust-part-2

參考資料

[1]

engram 的開源儲存庫: https://github.com/adamjberg/engram/tree/main/clients/cli/ego

推薦閱讀

覺得不錯,點個贊吧

掃碼關注「 Rust程式設計指北