Rust学习-构建命令行程序

本文通过重构一个简单的Rust命令行程序minigrep,探讨了如何改进参数读取、实现错误处理、提高代码可测试性以及添加新功能,如大小写敏感性。通过TDD方法,逐步完善搜索功能,并利用迭代器和过滤器简化代码。同时,介绍了如何利用环境变量来控制程序行为,以及将输出错误信息到标准错误流。

Rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择
本文以实现一个minigrep为例,展开对之前学习的回归

初版

接收命令行参数并打印文件内容

// 当所需函数嵌套了多于一层模块时,通常将父模块引入作用域
// std::env::args 在其任何参数包含无效 Unicode 字符时会 panic
// 如果需要接受包含无效 Unicode 字符的参数,使用 std::env::args_os
// 它返回 OsString 值,且OsString 值每个平台都不一样
use std::env;
use std::fs;

fn main() {
	// env::args()返回一个传递给程序的命令行参数的 迭代器(iterator)
    let args: Vec<String> = env::args().collect();
    
    // 程序的名称占据了 vector 的第一个值 args[0],和C的命令行参数相同
    
    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);
}

问题

(1)main 进行了两个任务,函数功能不单一
(2)query 和 filename 是程序中的配置变量,和代码中其他变量混到一起
(3)打开文件失败使用 expect 来打印出错误信息,没有得到失败原因
(4)使用 expect 来处理不同的错误,如果用户没有指定足够的参数来运行程序,则展示的错误依旧无法让使用者阅读

解决方式-关注分离

main的职责:
(1)使用参数值调用命令行解析逻辑
(2)设置任何其他的配置
(3)调用 lib.rs 中的 run 函数
(4)如果 run 返回错误,则处理这个错误

main.rs 处理程序运行
lib.rs 处理所有的真正的任务逻辑
因为不能直接测试 main 函数,这个结构通过将所有的程序逻辑移动到 lib.rs 的函数中使得可以测试他们

重构

重构参数读取

方案一

use std::env;
use std::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,
}

// 定义 Config 来包含拥有所有权的 String 值
// main 中的 args 变量是参数值的所有者并只允许 parse_config 函数借用他们
// 意味着如果 Config 尝试获取 args 中值的所有权将违反 Rust 的借用规则
fn parse_config(args: &[String]) -> Config {
    // 由于其运行时消耗,尽量避免使用 clone 来解决所有权问题
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

更合理的参数读取

use std::env;
use std::fs;

fn main() {
    // args类型是:alloc::vec::Vec<alloc::string::String>
    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 {
    // args类型是 &[alloc::string::String]
    // 使用这个方式也可以:fn new(args: &Vec<String>) -> Config
    // 此时argos类型为:&alloc::vec::Vec<alloc::string::String>
    // 这种转换有待后续深挖
    fn new(args: &[String]) -> Config {
        let query = args[1].clone();
        let filename = args[2].clone();
        Config { query, filename }
    }
}

改善错误信息

执行 cargo run test,直接panic

thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 2', src/main.rs:31:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

糟糕的方案一

fn new(args: &[String]) -> Config {
    if args.len() < 3 {
        panic!("not enough arguments"); // 不友好
    }
    ......

// panic信息如下
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值