1、创建项目:
cargo new minigrep
然后使用vs code打开
我们的需求是要通过命令行参数来搜索文件
cargo run 【字符串】 【xxx.txt】
下面我们首先要做的就是读取命令行参数:
use std::env;
fn main() {
let args : Vec<String> = env::args().collect();
print!("{:?}",args)
}
我们可以使用cargo run 和 cargo run 1234 abc.txt来进行测试一下
通过上面我们已经能够进行获取命令行参数了,现在我们在来提取输入的两个参数【1234,abc.txt】
use std::env;
fn main() {
let args : Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("Search for {}",query);
println!("In file {}",filename);
}
2、读取文件
上面我们已经能够获取命令行参数了,现在我们来读取文件
我们在项目src的同级目录中创建一个poem.txt文件:
greed is good
whos your daddy
i'm your father
you are my Son
are You ok
oh shit
现在我们来读取文件的内容
use std::env;
use std::fs;
fn main() {
let args : Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("Search for {}",query);
println!("In file {}",filename);
let contents = fs::read_to_string(filename).expect("文件读取失败");
println!("读取的内容\n{}",contents);
}
cargo run 1234 abc.txt 运行后:
3、改善模块化
上面的代码中我们已经能够读取命令行参数和文件的数据了,但是代码模块化不够好,错误处理不够完善,我们趁着代码比较简单的时候,我们应该尽早进行重构
二进制程序关注点分离的指导性原则
将程序拆分为main.rs和 lib.rs,将业务逻辑放入lib.rs
当命令行解析逻辑较少时,将它放在 main.rs也行
当命令行解析逻辑变复杂时,需要将它从main.rs提取到lib.rs
经过上述拆分,留在main的功能有:
使用参数值调用命令行解析逻辑
进行其它配置
调用lib.rs中的run函数
处理run函数可能出现的错误
use std::env;
use std::fs;
fn main() {
let args : Vec<String> = env::args().collect();
let (query,filename) = parse_config(&args);
let contents = fs::read_to_string(filename).expect("文件读取失败");
println!("读取的内容\n{}",contents);
}
fn parse_config(args: &[String]) -> (&str,&str) {
let query = &args[1];
let filename = &args[2];
(query,filename)
}
我们进行了第一个重构,但是解析出来的参数并没有关系,我们可以用struct来再次重构一下
use std::env;
use std::fs;
fn main() {
let args : Vec<String> = env::args().collect();
let config = Config::new(&args);
let contents = fs::read_to_string(config.filename).expect("文件读取失败");
println!("读取的内容\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}
}
}
现在我们使用了一个struct来进行了重构,代码更加的模块化了
4、错误处理
之前我们运行的都是cargo run 1234 abc.txt 两个参数都没有报错,但是如果我们不传参数直接使用cargo run会发生什么呢?
上述代码发生了恐慌,也就是我们的代码错误处理不够完善
虽然上诉的错误我们能够看的懂,但是我们需要让普通人也能够看懂,所以我们再次进行重构
use std::env;
use std::fs;
fn main() {
let args : Vec<String> = env::args().collect();
let config = Config::new(&args);
let contents = fs::read_to_string(config.filename).expect("文件读取失败");
println!("读取的内容\n{}",contents);
}
struct Config {
query:String,
filename:String
}
impl Config {
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("没有足够的参数");
}
let query = args[1].clone();
let filename = args[2].clone();
Config{query,filename}
}
}
重构后,我们能够在命令行里面输出一个更加通用的错误信息了,但是我们的程序并不是一个系统错误,我们不需要使用panic!来处理,只需要通过Result枚举即可
use std::env;
use std::fs;
use std::process;
fn main() {
let args : Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("参数解析失败:{}",err);
process::exit(1);
});
let contents = fs::read_to_string(config.filename).expect("文件读取失败");
println!("读取的内容\n{}",contents);
}
struct Config {
query:String,
filename:String
}
impl Config {
fn new(args: &[String]) -> Result<Config,&'static str> {
if args.len() < 3 {
return Err("没有足够的参数");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query,filename})
}
}
再次运行cargo run
现在我们的错误处理又完善了一部分
5、业务逻辑移动到lib.rs中
我们在src目录下创建一个lib.rs文件,然后把相关的业务移动到这个文件中
use std::fs;
use std::error::Error;
pub fn run(config:Config) -> Result<(),Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("读取的内容\n{}",contents);
Ok(())
}
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("没有足够的参数");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query,filename})
}
}
在main中只需要相关的胶水代码进行调用即可:
use std::env;
use minigrep::Config;
use std::process;
fn main() {
let args : Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
println!("参数解析失败:{}",err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
println!("程序错误{}",e);
process::exit(1);
}
}
6、使用TDD(测试驱动开发)开发库功能
测试驱动开发
TDD (Test-Driven Development)
编写一个会失败的测试,运行该测试,确保它是按照预期的原因失败
编写或修改刚好足够的代码,让新测试通过
重构刚刚添加或修改的代码,确保测试会始终通过
返回步骤1,继续
在lib.rs:
use std::fs;
use std::error::Error;
pub fn run(config:Config) -> Result<(),Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("读取的内容\n{}",contents);
Ok(())
}
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("没有足够的参数");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query,filename})
}
}
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.
";
assert_eq!(vec!["safe,fast,productive."],search(query,contents))
}
}
cargo test 然后运行后报错了,这也是我们的期待的:
use std::fs;
use std::error::Error;
pub fn run(config:Config) -> Result<(),Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
println!("读取的内容\n{}",contents);
Ok(())
}
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("没有足够的参数");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query,filename})
}
}
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.trim());
}
}
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))
}
}
现在我们通过了测试,然后把测试的相关逻辑编写到run函数中
use std::fs;
use std::error::Error;
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(())
}
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("没有足够的参数");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query,filename})
}
}
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.trim());
}
}
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))
}
}
然后运行cargo run father poem.txt
这样我们的程序就完成了
7、使用环境变量
现在我们的程序需要扩展一些功能,比如区分大小写
我们可以通过一个环境变量的控制是否要区分大小写
use std::fs;
use std::error::Error;
use std::env;
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 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("没有足够的参数");
}
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 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.trim());
}
}
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.trim());
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
// 区分大小写
let query = "duct";
let contents = "\
Rust
safe,fast,productive.
Pick three.
Duck 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))
}
}
运行cargo run you poem.txt
运行set CASE_INSENSITIVE=1 && cargo run you poem.txt【这个是windows10上运行的,其他操作系统如果失效,请自行百度】
8、将错误信息写入到标准错误
标准输出vs标准错误。
标准输出: stdout
printIn!
标准错误: stderr
eprintln!
首先我们来运行一个命令:
cargo run > output.txt
这里的错误信息没有显示,
那么错误信息在哪呢?
我们现在把错误信息打印到了标准输出里面了,但是更好的做法应该是把错误信息打印到标准错误里面
我们修改main函数:
use std::env;
use minigrep::Config;
use std::process;
fn main() {
let args : Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err|{
eprintln!("参数解析失败:{}",err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("程序错误{}",e);
process::exit(1);
}
}
使用错误打印的方式进行打印
然后我们在运行一下:cargo run > output.txt
这个时候错误信息就被输出到屏幕上了,而output.txt文件上就什么都没有了。
我们再来是一个带参数的命令:cargo run is poem.txt > output.txt
最后发现结果被输出到了output.txt文件中了