上篇文章讲完了项目的目标和准备流程,本文目标是实现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派生宏。这个派生宏的作用,很快我们就会看到。

结构体中有两个字段,patternpath

pattern保存的是用户需要搜索的样式,使用String结构;而对于查询文件路径,我们使用了标准库中的std::path::PathBuf来保存,你可以理解为这是一种特殊的字符串。

这里的PathBuf,在实现上可以兼容不同操作系统的路径名风格(正斜杠、反斜杠),同时还可以和其他标准库 API 进行集成,总之就是很好用。

最终,我们在main函数中有:

    let args = Cli::parse();

你可能会问,这就完了?

对,这就结束了,就是这么简单。我们所做的只是定义了一个结构体,剩下的由clap库中的Parser派生宏帮我们办完了。

现在,我们一起看看效果:

首先,我们尝试不传入任何参数,直接运行项目。

cargo run

结果是:

引入clap后直接运行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函数中添加的代码细节具体有何作用,为什么我说现在的错误处理不够好。

受篇幅限制,我无法将所有细节全部放在这篇文章中。这些问题,我们下回再详细解释吧!


希望这篇博客能帮助到你,也欢迎关注交流。愿与君共勉!

点击阅读下一篇文章