rust实现命令行grep功能

前言

目标:命令行中在指定的文件中搜索指定的文字,实现类似grep功能。

内容:

  1. 接收命令行参数
  2. 读取文件
  3. 重构:改进模块和错误处理
  4. 使用TDD(测试驱动开发)开发库功能
  5. 使用环境变量
  6. 将错误消息写入标准错误而不是标准输出

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. 重构:改进模块和错误处理

我们先看一下上面代码书写存在哪些问题:

  1. main的功能多,违反单一原则
  2. 变量多,配置变量放在一个结构体
  3. 读取失败时,失败信息不明确
  4. 未指定参数时,发生panic,需要集中放置在一处

在rust中,二进制程序关注点分离的指导性原则:

  1. 将程序拆分为maim.rs和lib.rs将业务逻辑放入lib.rs

  2. 当命令行解析逻辑较少时,将它放在main.rs也行

  3. 当命令行解析逻辑变复杂时,需要将它从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

main.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);
    }
}

lib.rs

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. 编写一个会失败的测试,运行该测试,确保它是按照预期的原因失败

  2. 编写或修改刚好足够的代码,让新测试通过

  3. 重构刚刚添加或修改的代码,确保测试会始终通过

  4. 返回步骤1,继续

下面所有代码只在lib.rs中编写业务代码。

添加测试用例

写一个失败的测试用例

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
}

运行测试用例,通过。

如何读取环境变量

lib.rs

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命令的功能。

  1. 使用标准库中的env::args().collect()函数来获取命令行参数。
  2. 使用fs::read_to_string函数读取文件内容。
  3. 代码进行了重构,将解析参数的逻辑封装成函数,并使用结构体来存储参数。改进错误处理,使用Result枚举来处理传入参数不合法和文件读取失败的情况。
  4. 使用TDD(测试驱动开发)的方式开发了搜索函数,并通过测试用例进行验证。
  5. 使用环境变量来配置搜索的大小写敏感性,并使用标准错误输出来打印错误消息。

如有错误,请指正O^O!

相关推荐
软件技术NINI5 分钟前
html知识点框架
前端·html
深情废杨杨8 分钟前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS8 分钟前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
众生回避14 分钟前
鸿蒙ms参考
前端·javascript·vue.js
洛千陨15 分钟前
Vue + element-ui实现动态表单项以及动态校验规则
前端·vue.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
&白帝&2 小时前
uniapp中使用picker-view选择时间
前端·uni-app
魔术师卡颂2 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
白总Server2 小时前
MongoDB解说
开发语言·数据库·后端·mongodb·golang·rust·php
谢尔登2 小时前
Babel
前端·react.js·node.js