实战:用 Rust 从头实现一个 CLI 应用(2)

共 4777字,需浏览 10分钟

 ·

2022-02-20 05:20

本系列的第一部分中,我们创建了一个基本的 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




往期推荐


我是 polarisxu,北大硕士毕业,曾在 360 等知名互联网公司工作,10多年技术研发与架构经验!2012 年接触 Go 语言并创建了 Go 语言中文网!著有《Go语言编程之旅》、开源图书《Go语言标准库》等。


坚持输出技术(包括 Go、Rust 等技术)、职场心得和创业感悟!欢迎关注「polarisxu」一起成长!也欢迎加我微信好友交流:gopherstudio

浏览 44
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报