前言
本系列文章章将构建一个与文件和命令行输入/输出交互的命令行工具来练习现在一些你已经掌握的 Rust 技能。
Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,因此我们的项目将创建一个我们自己版本的经典命令行工具:grep
。grep
是 “G
lobally search a R
egular E
xpression and P
rint.” 的首字母缩写。grep
最简单的使用场景是在特定文件中搜索指定字符串。为此,grep
获取一个文件名和一个字符串作为参数,接着读取文件并找到其中包含字符串参数的行,然后打印出这些行。
一、任务目的
使用rust制作一个简化版本的grep工具,我们希望在使用我们的程序的时候是下面这样的
minigrep searchstring example-filename.txt
其中minigrep
是程序名字,searchstring
是要搜索的字符串,example-filename.txt
是被搜索的文件名。在开发过程中,不能每次编译都要去build目录去找这个程序然后运行,我们一般采用的运行方式是cargo run
,因此我们采用以下方式来运行上面这条命令,即使用cargo run‘
来代替原来的程序名字,按照运行规则,会自动把后面的参数传递给rust
cargo run searchstring example-filename.txt
传入一个要搜索的字符串searchstring
,和一个文件名example-filename.txt
,并且接收这两个参数。现在crates.io已经有很多现成的库,可以很简单的实现获取程序的参数,可以直接调用,但是我们这里正在学习这部分内容,因此这里我们自己手动实现。在后续的开发过程中,我们在优化程序时,也会将我们的代码替换成creats.io中的库来实现,用来提高程序的效率。
本期我们要实现的是读取传入的这两个参数,并且读取传入文件名的文件内容,输出到屏幕上。
我们需要实现的效果如下,在poem.txt
中写入文件内容,我们写入一首诗
中山孺子妾,特以色见珍。虽然不如延年妹,亦是当时绝世人。 桃李出深井,花艳惊上春。一贵复一贱,关天岂由身。 芙蓉老秋霜,团扇羞网尘。戚姬髡发入舂市,万古共悲辛。
运行以下命令
cargo run abc poem.txt
然后屏幕会输出在poem.txt
中写入的文件内容
中山孺子妾,特以色见珍。虽然不如延年妹,亦是当时绝世人。 桃李出深井,花艳惊上春。一贵复一贱,关天岂由身。 芙蓉老秋霜,团扇羞网尘。戚姬髡发入舂市,万古共悲辛。
这样就意味着我们取到了传递的命令行参数,并且通过io来读取到了文件的内容。
二、创建新项目
和以前一样,我们找个地方,打开 终端
运行以下新建项目的命令,
cargo new minigrep
然后使用vscode打开minigrep文件夹,里面就是cargo为我们创建好的基础项目。
三、读取参数值
Rust 标准库提供了一个函数std::env::args
,返回一个传递给程序的命令行参数的 迭代器
,迭代器
我们在后面会详细介绍,现在你只需记住两件事:
- 迭代器生成一系列的值
- 可以在迭代器上调用
collect
方法将其转换为一个集合
我们将传递给程序的命令行参数读取并收集到 vector
中,先导入标准库
use std::env;
然后在 main
中接收该参数,并且调用 collect
方法接收参数,并且存入变量 args
中
let args: Vec<String> = env::args().collect();
完整代码如下
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:#?}", args); }
接下来我们使用 cargo run
传入参数运行程序看看效果,
cargo run needle haystack
运行效果如下图所示,可以看到,接收到的参数一共有三个,
第一个参数是:二进制文件的名称,也是位置。后面的参数就是我们输入的参数,我们传入的参数一共有两个,但是这里给出了三个参数,这是没有问题的,传入的参数第一个是文件名,接下来才我们传入的参数
四、将参数保存至变量
由于我们将参数存入了 Vector
的变量 args
中,并且我们已知参数的数量和含义是什么,利用以前学过的知识,取到这两个参数
let query = &args[1]; let filename = &args[2];
完整代码
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:#?}", args); let query = &args[1]; let filename = &args[2]; println!("Searching for {}", query); println!("In file {}", filename); }
现在运行试试效果,如下图示
五、读取文件
1. 新建文本文件
在项目目录新建文本文件,如下图所示,实际情况任意,本次仅作为例子,
文件内容如下:
中山孺子妾,特以色见珍。虽然不如延年妹,亦是当时绝世人。 桃李出深井,花艳惊上春。一贵复一贱,关天岂由身。 芙蓉老秋霜,团扇羞网尘。戚姬髡发入舂市,万古共悲辛。
2. 导入文件操作函数
文件操作是在标准库中的std::fs
下提供的,使用之前我们需要将这些函数导入进来,代码如下:
use std::fs;
3. 读取文件内容
fs::read_to_string
是一个读取文件内容的函数,需要接收 filename
,返回 Result<String>
,其中包含文件内容。
在上一小节的代码基础之上,我们进行修改。上节代码
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:#?}", args); let query = &args[1]; let filename = &args[2]; println!("搜索 {}", query); println!("在文件 {}", filename); }
我们已经从命令行参数中读取到了 filename
,因此我们只需要将这个参数传递给 fs::read_to_string
就得以得到我们要的文件内容,拿到文件内容以后输出文件内容。
现在我们加入以下代码
let contents = fs::read_to_string(filename) .expect("读取文件出错"); println!("读取到的文本为:\n{}", contents);
完整代码:
use std::env; use std::fs; fn main() { let args: Vec<String> = env::args().collect(); println!("{:#?}", args); let query = &args[1]; let filename = &args[2]; println!("搜索 {}", query); println!("在文件 {}", filename); let contents = fs::read_to_string(filename) .expect("读取文件出错"); println!("读取到的文本为:\n{}", contents); }
4. 运行效果
现在我们在终端执行命令,
cargo run the poem.txt
效果如下,
总结
以上就是本节的所有内容。
通过本节学到了:
- 如何接收命令行参数
- 将命令行参数拆解开来
了解了怎么利用 Rust 来读取一个文件的内容,即利用标准库fs
来读取文件内容,仅需传入一个 filename
即可读出文件的内容。
作业
正常来说是可以读取到两个参数的,但是怎么确保我们真的能够读到两个参数,而且程序应该给予一定的提示,需要做一些错误的处理,请你对这里的问题进行处理。
你已学会了读取文件内容的操作。现在你可以考虑以下内容:
- 如何按行来读取文件内容
- 如何删除一个文件
- 如何新建文件
- 如何给文件追加内容
- 如何获取文件的详细信息(可选)