前言#
昨天我们学完了测试相关的
今天继续往下
一个I/O项目:构建一个命令行程序#
我们来写一个命令行程序,验收下之前学的内容。
要做一个搜索工具类似grep (globally search aregularexpression andprint)。
相信大家都或多或少接触过linux服务器,查日志的时候用这个grep再好不过了。
它接收两个参数,一个是需要匹配的字符串,另一个是文件路径。
然后输出匹配的信息。
涉及到的概念#
我们的程序将涉及到以下几个之前学过的概念:
实现步骤#
- 获取命令行参数
- 保存命令行参数
- 根据路径读文件
- 匹配文件内容
- 输出结果
创建项目#
我们来创建一个binary项目,就叫minigrep。
cd ../
cargo new minigrep
cd minigrep 从命令行获取数据#
我们之前有用到过,在实现一个猜数游戏的时候,从命令行获取用户输入的数字。
io::stdin().read_line()然而这玩意儿只能接受一个参数,不过如果分两次输入也不是不行。
但是没必要,rust的标准库中有比这更好康的:std::env::args。
这个args的function返回的是一个迭代器iterator,这个我们暂时还没学到。
目前关于迭代器我们只需要知道两点:
- 它返回一系列的值
- 它有个
collect的方法可以把这个迭代器转化成一个集合。
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
这里有一点需要注意,那就是args这个返回的内容并不一定是有效的,而当他输入的是违法的字符时,整个程序就会直接panic。
不过这个问题也是可以避免的,可以使用std::env::args_os,他可以接收非法字符,它返回的迭代器中的值的类型是OsString而非args的string。
不过我们这里不用它,因为没这个需求。我们只是做一个简单的demo。而且args_os的值会因为平台的不同而不同。
另外有一点你应该注意到了,那就是我们手动声明了args这个集合的类型,这是因为rust并不能推断出你想要的集合的类型。
存储输入的数据#
既然能获得数据了,那么我们就得有个变量来存储它们。
我们可以声明两个变量或者用元组,但这都不是最好的方案。
没错,相信你已经想到了,那就是struct。
use std::env;
#[derive(Debug)]
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new () -> Config {
let args: Vec<String> = env::args().collect();
let query = args[1].clone();
let file_path = args[2].clone();
Config {
query,
file_path
}
}
}
fn main() {
let config = Config::new();
dbg!(arg);
}
这里有个点就是我用了&String.clone()这个方法,直接copy了一份堆内存也就是深复制。
为什么这么做而不是用生命周期呢?
因为这样写的代码用不了生命周期,new方法中的args在new被调用完之后就已经失效了,而这个时候我们引用的数据随着struct还在main函数中,这个时候引用指向的地址就已经有数据问题了。
这个代码其实也能改成使用生命周期的方式
use std::env;
#[derive(Debug)]
struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
impl<'a> Config<'a> {
fn new (args: &'a Vec<String>) -> Config {
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
dbg!(config);
}
俩种都可以,看你自己。一方面是性能问题,一方面代码维护问题。
我个人更倾向于第一种写法,不过args获取应该放到main函数里,这样方便阅读。
现在我们的就能获取和保存这些数据了,接下来我们要实现查询了。
读取文件#
我们直接创建一个文件poem.txt
然后复制以下内容到文件里
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
接下来我们就可以调用读取的api了。
我们之前其实也用过读取文件的方法,File::open
use std::{env, fs::{File, read_to_string}, io::Read};
#[derive(Debug)]
struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
impl<'a> Config<'a> {
fn new (args: &'a Vec<String>) -> Config {
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let mut poem = String::new();
let mut res = File::open(&config.file_path).expect("read poem.txt error");
res.read_to_string(&mut poem).expect("file to string error");
println!("{poem}");
}
成功获取到了。
但是上面的代码有些冗杂,还记得我们在学可恢复错误的?操作符的时候最后用的一个方法吗?
直接fs::read_to_string()
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let poem = fs::read_to_string(&config.file_path).expect("read_file_error");
println!("{poem}");
}简洁明了。
这样我们的文件内容就获取成功了。
接下来该轮到匹配了。
匹配字符串#
poem变量目前是一个String,所以对于字符串来说,我们可以用split("\n")将它们切割成一行一行的。
use std::{env, fs};
#[derive(Debug)]
struct Config<'a> {
query: &'a String,
file_path: &'a String,
}
impl<'a> Config<'a> {
fn new (args: &'a Vec<String>) -> Config {
let query = &args[1];
let file_path = &args[2];
Config {
query,
file_path
}
}
}
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
let poem = fs::read_to_string(&config.file_path).expect("read_file_error");
let poem = poem.to_lowercase();
let mut res = vec![];
for line in poem.split("\n") {
if line.to_lowercase().contains(config.query.as_str()) {
res.push(line);
}
}
dbg!(res);
}
其中contains可以传入一个闭包,或者&str或者char。

当然,我们可以用lines替代split("\n")

实际上他俩差不多。
ok,这样我们就匹配完了,并且也最终输出到终端。
总结#
今天我们一步一步的实现了这个minigrep,但是它还有挺多问题,比如现在的代码全部都堆在main.rs中,这是不优雅的,另外我们也应该引入测试,这样可以提高代码的可维护性。
还有我们目前对错误的处理都是直接expect,这样不太友好。
明天我们来优化下代码组织,然后加上驱动测试以及对错误的处理。
参考#
- ^rust-building-a-command-line-program https://doc.rust-lang.org/book/ch12-00-an-io-project.html#an-io-project-building-a-command-line-program
- ^rust-code-organization https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html
- ^rust-collections https://doc.rust-lang.org/book/ch08-00-common-collections.html
- ^rust-error-handling https://doc.rust-lang.org/book/ch09-00-error-handling.html
- ^rust-trait-and-lifetime https://doc.rust-lang.org/book/ch10-00-generics.html
- ^rust-writing-tests https://doc.rust-lang.org/book/ch11-00-testing.html
- ^rust-accepting-command-line-arguments https://doc.rust-lang.org/book/ch12-01-accepting-command-line-arguments.html#accepting-command-line-arguments
- ^rust-saving-arguments https://doc.rust-lang.org/book/ch12-01-accepting-command-line-arguments.html#saving-the-argument-values-in-variables
- ^rust-reading-a-file https://doc.rust-lang.org/book/ch12-02-reading-a-file.html#reading-a-file
发布于 2022-12-22 21:11・IP 属地广东
