實(shí)戰(zhàn):用 Rust 從頭實(shí)現(xiàn)一個 CLI 應(yīng)用(2)
在本系列的第一部分中,我們創(chuàng)建了一個基本的 Rust CLI 程序,它允許我們創(chuàng)建筆記并將它們保存在一個 sqlite 數(shù)據(jù)庫中。如果你還沒有讀過那篇文章,你應(yīng)該先看看,因?yàn)檫@是從那篇文章開始的。
本文將涵蓋 CRUD 的其余部分:讀取、更新和刪除。
本系列中描述的 Rust 應(yīng)用程序已登陸 engram 的開源存儲庫[1]。歡迎點(diǎn)個 Star。
01 讀
創(chuàng)建內(nèi)容后,應(yīng)該讀取它。在命令行應(yīng)用程序中,這里的選項更加有限,這使我們可以跳過討論各種圖形用戶界面 (GUI) 相關(guān)主題。但這并不一定使它變得簡單。
關(guān)于這在我們的應(yīng)用程序中如何工作有一些注意事項。即我們要如何觸發(fā)并向用戶顯示歷史記錄。
觸發(fā)器或命令
筆記應(yīng)用程序啟動后將繼續(xù)運(yùn)行,直到提交空字符串。為了查詢某種信息,我們需要一些功能,允許用戶區(qū)分簡單的筆記和更高級的命令。
為此,我們將實(shí)現(xiàn) “/ 命令”。這些已經(jīng)被 Slack 和 Notion 之類的東西所普及,并且應(yīng)該可以很好地滿足我們的需求。如果筆記以 “/” 開頭,則后面的文本將被解釋為命令。更特別的是為了閱讀筆記,我們將添加一個 “/list” 命令。為簡單起見,它將轉(zhuǎn)儲數(shù)據(jù)庫中的所有筆記。我們將在稍后的帖子中對此進(jìn)行講解,以改進(jìn)其工作方式。每一項新功能都比你最初意識到的要多得多,因此推遲講解某些事情可以防止你過早糾纏在尚不重要的細(xì)節(jié)上。
/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])?;
??}
}
每當(dāng)提交筆記時,我們都會檢查是否輸入了 “/list”。如果是,我們進(jìn)入該else if塊以打印出現(xiàn)有的筆記。
let?mut?stmt?=?conn.prepare("SELECT?id,?body?from?notes")?;
通過主函數(shù)開始時初始化的連接來預(yù)處理 SQL 語句。SELECT id, body from notes 指定我們要從 notes 表中返回 id 和 body 列。
let?mut?rows?=?stmt.query(rusqlite::params![])?;
然后在預(yù)處理語句發(fā)出 query 查詢。由于我們在查詢所有筆記,因此沒有參數(shù),這就是為什么我們使用 rusqlite::params![] 傳遞空參數(shù)的原因。
while?let?Some(row)?=?rows.next()??{
????let?id:?i32?=?row.get(0)?;
????let?body:?String?=?row.get(1)?;
????println!("{}?{}",?id,?body.to_string());
}
上面的代碼遍歷所有返回的行。每一行都是之前輸入到我們數(shù)據(jù)庫中的筆記。row.get(0) 和 row.get(1) 獲取關(guān)聯(lián)列索引的值。在我們的例子中,我們的查詢: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 位整數(shù),通過 String 識別 body 為一個字符串。
最后,我們將它們傳遞給println函數(shù),以便輸出到終端。
現(xiàn)在你可以運(yùn)行該應(yīng)用程序:cargo run,如果你發(fā)出 /list 命令,現(xiàn)在應(yīng)該看到你已提交的所有筆記回顯輸出。
02 刪除
我?guī)缀蹩偸窃趯?shí)現(xiàn)更新之前實(shí)現(xiàn)刪除功能。這是一個簡單的操作,因此很快就能搞定。但它也可以作為編輯項目的臨時方式。在我們的筆記示例中,如果我想編輯一個筆記,我可以先將其刪除,然后創(chuàng)建一個具有正確內(nèi)容的新筆記。如上所述,這在像這樣的小例子中似乎不太實(shí)用,但在更大的應(yīng)用程序中,編輯 GUI 可能非常復(fù)雜。
...
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命令所沒有的東西——一個附加參數(shù)。我們需要指定要刪除的筆記。考慮了一會兒,我決定通過/del 1刪除 id 為 1 的筆記。
為了區(qū)分“命令”和“參數(shù)”,我決定使用 split_once 方法。
let?cmd_split?=?trimmed_body.split_once("?");
該split_once方法根據(jù)傳遞的分隔符拆分字符串。在我們的示例中,“/del 1” 將返回為 Some(("/del", "1")),然后我繼續(xù)解開這些值并將它們存儲在cmd和msg變量中。
if?cmd_split?!=?None?{
此相等性檢查涵蓋沒有空格的情況。在這種情況下,split_once 方法返回 None 以表示“ ”分隔符不存在。
我還是 Rust 的新手,發(fā)現(xiàn)這有點(diǎn)笨拙。我懷疑可能有更好的方法來實(shí)現(xiàn),但現(xiàn)在它可以完成這項工作。我已經(jīng)多次學(xué)會不要糾結(jié)于小細(xì)節(jié),因?yàn)閮H此一項就可能導(dǎo)致 30 分鐘的 Rust 文檔陷入困境。如果你有任何建議,歡迎交流!
if?cmd?==?"/del"?{
????let?id?=?msg;
????conn.execute("delete?from?notes?where?id?=?(?1)",?[id])?;
}
我們現(xiàn)在檢查輸入文本的第一部分是否是 /del,如果是,我們知道可以從msg變量中獲取要刪除的 id 。
"delete?from?notes?where?id?=?(?1)",?[id]
這是 SQL 命令刪除 notes 表中的一個與指定 id 匹配的行。
你可以再次運(yùn)行cargo run,現(xiàn)在嘗試輸出 /del 1 刪除你創(chuàng)建的第一條消息。你可以通過運(yùn)行/list來確認(rèn)它是否有效,并且你不應(yīng)該無法看到索引為 1 的筆記。
03 更新
有幾個關(guān)于更新如何工作的選項。為了繼續(xù)保持簡單,我決定將編輯作為一個命令全部發(fā)出:/edit 1 the new body I want to have。與刪除類似,傳遞 id 來標(biāo)識要編輯的筆記。id 之后的所有內(nèi)容都將被視為新 body 以覆蓋現(xiàn)有 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,主要區(qū)別是我們需要再次用空格分割 msg。split_once在第一個空白處分割,這可以讓 body 保持完整。
"update?notes?set?body?=?(?1)?where?id?=?(?2)",?[body,?id]
此更新命令指定我們將設(shè)置body列為我們從具有id與id指定匹配的任何行的輸入中解析的內(nèi)容。
(?1)?和?(?2)
這些表示位置參數(shù)。我們之前所有的 SQL 語句都只有一個,但在里有兩個。(?1)將被提供的參數(shù)中的第一個條目替換,即 body,(?2) 將被id變量替換。
再啟動一次并嘗試編輯你現(xiàn)有的某條筆記。你可以用/list命令查看以前的,然后發(fā)出/edit命令,最后發(fā)出另一個/list命令來確認(rèn)筆記是否被正確修改。
04 安裝 Notes 應(yīng)用程序
cargo?install?--path?.
運(yùn)行上面的命令將編譯 rust 應(yīng)用程序并將其添加到你的 PATH 系統(tǒng)路徑中。如果你在開始時使用了該命令 cargo new notes,那么你現(xiàn)在應(yīng)該可以從終端訪問 notes 命令。如果你想更新可執(zhí)行文件的名稱,你可以修改Cargo.toml文件中的name屬性,用你自己喜歡的名字替換。
我一直在研究筆記應(yīng)用程序,一段時間叫engram,為了保持我的可執(zhí)行文件名稱簡短,我將其縮短為eg。現(xiàn)在,無論何時我在終端中都可以輸入eg并立即訪問我的筆記。
05 總結(jié)
如果你按照整個教程進(jìn)行了操作,你現(xiàn)在應(yīng)該可以從終端訪問一個用 Rust 編寫的筆記應(yīng)用程序。在下一篇文章中,我們將向應(yīng)用程序添加一些附加功能并開始組織代碼。
原文鏈接:https://devtails.xyz/how-to-build-a-note-taking-command-line-application-with-rust-part-2
參考資料
engram 的開源存儲庫: https://github.com/adamjberg/engram/tree/main/clients/cli/ego
我是 polarisxu,北大碩士畢業(yè),曾在 360 等知名互聯(lián)網(wǎng)公司工作,10多年技術(shù)研發(fā)與架構(gòu)經(jīng)驗(yàn)!2012 年接觸 Go 語言并創(chuàng)建了 Go 語言中文網(wǎng)!著有《Go語言編程之旅》、開源圖書《Go語言標(biāo)準(zhǔn)庫》等。
堅持輸出技術(shù)(包括 Go、Rust 等技術(shù))、職場心得和創(chuàng)業(yè)感悟!歡迎關(guān)注「polarisxu」一起成長!也歡迎加我微信好友交流:gopherstudio
