编程语言 重构minigrep-Rust xiu 2025-04-11 2025-04-11 综合所学基础构建一个简易的grep程序
grep程序可以过滤文件中的特定字符串并且打印相关行
接受命令行参数 新建项目
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); }
根据截图可知想要查询的字符串为迭代器的第二个元素,想要查找的文件路径在迭代器的第三个元素,为两个元素赋变量名
读取文件 在项目根目录创建一个文件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!
尝试读取文件打印出来,由于未能实现搜索功能,第一个参数随便填
改进模块与错误处理 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,并且提示信息面向的是程序员而不是给用户去看
在 new 函数中增加了一个检查在访问索引 1 和 2 之前检查 slice 是否足够长。如果 slice 不够长,程序会打印一个更好的错误信息并 panic
但是报错仍然有一些额外信息打扰,我们希望去除它,针对于程序运行问题出错我们倾向于使用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| { println! ("Problem parsing arguments: {err}" ); process::exit (1 ); }); if let Err (e)= run (config){ println! ("程序错误{}" ,e); process::exit (1 ); }; } 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| { println! ("Problem parsing arguments: {err}" ); process::exit (1 ); }); if let Err (e)= minigrep::run (config){ println! ("程序错误{}" ,e); process::exit (1 ); }; }
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 >{ 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)) } }
使用环境变量 搜索字符串的大小写我们需要可以自主控制
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 >{ 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 >{ 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)) } }
标准输出与标准错误 标准输出:stdout
println!
标准错误:stderr
eprintln!