【跟小嘉学 Rust 编程】十二、构建一个命令行程序

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序

前言

本章是一个目前所学的很多技能的应用,以及标准库的探索,我们讲构建一个命令行程序工具来练习现在已经学习过的一些Rust的技能。我们将构建自己的版本的命令行工具:grep(Globally search a Regular Expression and print)。

主要教材参考 《The Rust Programming Language》


一、如何接受命令行参数

1.1、创建项目

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

1.2、需求介绍

minigrep 能够接受两个命令行参数:文件名和要搜索的字符串。也就是说我们希望使用cargo run的时候,可以使用如下的方式。

cargo run searchstring example-filename.txt

Crates.io 上会有一些现场的库帮助我们接受命令行参数(clap)。不过我们现阶段使用标准库。

1.3、读取参数值

为了能够接受命令行参数的值,我们需要使用 rust 标准库提供的函数。该函数返回一个命令行参数的迭代器(iiterator),迭代器我们将会在下一章详细讲解。我们只需要知道在迭代器上有一个方法 collect 可以将其转换为一个集合。

use std::env;

fn main() {
    
    
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}

需要注意 args 函数 在其任何参数包含 无效Unicode 字符时会panic。 如果你需要接受包含无效Unicode字符的参数,使用 std::env::args_os 代替。该函数返回 OsString值而不是 String 值。

Vector 的第一个参数是二进制文件的名称。

1.4、将参数值保存进变量

use std::env;

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

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);
}

二、读取文件(使用 fs 模块读取文件)

use std::{
    
    env, fs};

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

    let query = &args[1];
    let filename = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
    
}

fs::read_to_string(filename) 方法打开文件,返回包含内容的Result<String>

三、模块化与错误处理

我们上述代码 main 函数有着多个职责,通常函数只负责一个功能会更加简洁并且易于维护。在开发的时候重构是一个最佳时间,重构少量代码要容易的多。

3.1、代码中存在的问题

我们最初的代码存在下面四个问题:

  • 1、 main 现在进行了两个功能:解析参数并且打开文件。但是当函数承担了更多责任,会更加难易推导,难以测试,并且难以在不破坏其他部分的情况下做出修改。
  • 2、query 和 flename 是程序中过的配置i变了,而 contents 则用来执行程序逻辑。当变量越来越多的时候便会难以追踪分析每个变量的目的,最好能够讲配置变量组织进一个结构。这样就能够使他们的目的更加明确;
  • 3、如果打开文件失败 ,我们使用 expect 来打印错误信息,不过这种错误信息并不明确,读取文件失败的原因有很多种:例如文件不存在,或者没有打开文件的权限等,无论那种情况,这并没有给予使用者具体的信息
  • 4、我们不停的使用 expect 来处理不同的错误,如果用户没有指定足够的的参数来运行程序,他们会从 rust 中得到 一个 index out of bounds 错误,而这并不能明确解释问题。如果所有的错误处理都位于一处,这样将来的维护者需要在修改错误处理逻辑时只需要考虑这一处代码。

3.2、二进制项目的关注分离

main 函数负责多个任务的组织问题在许多二进制项目中很常见。所以 Rust 社区开发出一类在 main 函数开始变得庞大时进行二进制程序的关注分离的指导性过程。这些过程有如下步骤:

  • 将程序拆分成 main.rs 和 lib.rs 并将程序的逻辑放入 lib.rs 中。
  • 当命令行解析逻辑比较小时,可以保留在 main.rs 中。
  • 当命令行解析开始变得复杂时,也同样将其从 main.rs 提取到 lib.rs 中。

经过这些过程之后保留在 main 函数中的责任应该被限制为:

  • 使用参数值调用命令行解析逻辑
  • 设置任何其他的配置
  • 调用 lib.rs 中的 run 函数
  • 如果 run 返回错误,则处理这个错误

这个模式的一切就是为了关注分离:main.rs 处理程序运行,而 lib.rs 处理所有的真正的任务逻辑。因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得我们可以测试他们。仅仅保留在 main.rs 中的代码将足够小以便阅读就可以验证其正确性。让我们遵循这些步骤来重构程序

3.3、提取参数解析器

use std::{
    
    env, fs};

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

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

    println!("Searching for {}", query);
    println!("In file {}", filename);

    let contents = fs::read_to_string(filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);

}

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

    (query, filename)
}

3.4、使用结构来组织配置变量

use std::{
    
    env, fs};

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

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);

}

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

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

    Config {
    
     query, filename }
}

我们需要注意 我们定义的 Config 包含拥有所有权的String值,我们返回来引用 args 中的 String值的字符串切片 slice。 main函数的args变量是参数值的所有者并只允许 parse_config 方法借用他们。这意味着 Config 尝试获取args 中的值的所有权将违反 Rust的借用规则。

还有许多不同的方式可以处理 String 的数据,而最简单但有些不太高效的方式是调用这些值的 clone 方法。这会生成 Config 实例可以拥有的数据的完整拷贝,不过会比储存字符串数据的引用消耗更多的时间和内存。不过拷贝数据使得代码显得更加直白因为无需管理引用的生命周期,所以在这种情况下牺牲一小部分性能来换取简洁性的取舍是值得的。

由于其运行时消耗,许多 Rustacean 之间有一个趋势是倾向于避免使用 clone 来解决所有权问题。

在关于迭代器的章节中,我们将学习如何更加有效率的处理这种情况,不过现在复制字符串取得进展是没有问题的。因为只会进行一次这样的拷贝,而且文件名和要搜索的字符串都比较短。

3.5、创建 Config 的构造函数

use std::{
    
    env, fs};

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

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);

}

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 }
    }
} 

3.6、修复错误处理

3.6.1、改善错误提示

对于错误,我们可以使用 panic!,但是 panic!更趋向于程序上的问题,而不是使用上的问题,我们应该使用Result 枚举来处理错误。

use std::{
    
    env, fs, process};

const ARGS_LENGTH:usize= 3;

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
    .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);

}

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

impl Config {
    
    
    fn new(args: &[String])-> Result<Config, &'static str>{
    
    
        if args.len() < ARGS_LENGTH  {
    
    
           return Err("not enough arguments")
        }

        let query = args[1].clone();
        let filename = args[2].clone();
    
       Ok( Config {
    
     query, filename })
    }
} 

3.6.2、业务逻辑处理:run方法

use std::{
    
    env, fs, process};

const ARGS_LENGTH:usize= 3;

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    run(config);
}

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

impl Config {
    
    
    fn new(args: &[String])-> Result<Config, &'static str>{
    
    
        if args.len() < ARGS_LENGTH  {
    
    
           return Err("not enough arguments")
        }

        let query = args[1].clone();
        let filename = args[2].clone();
    
       Ok( Config {
    
     query, filename })
    }
} 

fn run(config: Config) {
    
    
    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

3.6.3、run 函数返回 Result 错误

use std::{
    
    env, fs, process, error::Error};

const ARGS_LENGTH:usize= 3;

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = run(config) {
    
    
        println!(" Application error: {}", e);
        process::exit(1);
    }
}

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

impl Config {
    
    
    fn new(args: &[String])-> Result<Config, &'static str>{
    
    
        if args.len() < ARGS_LENGTH  {
    
    
           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>>{
    
    
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);
    Ok(())
}

3.7、将代码拆分

3.7.1、lib.rs


use std::{
    
    fs, error::Error};

const ARGS_LENGTH:usize= 3;

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

 impl Config {
    
    
    pub fn new(args: &[String])-> Result<Config, &'static str>{
    
    
        if args.len() < ARGS_LENGTH  {
    
    
           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>>{
    
    
    let contents = fs::read_to_string(config.filename)?;

    println!("With text:\n{}", contents);
    Ok(())
}

3.7.2、main.rs

use std::{
    
    env, process};
use minigrep::Config;

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    if let Err(e) = minigrep::run(config) {
    
    
        println!(" Application error: {}", e);
        process::exit(1);
    }
}

四、测试驱动开发(TDD)

4.1、search 方法编写和测试

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    
    
    let mut result = Vec::new();
    for line in contents.lines(){
    
    
        if line.contains(query) {
    
    
            result.push(line);
        }
    }
    println!("{:?}", result);
    result
}


#[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)
        );
    }
}

4.2、在 run 函数中使用 search 函数

pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
    
    
    let contents = fs::read_to_string(config.filename)?;

    for line in search(&config.query, &contents) {
    
    
        println!("{}", line);
    }
    
    Ok(())
}

五、处理环境变量


use std::{
    
    fs, error::Error, env};

const ARGS_LENGTH:usize= 3;

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

 impl Config {
    
    
    pub fn new(args: &[String])-> Result<Config, &'static str>{
    
    
        if args.len() < ARGS_LENGTH  {
    
    
           return Err("not enough arguments")
        }

        let query = args[1].clone();
        let filename = args[2].clone();
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
    
       Ok( Config {
    
     query, filename , case_sensitive})
    }
} 

pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
    
    
    let contents = fs::read_to_string(config.filename)?;

    for line in search(&config.query, &contents, config.case_sensitive) {
    
    
        println!("{}", line);
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str, case_sensitive: bool) -> Vec<&'a str> {
    
    
    let mut result = Vec::new();
    if case_sensitive {
    
    
        let query_ignore_sensitive = query.to_lowercase();
        for line in contents.lines(){
    
    
            if line.to_lowercase().contains(&query_ignore_sensitive) {
    
    
                result.push(line);
            }
        }
        return result;
    } else {
    
    
        for line in contents.lines(){
    
    
            if line.contains(&query) {
    
    
                result.push(line);
            }
        }
        return result;
    }
}


#[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)
        );
    }
}

六、标准输出和标准错误

6.1、标准输出:stdout

println!() 宏就是把输出信息输出到标准输出

6.2、标准错误:stderr

eprintln!() 宏就是把输出信息输出到标准错误

use std::{
    
    env, process};

use minigrep::Config;

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

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);
    println!("case_sensitive: {}", config.case_sensitive);

    if let Err(e) = minigrep::run(config) {
    
    
        eprintln!(" Application error: {}", e);
        process::exit(1);
    }
}

总结

以上就是今天要讲的内容

  • 主要讲解了一个项目的编写过程

猜你喜欢

转载自blog.csdn.net/fj_Author/article/details/132126668