Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

I/O 项目:构建命令行程序

这是一个有趣的章节!我们将同时使用你已经学过的很多知识,并展示一些常见的标准库功能。我们将通过构建一个与文件和命令行输入/输出交互的命令行工具来实践,以继续练习你现在熟悉的一些 X 语言概念。

我们将要构建的命令行工具是经典的命令行工具 grep 的一个版本,简称“全局正则表达式打印“。grep 最简单的用例是在文件中搜索包含指定字符串的行。为此,grep 接受一个文件名和一个字符串作为参数。然后它读取该文件,查找包含该字符串的行,并打印这些行。

在本章中,我们将逐步构建这个项目。我们将使用的关键概念包括:

  • 组织代码(使用你在第 5 章中学到的模块)
  • 使用向量和字符串(集合,第 6 章)
  • 处理错误(第 7 章)
  • 适当使用 trait 和生命周期(第 8 章)
  • 编写测试(第 11 章)

事不宜迟,让我们开始吧!

接受命令行参数

首先,我们将让我们的 minigrep 工具接受两个命令行参数:一个查询字符串和一个文件名。也就是说,我们希望能够使用 x run 运行我们的程序,并传入查询字符串和要搜索的文件,如下所示:

x run -- searchstring example-filename.txt

但现在,我们使用 x new 生成的程序不能接受任何参数。让我们确保我们的程序可以通过查看如何让 X 语言程序接受命令行参数来接受它们。

读取参数值

为了确保我们的 minigrep 能够接受传递给它的命令行参数值,我们需要使用 X 语言标准库中提供的 std::env::args 函数。这个函数返回传递给程序的命令行参数的迭代器。我们将在第 13 章全面讨论迭代器。对于我们的目的,我们不需要了解迭代器的所有细节,只需了解两个细节:

  • 迭代器产生一系列值
  • 我们可以在迭代器上调用 collect 方法将其变成一个向量

让我们创建一个程序,该程序只会打印出所有传递给它的命令行参数,如示例所示。首先,我们会将参数收集到一个向量中,然后打印该向量。

import std::env

function main() {
  let args: List<String> = env::args().collect()
  println!("{:?}", args)
}

注意,env::args 将产生的第一个值是二进制文件的名称。这与其他语言的行为匹配。让我们尝试使用 x run 首先运行这段代码,不传入任何参数:

x run

输出应该看起来像这样:

["target/debug/minigrep"]

第一个参数 "target/debug/minigrep" 是我们二进制文件的名称。这符合 C 族语言中参数列表的行为,让程序在执行时使用调用它的名称。如果需要,能够访问程序名称会很方便,这样我们就可以在消息中打印它,或者更改程序应有的行为,具体取决于命令行中调用程序时使用的别名。但就本章而言,我们将忽略第一个参数,只获取我们需要的两个参数。

现在让我们尝试使用两个参数运行程序:

x run -- needle haystack

注意我们在 x run 的参数和我们的程序参数之间添加了 --:这告诉 x 后面的参数是给我们的程序的,而不是给 x 的。输出应该看起来像这样:

["target/debug/minigrep", "needle", "haystack"]

太好了!程序正在接收它作为参数的查询和文件名。现在我们将第二个参数保存在变量中,第三个参数保存在另一个变量中。

将参数值保存到变量中

现在我们的程序能够访问作为命令行参数指定的值,让我们将这两个参数的值保存到变量中,以便我们可以在程序的其余部分使用这些值。我们将把查询参数称为 query,将文件名参数称为 filename,如示例所示。

import std::env

function main() {
  let args: List<String> = env::args().collect()

  let query = args[1].clone()
  let filename = args[2].clone()

  println!("搜索 {}", query)
  println("在文件 {} 中", filename)
}

我们再次使用 clone 在这里简化示例。我们可以直接使用引用,但这需要生命周期注解,我们还没有准备好深入研究。现在使用 clone 是一个可以接受的折衷方案,让我们可以继续编写这个程序的第一部分。

让我们尝试运行这段代码:

x run -- test sample.txt

输出应该看起来像这样:

搜索 test
在文件 sample.txt 中

太好了!程序正在工作;查询和文件名正在被保存到变量中。稍后我们会添加一些错误处理,以防有人在没有提供任何参数的情况下运行我们的程序,但现在我们将忽略这种情况,而是处理添加读取文件功能。

读取文件

现在我们要添加读取作为 filename 命令行参数指定的文件的功能。首先,我们需要一个示例文件来测试,这是使用 grep 时的最佳用例。我们将使用一个包含多行文本的文件,其中一些行包含“duct“。这就是示例。创建一个名为 poem.txt 的文件,其中包含 Emily Dickinson 的诗《我没告诉你的那个早晨》:

我没告诉你的那个早晨,
那最初的早晨,太阳升起来了——
从那之后,我数不清多少个日子,
但它们都不如那第一个日子重要。

夏天来得太早了,
我忘记了夏天也会离开。
我记得的唯一一件事,
是那第一个早晨,以及它如何照亮我。

很好,现在我们有了一个文件,让我们编辑 src/main.x 以添加读取文件的代码,如示例所示:

import std::env
import std::fs

function main() {
  // --snip--
  println!("搜索 {}", query)
  println!("在文件 {} 中", filename)

  let contents = fs::read_to_string(filename)
    .expect("读取文件出错了")

  println("带有文本的文件:\n{}", contents)
}

首先,我们添加了另一个 import 语句以从标准库引入 std::fs,它处理文件相关的事情。

main 中,我们添加了新行:fs::read_to_string 接受 filename,打开该文件,然后返回一个 Result<String>,其中包含该文件的内容。

在那之后,我们再次添加了一个临时的 println! 语句,该语句在读取文件后打印 contents 的值,以便我们可以检查程序是否正常工作。

让我们尝试运行这段代码,首先将任何字符串作为第一个命令行参数,将 poem.txt 作为第二个参数:

x run -- the poem.txt

我们应该看到打印出的诗的内容!很好。我们的程序正在工作。

现在让我们重构并改进它。