前言
目标:命令行中在指定的文件中搜索指定的文字,实现类似grep功能。
内容:
- 接收命令行参数
- 读取文件
- 重构:改进模块和错误处理
- 使用TDD(测试驱动开发)开发库功能
- 使用环境变量
- 将错误消息写入标准错误而不是标准输出
1. 接收命令行参数
使用标准库中的env::args().collect()
获取命令行参数。
rs
use std::env;
fn main() {
let args:Vec<String> =env::args().collect();
println!("{:?}",args);
}
注意: env::args
能获取指定参数,但不能获取非法的utf-8参数,可以使用env::args_os()
返回OsString类型。本案例暂不考虑这种情况。
运行cargo run day poem.txt
,结果如下:
2. 读取文件
读取文件使用fs::read_to_string
读取文本内容。
rs
use std::env;
use std::fs;
fn main() {
let args:Vec<String> =env::args().collect();
println!("{:?}",args);
let query = &args[1];
let filename = &args[2];
let contents = fs::read_to_string(filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents)
}
创建poem.txt,运行cargo run day poem.txt
,结果如下:
3. 重构:改进模块和错误处理
我们先看一下上面代码书写存在哪些问题:
- main的功能多,违反单一原则
- 变量多,配置变量放在一个结构体
- 读取失败时,失败信息不明确
- 未指定参数时,发生panic,需要集中放置在一处
在rust中,二进制程序关注点分离的指导性原则:
当命令行解析逻辑较少时,将它放在main.rs也行
当命令行解析逻辑变复杂时,需要将它从main.rs提取到lib.rs
代码重构
经过上述分析,留在main的功能有:
- 使用参数值调用命令行解析逻辑
- 进行其它配置
- 调用lib.rs中的run函数
- 处理run函数可能出现的错误
将解析参数进行封装成函数
js
let (query,filename) = parse_config(&args);
fn parse_config(args:&[String])->(&str,&str){
let query = &args[1];
let filename = &args[2];
(query,filename)
}
这样就会出现很多参数,如query、filename, 可以使用struct结构体,将这些参数放在一起
js
use std::env;
use std::fs;
struct Config {
query: String,
filename: String,
}
fn main() {
let args:Vec<String> =env::args().collect();
let config = parse_config(&args);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents)
}
fn parse_config(args: &[String]) -> Config {
let query = args[1];
let filename = args[2];
Config{query, filename}
}
上述代码会出现所有权问题,改变了args引用的所有权,简单方法使用clone
rs
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
接着将读取的方法移动到结构体中
rs
use std::env;
use std::fs;
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}
fn main() {
let args:Vec<String> =env::args().collect();
let config = Config::new(&args);
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents)
}
至此代码看起来非常的rust。
错误处理
如何处理传入参数不合法,提示信息人性化。判断传入参数是否合法,提示错误。
- painc一般是程序的问题使用
js
impl Config {
fn new(args: &[String]) -> Config {
if args.len()<3 {
panic!("not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Config { query, filename }
}
}
- 而传入参数出现的问题,是属于用户使用问题,可以返回一个Result枚举进行处理
rs
use std::env;
use std::fs;
use std::process;
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Result<Config,&'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query, filename})
}
}
fn main() {
let args:Vec<String> =env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}",err);
process::exit(1);
});
let contents = fs::read_to_string(config.filename)
.expect("Something went wrong reading the file");
println!("With text:\n{}", contents)
}
继续优化读取错误,进行封装
rs
use std::env;
use std::fs;
use std::process;
use std::error::Error;
struct Config {
query: String,
filename: String,
}
impl Config {
fn new(args: &[String]) -> Result<Config,&'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query, filename})
}
}
fn run(config:Config)->Result<(),Box<dyn Error>>{
// 错误处理使用config一样的处理方式 ?处理方式是把错误返回给调用者
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
fn main() {
let args:Vec<String> =env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}",err);
process::exit(1);
});
if let Err(e) = run(config){
println!("Application error:{}",e);
process::exit(1);
}
}
模块化
main.rs
中只保留主逻辑,业务代码放在lib.rs
rs
use mini::Config;
use std::env;
use std::process;
fn main() {
let args:Vec<String> =env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing arguments:{}",err);
process::exit(1);
});
if let Err(e) = mini::run(config){
println!("Application error:{}",e);
process::exit(1);
}
}
rs
use std::fs;
use std::error::Error;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config,&'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query, filename})
}
}
pub fn run(config:Config)->Result<(),Box<dyn Error>>{
// 错误处理使用config一样的处理方式 ?处理方式是把错误返回给调用者
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}
4. 使用TDD(测试驱动开发)开发库功能
使用TDD,编写添加搜索逻辑。
常规的TDD开发流程:
编写一个会失败的测试,运行该测试,确保它是按照预期的原因失败
编写或修改刚好足够的代码,让新测试通过
重构刚刚添加或修改的代码,确保测试会始终通过
返回步骤1,继续
下面所有代码只在lib.rs
中编写业务代码。
添加测试用例
写一个失败的测试用例
rs
pub fn search<'a>(query:&str,contents:&'a str)->Vec<&'a str>{
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result(){
let query = "duct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe,fast,productive."],search(query,contents));
}
}
开始写search函数的逻辑:
rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
// 检查每一行是否包含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));
}
}
执行测试用例,通过。
5. 使用环境变量
搜索的query
是否区分大小写,如果使用环境变量,只需要在终端配置一次,一直有效。
具体做法是根据配置的环境变量,确认是否需要将query和contents中的每一行都转换成小写再进行比较。
编写测试用例
rs
pub fn search_case_insensitive<'a>(query:&str,contents:&'a str)->Vec<&'a str>{
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive(){
let query = "dUct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe,fast,productive.","Duct tape."],search_case_insensitive(query,contents));
}
}
编写search_case_insensitive
函数逻辑:
rs
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
}
运行测试用例,通过。
如何读取环境变量
js
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub query: String,
pub filename: String,
pub case_insensitive: bool,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
// 判断是否设置环境变量
let case_insensitive = env::var("CASE_INSENSITIVE").is_err();
println!("case_insensitive:{}", case_insensitive);
Ok(Config {
query,
filename,
case_insensitive,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// 错误处理使用config一样的处理方式 ?处理方式是把错误返回给调用者
let contents = fs::read_to_string(config.filename)?;
// 打印结果
let results = if config.case_insensitive {
println!("case_insensitive:{}", config.case_insensitive);
search_case_insensitive(&config.query, &contents)
} else {
println!("case_sensitive:{}", config.case_insensitive);
search(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
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 one_result() {
let query = "duct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe,fast,productive."], search(query, contents));
}
#[test]
fn case_sensitive() {
let query = "dUct";
let contents = "\
Rust:
safe,fast,productive.
Pick three.
Duct tape.";
assert_eq!(
vec!["safe,fast,productive.", "Duct tape."],
search_case_insensitive(query, contents)
);
}
}
运行 set CASE_INSENSITIVE=1 && cargo run ha poem.txt
6. 将错误消息写入标准错误而不是标准输出
标准输出vs标准错误
-
标准输出:stdout
- println!
-
标准错误::stderr
- eprintln!
标准输出
运行 cargo run > output.txt
错误信息打印到标准输出
标准错误
错误时 使用eprintln,就不会在output.txt中输出,只在控制台中显示
rs
use mini::Config;
use std::env;
use std::process;
fn main() {
let args:Vec<String> =env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
eprintln!("Problem parsing arguments:{}",err);
process::exit(1);
});
if let Err(e) = mini::run(config){
eprintln!("Application error:{}",e);
process::exit(1);
}
}
就不会输出到output.txt
运行 cargo run ha poem.txt > output.txt ,输出结果会在output.txt中显示
总结
学习了如何使用命令行参数来搜索指定文件中的文字,实现类似于grep命令的功能。
- 使用标准库中的
env::args().collect()
函数来获取命令行参数。 - 使用
fs::read_to_string
函数读取文件内容。 - 代码进行了重构,将解析参数的逻辑封装成函数,并使用结构体来存储参数。改进错误处理,使用
Result
枚举来处理传入参数不合法和文件读取失败的情况。 - 使用TDD(测试驱动开发)的方式开发了搜索函数,并通过测试用例进行验证。
- 使用环境变量来配置搜索的大小写敏感性,并使用标准错误输出来打印错误消息。
如有错误,请指正O^O!