重构minigrep-Rust

综合所学基础构建一个简易的grep程序

grep程序可以过滤文件中的特定字符串并且打印相关行

接受命令行参数

新建项目

1
cargo new minigrep

minigrep需要接收两个参数,关键字和文件路径

个 Rust 标准库提供的函数 std::env::args用于获取命令行参数,该函数返回一个迭代器

我们目前只需要知道迭代器生成一系列的值,可以在迭代器上调用 collect 方法将其转换为一个集合,比如包含所有迭代器产生元素的vector。

1
2
3
4
5
6
7
use std::env;

fn main() {
let args:Vec<String>=env::args().collect();
dbg!(args);
}

image-20250106125922960

根据截图可知想要查询的字符串为迭代器的第二个元素,想要查找的文件路径在迭代器的第三个元素,为两个元素赋变量名

image-20250106130507334

读取文件

在项目根目录创建一个文件poem.txt

1
2
3
4
5
6
7
8
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!

尝试读取文件打印出来,由于未能实现搜索功能,第一个参数随便填

image-20250106131233927

改进模块与错误处理

1、main函数功能过多

2、query和file_name随着程序变大自身的含义将会更加难以理解

3、文件打开失败我们使用expect处理信息有限,文件打不开有多种原因,权限,文件名错误等等

4、将错误处理集中放到一处方便修改

模块化

将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。

main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。

我们将解析参数的功能提取到一个 main 将会调用的函数中,为将命令行解析逻辑移动到 src/lib.rs 中做准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::env;
use std::fs;

fn main() {
let args:Vec<String>=env::args().collect();

let (query,file_path)=parse_config(&args);

println!("查找的字符串为{query}");
println!("查找的文件名为{file_path}");

let contents=fs::read_to_string(file_path).expect("不能打开文件");

println!("{contents}");


}

fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}

现在函数返回一个元组,不过立刻又将元组拆成了独立的部分

目前除了将这两个值组合进元组之外并没有表达这个数据结构的意义:我们可以将这两个值放入一个结构体并给每个字段一个有意义的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
use std::env;
use std::fs;

fn main() {
let args:Vec<String>=env::args().collect();

let config=parse_config(&args);

println!("查找的字符串为{}",config.query);
println!("查找的文件名为{}",config.file_path);

let contents=fs::read_to_string(config.file_path).expect("不能打开文件");

println!("{contents}");


}

struct Config{
query: String,
file_path: String,
}

fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config{query, file_path}
}

新定义的结构体 Config 中包含字段 query 和 file_path。 parse_config 的签名表明它现在返回一个 Config 值。

现在 parse_config 函数的目的是创建一个 Config 实例,我们可以将 parse_config 从一个普通函数变为一个叫做 new 的与结构体关联的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use std::env;
use std::fs;

fn main() {
let args:Vec<String>=env::args().collect();

let config=Config::new(&args);

println!("查找的字符串为{}",config.query);
println!("查找的文件名为{}",config.file_path);

let contents=fs::read_to_string(config.file_path).expect("不能打开文件");

println!("{contents}");


}

struct Config{
query: String,
file_path: String,
}


impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}

修复错误处理

尝试不输入任何参数程序会panic,并且提示信息面向的是程序员而不是给用户去看

image-20250106133553527

在 new 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,程序会打印一个更好的错误信息并 panic

image-20250106133742255

但是报错仍然有一些额外信息打扰,我们希望去除它,针对于程序运行问题出错我们倾向于使用panic结束程序,但是参数出错属于用户的使用错误,这种情况下我们可以返回一个枚举让程序按照我们预定的意思继续运行,而不要直接panic

我们可以选择返回一个 Result 值,它在成功时会包含一个 Config 的实例,而在错误时会描述问题。

我们还将把函数名从 new 改为 build,因为许多程序员希望 new 函数永远不会失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use std::env;
use std::error::Error;
use std::fs;
use std::process;

fn main() {
let args:Vec<String>=env::args().collect();

let config = Config::build(&args).unwrap_or_else(|err| {//unwrap_or_else定义在result枚举上,如果返回ok会正常返回相关数值,如果返回err会调用闭包,相当于匿名函数
println!("Problem parsing arguments: {err}");
process::exit(1);//这种方式显示的报错只有一行,然后程序正常退出
});

if let Err(e)= run(config){
println!("程序错误{}",e);
process::exit(1);
};//函数调用的时候必须添加错误处理,由于run函数不返回任何数值,所以这块处理不需要用unwrap_or_else


}

fn run(config:Config)->Result<(),Box<dyn Error>>{
let contents=fs::read_to_string(config.file_path)?;//错误传递给函数调用者

println!("{contents}");
Ok(())
}

struct Config{
query: String,
file_path: String,
}


impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("缺少必须的参数");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}

逻辑迁移

main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::env;
use minigrep::Config;
use std::process;

fn main() {
let args:Vec<String>=env::args().collect();

let config = Config::build(&args).unwrap_or_else(|err| {//unwrap_or_else定义在result枚举上,如果返回ok会正常返回相关数值,如果返回err会调用闭包,相当于匿名函数
println!("Problem parsing arguments: {err}");
process::exit(1);//这种方式显示的抱错只有一行,然后程序正常退出
});

if let Err(e)= minigrep::run(config){
println!("程序错误{}",e);
process::exit(1);
};//函数调用的时候必须添加错误处理,由于run函数不返回任何数值,所以这块处理不需要用unwrap_or_else


}

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::error::Error;
use std::fs;


pub fn run(config:Config)->Result<(),Box<dyn Error>>{
let contents=fs::read_to_string(config.file_path)?;//错误传递给函数调用者

println!("{contents}");
Ok(())
}

pub struct Config{
pub query: String,
pub file_path: String,
}


impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("缺少必须的参数");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}

编写测试代码

编写一个会失败的测试,确保按照预期错误

修改测试让它通过

重构刚刚的代码确保测试始终通过

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
use std::error::Error;
use std::fs;


pub fn run(config:Config)->Result<(),Box<dyn Error>>{
let contents=fs::read_to_string(config.file_path)?;//错误传递给函数调用者
for line in search(&config.query,&contents){
println!("{}",line);
}

Ok(())
}

pub struct Config{
pub query: String,
pub file_path: String,
}


impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("缺少必须的参数");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}

pub fn search<'a>(query:&str,contents:&'a str)->Vec<&'a str>{//检查contents里面是否有query这个字符串,有的话返回相关行
let mut results = Vec::new();

for line in contents.lines(){
if line.contains(query) {
results.push(line);
}
}
results
}

#[cfg(test)]
mod tests{
use super::*;
#[test]
fn one_result(){
let query = "duct";
let contents ="\
Rust:
safe,fast,productive.
Pick three";

assert_eq!(vec!["safe,fast,productive."],
search(query,contents))
}
}

image-20250106182507336

使用环境变量

搜索字符串的大小写我们需要可以自主控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
use std::error::Error;
use std::fs;
use std::env;


pub fn run(config:Config)->Result<(),Box<dyn Error>>{
let contents=fs::read_to_string(config.file_path)?;//错误传递给函数调用者

let result = if config.case_sensitive{
search(&config.query, &contents)
}else{
search_case_insensitive(&config.query, &contents)
};

for line in result{
println!("{}",line);
}

Ok(())
}

pub struct Config{
pub query: String,
pub file_path: String,
pub case_sensitive:bool,
}


impl Config {
pub fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("缺少必须的参数");
}
let query = args[1].clone();
let file_path = args[2].clone();
let case_sensitive=env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, file_path, case_sensitive})
}
}

pub fn search<'a>(query:&str,contents:&'a str)->Vec<&'a str>{//检查contents里面是否有query这个字符串,有的话返回相关行
let mut results = Vec::new();

for line in contents.lines(){
if line.contains(query) {
results.push(line);
}
}
results
}

pub fn search_case_insensitive<'a>(query:&str,contents:&'a str)->Vec<&'a str>{//检查contents里面是否有query这个字符串,有的话返回相关行
let mut results = Vec::new();

let query=query.to_lowercase();//转换为小谢

for line in contents.lines(){
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}

#[cfg(test)]
mod tests{
use super::*;
#[test]
fn case_sensitive(){//区分大小写
let query = "duct";
let contents ="\
Rust:
safe,fast,productive.
Pick three.
Duct";

assert_eq!(vec!["safe,fast,productive."],
search(query,contents))
}

#[test]
fn case_insensitive(){//不区分大小写
let query = "rUst";
let contents ="\
Rust:
safe,fast,productive.
Pick three.
Trust me";

assert_eq!(vec!["Rust","Trust me"],
search_case_insensitive(query,contents))
}
}

image-20250106185827784

标准输出与标准错误

标准输出:stdout

println!

标准错误:stderr

eprintln!