本章是一个综合性练习项目,重点回顾一下学习过的技巧并探究一些更多的标准库功能。我们将创建一个命令行工具,它会和文件进行交互,也会和标准的输入和输出设备进行交互,练习一些你已经掌握的Rust概念。
Rust是快速的、安全的,单纯的二进制文件输出,它对交叉平台的支持使得自己成为创建命令行工具的理想语言。对于我们这个项目,我们将创建自己的命令行查找工具grep(使用正则表达式进行全局搜索并显示结果)。这个示例最简化的方式是:在指定文件中搜索特定字符串。未实现此目的,grep必须要接受两个参数,一个是文件名称,一个是要搜索的字符串。之后读取文件的内容,找出包含该字符串的那些行,最后给用户显示出这些行。
按照此方式,我们将展示如何使用终端特性创建命令行工具,如同其他命令行工具一样的操作。我们读取环境变量的值,这些值定义了我们工具的一些行为方式。我们也将错误信息输出到标准错误控制流(stderr),而不是到标准输出(stdout)。我们也可以将成功的信息保存到一个文件中,同时也可以在屏幕上看到错误信息。
在Rust社区,爱德华.格兰特已经创建了功能完备,运行速度快的ripgrep。相比之下,我们的版本还是非常的简单,但是这个项目会让你理解如同ripgrep那样的真实项目的一些背景知识。
- 我们自己的grep项目中,你将会学习到:
- 代码的组织结构
- 向量和字符串的使用
- 如何处理错误
- 恰当的使用接口和生命周期
- 编写测试程序
本项目也会涉及到闭包、迭代器和接口对象。
14.1 接受命令行参数
首先使用cargo new创建一个新项目,我们这个项目名称叫lession14_001。
bash
$ cargo new lession14_001
Created binary (application) `lession14_001` project
$ cd lession14_001
接下来的首要任务是让该项目接收两个参数:文件名称和要查找的字符串。即:我们使用cargo run运行这个项目,两个横杠表示后面的参数是给我们的项目使用的,而不是给cargo的,一个是要搜索的字符串,一个是要搜索的文件,如下所示:
bash
$ cargo run -- searchstring example-filename.txt
由cargo new生成的项目还不能处理这些参数,但是标准库crates.io提供了一些功能帮我们处理这些参数,让我们自己来实现它吧。
14.1.1 获取参数值
标准库提供了std::env::args函数可以读取命令行参数。这个函数返回了一个命令行参数的迭代器,现在我们需要详细了解一个迭代器的使用方法:迭代器会产生一系列值,可以使用collect函数将其转化为集合,例如向量,它就包含了所有迭代器生成的所有元素。
下面的代码允许你读取任何命令行参数并将其转换为向量集合。
rust
use std::env;
fn main() {
let args:Vec<String> = env::args().collect();
dbg!(args);
}
首先我们使用use引入std::env模块到我们的作用域中,然后就可以使用args函数了;注意std::env::args函数是两级嵌套函数,如果有不止一层模块时,我们引入函数的父模块。这样做,我们可以很容易使用来自std::env中的函数。同时它比引入std::env::args更加清晰,因为调用args函数时,你会无法区分args函数是本模块的还是来自引入模块的(当本模块也有args函数时)。
14.1.2 args函数和无效的unicode
注意如果std::env::args中包含无效的Unicode编码的参数会导致panic,如果必须使用无效的Unicode编码时,请使用std::env::args_os函数。这个参数会返回OsString类型的值,而不是String的值。使用std::env::args是为了简化代码,因为每个平台的OsString都不同。和String相比,使用OsString会更加的复杂。
在主函数内的第一行,我们就调用了env::args函数,并立即使用collect将其转换为包含所有参数的向量。使用collect可以转换为多种类型的集合,但是变量args已经指定为向量字符串,collect会自动转化为向量字符串。虽然在Rust中很少需要声明变量类型,但是collect通常需要声明需要转换的类型,因为Rust还不能出你想要哪种类型。
最后,使用debug宏显示出向量中的所有的元素信息。我们先不带参数运行,接着带两个参数在运行此项目:
rust
cargo run
warning: `C:\Users\刘海涛\.cargo\config` is deprecated in favor of `config.toml`
|
= help: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target\debug\lession14_001.exe`
[src\main.rs:4:5] args = [
"target\\debug\\lession14_001.exe",
]
cargo run -- abc def gah
warning: `C:\Users\刘海涛\.cargo\config` is deprecated in favor of `config.toml`
|
= help: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target\debug\lession14_001.exe abc def gah`
[src\main.rs:4:5] args = [
"target\\debug\\lession14_001.exe",
"abc",
"def",
"gah",
]
注意向量的一个值是target\\debug\\lession14_001.exe,它是我们运行的程序的名称。这和C代码程序相同,让程序可以获取自己的名称。我们目前不需要这个名称,只需要获取之后的参数。
14.1.3 保存参数值到变量内
现在程序可以访问命令行中的这两个值,如今要做的是将值保存到变量中以便之后的程序中可以使用这两个值。
rust
use std::env;
fn main() {
let args:Vec<String> = env::args().collect();
//dbg!(args);
let query = &args[1];
let file_path = &args[2];
println!("要查找的字符串是{query}");
println!("在{file_path}文件中");
}
因为程序的名称占据了向量的第一位args0,因此我们从索引位1开始,第一位表示要搜索的字符串,保存到变量query中;第二位表示文件路径名,我们将其保存到变量file_path中。
我们临时将这两个变量值打印输出到屏幕上,来验证代码如期运行。再次运行代码并带有两个参数test和sample.txt:
rust
cargo run -- test sample.txt
warning: `C:\Users\刘海涛\.cargo\config` is deprecated in favor of `config.toml`
|
= help: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target\debug\lession14_001.exe test sample.txt`
要查找的字符串是test
在sample.txt文件中
很好,代码运行正常!我们保存到变量的值完全正常。后面我们会处理潜在的错误,例如:参数没有或缺失的情况。