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信息如下
// thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
}
方案二
返回一个 Result
成功时带有一个 Config 实例
出错时带有一个 &'static str:字符串字面量
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();// unwrap_or_else 定义于标准库的 Result<T, E> // 使用它可以进行一些自定义的非 panic! 的错误处理// 当其值是 Err 时,该方法会调用一个 闭包(closure),即匿名函数// unwrap_or_else 会将 Err 的内部值,即静态字符串传递给|err|let config = Config::new(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {}", err);// process::exit 会立即停止程序并将传递给它的数字作为退出状态码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);
}// 打印信息如下
// Problem parsing arguments: not enough arguments
main 函数处理 new 函数返回的 Result 值
并在出现错误的情况更明确的结束进程
精简main
main的修改
fn main() {// --snip--println!("Searching for {}", config.query);println!("In file {}", config.filename);// 其他处理逻辑全部放入run函数// if let 来检查 run 是否返回一个 Err 值// run 并不返回像 Config::new 返回的 Config 实例那样需要 unwrap 的值// 因为 run 在成功时返回 ()// 而只关心检测错误,所以并不需要 unwrap_or_else 来返回未封装的值// 因为它只会是 ()if let Err(e) = run(config) {println!("Application error: {}", e);process::exit(1);}
}
run的处理
// 引入 trait 对象 Box<dyn Error>的路径
use std::error::Error;
// --snip--// unit 类型 ():作为 Ok 时的返回值类型
// trait 对象 Box<dyn Error>:
// 返回实现了 Error trait 的类型,无需指定具体将会返回的值的类型
// 因为在不同的错误场景可能有不同类型的错误返回值
fn run(config: Config) -> Result<(), Box<dyn Error>> {// 不同于遇到错误就 panic!// ? 会从函数中返回错误值并让调用者来处理它let contents = fs::read_to_string(config.filename)?;println!("With text:\n{}", contents);// 调用 run 函数只是为了它的副作用;函数并没有返回什么有意义的值Ok(())
}
拆模块
拥有可以测试的公有 API 的库 crate
逻辑提取到了 src/lib.rs
所有的参数解析和错误处理留在了 src/main.rs
直接使用多种参数调用函数并检查返回值,无需从命令行运行二进制文件
// src/lib.rs
use std::error::Error;
use std::fs;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>> {println!("Searching for {}", config.query);println!("In file {}", config.filename);// 不同于遇到错误就 panic!// ? 会从函数中返回错误值并让调用者来处理它let contents = fs::read_to_string(config.filename)?;println!("With text:\n{}", contents);// 调用 run 函数只是为了它的副作用;函数并没有返回什么有意义的值Ok(())
}
// main.rs
use std::env;
use std::process;use rust_minigrep::Config;fn main() {let args: Vec<String> = env::args().collect();// unwrap_or_else 定义于标准库的 Result<T, E>// 使用它可以进行一些自定义的非 panic! 的错误处理// 当其值是 Err 时,该方法会调用一个 闭包(closure),即匿名函数// unwrap_or_else 会将 Err 的内部值,即静态字符串传递给|err|let config = Config::new(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {}", err);// process::exit 会立即停止程序并将传递给它的数字作为退出状态码process::exit(1);});if let Err(e) = rust_minigrep::run(config) {println!("Application error: {}", e);process::exit(1);}
}
TDD开发搜索功能
测试驱动开发(Test Driven Development, TDD)
use std::error::Error;
use std::fs;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>> {println!("Searching for {}", config.query);println!("In file {}", config.filename);// 不同于遇到错误就 panic!// ? 会从函数中返回错误值并让调用者来处理它let contents = fs::read_to_string(config.filename)?;println!("With text:\n{}", contents);// 调用 run 函数只是为了它的副作用;函数并没有返回什么有意义的值Ok(())
}// 在 search 的签名中定义一个显式生命周期 'a 并用于 contents 参数和返回值
// 告诉 Rust 函数 search 返回的数据将与 search 函数中的参数 contents 的数据存在的一样久
// 为了使这个引用有效那么 被 slice 引用的数据也需要保持有效
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {vec![]
}// 如下则编译失败
// Rust 不可能知道需要的是哪一个参数,所以需要明确告诉它
// 参数 contents 包含了所有的文本而且希望返回匹配的那部分文本,所以contents 应该要使用生命周期语法来与返回值相关联的参数
// 其他语言中并不需要你在函数签名中将参数与返回值相关联
// pub fn search(query: &str, contents: &str) -> Vec<&str> {// 先编写测试用例
#[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));}
}
上述铁定测试失败,因为没有开发search模块,所以search的功能如下:
(1)遍历内容的每一行文本
(2)查看这一行是否包含要搜索的字符串
(3)如果有,将这一行加入列表返回值中
(4)如果没有,什么也不做。
(5)返回匹配到的结果列表
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut results = Vec::new();// contents.lines 返回一个迭代器for line in contents.lines() {// 字符串的contains方法检查包含操作if line.contains(query) {results.push(line);}}results
}
最终lib.rs内容如下
use std::error::Error;
use std::fs;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>> {println!("Searching for {}", config.query);println!("In file {}", config.filename);// 不同于遇到错误就 panic!// ? 会从函数中返回错误值并让调用者来处理它let contents = fs::read_to_string(config.filename)?;println!("With text:\n{}", contents);for line in search(&config.query, &contents) {println!("{}", line);}// 调用 run 函数只是为了它的副作用;函数并没有返回什么有意义的值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
}#[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));}
}
错误信息打印到标准输出流
目前为止将所有的输出都 println! 到了终端
大部分终端都提供了两种输出:
标准输出(standard output,stdout)对应一般信息
标准错误(standard error,stderr)则用于错误信息
cargo run > output.txt
shell将所有信息存储到 output.txt
结果output.txt中存储了错误信息
将错误打印到标准错误
// 标准库提供了 eprintln! 宏来打印到标准错误流eprintln!("Problem parsing arguments: {}", err);
添加区分大小写的功能
设置环境变量来设置搜索是否是大小写敏感
vi ~/.zshrc
添加如下一行
export RUST_CASE_INSENSITIVE=1env
查看是否设置成功
RUST_CASE_INSENSITIVE=1
use std::error::Error;
use std::fs;
use std::env;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() < 3 {return Err("not enough arguments");}let query = args[1].clone();let filename = args[2].clone();// 处理环境变量的函数位于标准库的 env 模块中let case_sensitive = env::var("RUST_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)?;let results = if config.case_sensitive {search(&config.query, &contents)} else {search_case_insensitive(&config.query, &contents)};for line in results {println!("{}", line);}Ok(())
}pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let query = query.to_lowercase();let mut results = Vec::new();for line in contents.lines() {if line.to_lowercase().contains(&query) {results.push(line);}}results
}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 case_sensitive() {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_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));}
}
使用迭代器重构
// main.rs
use std::env;
use std::process;use rust_minigrep::Config;fn main() {// unwrap_or_else 定义于标准库的 Result<T, E>// 使用它可以进行一些自定义的非 panic! 的错误处理// 当其值是 Err 时,该方法会调用一个 闭包(closure),即匿名函数// unwrap_or_else 会将 Err 的内部值,即静态字符串传递给|err|let config = Config::new(env::args()).unwrap_or_else(|err| {println!("Problem parsing arguments: {}", err);// process::exit 会立即停止程序并将传递给它的数字作为退出状态码process::exit(1);});if let Err(e) = rust_minigrep::run(config) {println!("Application error: {}", e);process::exit(1);}
}
// lib.rsuse std::error::Error;
use std::fs;
use std::env;pub struct Config {pub query: String,pub filename: String,pub case_sensitive: bool, // 新增加控制字段
}impl Config {pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {args.next();let query = match args.next() {Some(arg) => arg,None => return Err("Didn't get a query string"),};let filename = match args.next() {Some(arg) => arg,None => return Err("Didn't get a file name"),};let case_sensitive = env::var("RUST_CASE_INSENSITIVE").is_err();println!("query={}", query);println!("filename={}", filename);println!("case_sensitive={}", case_sensitive);Ok(Config { query, filename, case_sensitive })}
}pub fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.filename)?;let results = if config.case_sensitive {println!("run case senstive");search(&config.query, &contents)} else {println!("run case insenstive");search_case_insensitive(&config.query, &contents)};for line in results {println!("搜索结果: {}", line);}Ok(())
}pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let query = query.to_lowercase();let mut results = Vec::new();for line in contents.lines() {if line.to_lowercase().contains(&query) {results.push(line);}}results
}pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {// 字符串数组的迭代器contents.lines()// 留下true的,去掉false的.filter(|line| line.contains(query))// 收集结果.collect()
}#[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."],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));}
}
附录
打印类型
fn print_type_of<T>(_: &T) {println!("{}", std::any::type_name::<T>())
}
环境变量的设置
vi ~/.bash_profile
vi ~/.zshrc需要设置哪个?
echo $SHELL系统安装了哪些shell
cat /etc/shells
mac 中使用 zsh,部分因为 oh-my-zsh 配置集,兼容 bash,还能自动补全。
sh 是 unix 上的标准 shell,很多 unix 版本都配有它
bash由 gnu 组织开发,保持了对 sh shell 的兼容性,是各种 Linux 发行版默认配置的 shell。
bash 兼容 sh:
针对 sh 编写的 shell 代码可以不加修改地在 bash 中运行
bash 和 sh 不同:
bash 扩展了一些命令和参数
bash 并不完全和 sh 兼容,它们有些行为并不一致