上篇文章讲完了项目的目标和准备流程,本文目标是实现grrs命令行软件的初始版本。
如果你还没有读过上文,欢迎查看。
也欢迎关注这个系列专栏,我将更新更多精彩内容。
1. 安装clap库
首先,我们在处理业务逻辑前,首先要解决命令行参数传入的问题。
换言之,我们最先要做的,就是正确接收用户传入的搜索模式和搜索文件参数。
在rust生态中,一个名为clap的库基本已成为实现命令行软件的事实标准。这个库集成了大量命令行软件必备的功能,可以帮助我们快速搭建起一个命令行应用原型。
首先,为我们的项目安装clap库:
cargo add clap --features derive
注意在安装过程中需要传入参数--features derive,其原因是clap需要使用派生宏来方便开发者开发。
如果你目前不知道什么是宏,没关系,这不影响我们的代码实践,完全可以等到对rust更熟悉之后再研究宏的知识。
你只需要注意,在安装clap时同时安装了派生宏特性即可。当你运行完上述命令后,请检查项目根目录下的Cargo.toml文件。
其中应该有:
clap = { version = "4.5.38", features = ["derive"] }
当然,可能到你看到这个教程时clap已经有了新版本。但是总之,当你看到类似上面的内容时,你就成功为你的项目引入了第一个依赖了!
2. 命令行参数传入
现在,我们开始使用clap来完成命令行参数传入的工作。
请你将./src/main.rs修改成如下内容:
use clap::Parser;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
path: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}
这些代码是什么意思呢?
use clap::Parser;
这第一行的use就是引入了clap库的中的Parser。这个Parser很快就在下文用到:
#[derive(Parser)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
path: std::path::PathBuf,
}
这里,我们构造了一个名为Cli的结构体,并对这个结构体使用了Parser派生宏。这个派生宏的作用,很快我们就会看到。
结构体中有两个字段,pattern和path。
pattern保存的是用户需要搜索的样式,使用String结构;而对于查询文件路径,我们使用了标准库中的std::path::PathBuf来保存,你可以理解为这是一种特殊的字符串。
这里的PathBuf,在实现上可以兼容不同操作系统的路径名风格(正斜杠、反斜杠),同时还可以和其他标准库 API 进行集成,总之就是很好用。
最终,我们在main函数中有:
let args = Cli::parse();
你可能会问,这就完了?
对,这就结束了,就是这么简单。我们所做的只是定义了一个结构体,剩下的由clap库中的Parser派生宏帮我们办完了。
现在,我们一起看看效果:
首先,我们尝试不传入任何参数,直接运行项目。
cargo run
结果是:

你看这个错误提示,是不是有模有样?注意,这些都是clap直接替我们生成的哦。
如果我们尝试传入一下参数
cargo run some-pattern some-path
结果是:

果然,运行结果 print 出了一行结果,这和我们刚刚代码中写的println!("pattern: {:?}, path: {:?}", args.pattern, args.path)预期相符。
到这里,我们就完成了传入命令行参数的准备工作。
其实,clap还有很多高级功能可以探索,可以帮助命令行软件实现丰富的功能。如果大家需要,欢迎评论区告诉我,我可能后续会在专栏更新更多clap库的进阶功能。
3. 实现grrs的第一个可用版本
现在将main.rs修改成如下状态:
use clap::Parser;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
path: std::path::PathBuf,
}
fn main() {
let args = Cli::parse();
let content = std::fs::read_to_string(&args.path).expect("could not read file");
for line in content.lines() {
if line.contains(&args.pattern) {
println!("{}", line);
}
}
}
很简单吧?我们只是稍微修改了main函数中的内容,加入了一些简单的业务逻辑。现在,让我们一起看看这段简单的代码效果如何。
此时,我们输入如下命令:
cargo run -- content ./src/main.rs
命令中的--用于隔开cargo自己的参数和我们创建的命令行软件的参数。
你应当看到我在第一篇文章开头,我们展示的结果:

如果我们尝试传入一个错误的参数,比如传入一个不存在的文件名:
cargo run -- content false-file-path
此时你应该看到:

说实话,这不是一个很完美的实现,错误处理这块儿还是有瑕疵的。但是起码现在这个代码看起来能用,还要什么自行车!
你可能还是很好奇,这段代码是如何实现这样的功能的,我们在main函数中添加的代码细节具体有何作用,为什么我说现在的错误处理不够好。
受篇幅限制,我无法将所有细节全部放在这篇文章中。这些问题,我们下回再详细解释吧!
希望这篇博客能帮助到你,也欢迎关注交流。愿与君共勉!