实战:用 Rust 从头实现一个 CLI 应用(2)
在本系列的第一部分中,我们创建了一个基本的 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"))
,然后我继续解开这些值并将它们存储在cmd
和msg
变量中。
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
,主要区别是我们需要再次用空格分割 msg
。split_once
在第一个空白处分割,这可以让 body 保持完整。
"update notes set body = (?1) where id = (?2)", [body, id]
此更新命令指定我们将设置body
列为我们从具有id
与id
指定匹配的任何行的输入中解析的内容。
(?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
参考资料
engram 的开源存储库: https://github.com/adamjberg/engram/tree/main/clients/cli/ego
我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。
坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio