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
我们应该看到打印出的诗的内容!很好。我们的程序正在工作。
现在让我们重构并改进它。