【Rust 学习记录】12. 编写一个命令行程序

TwoSix Lv3

本章节我们将开始学习编写一个小项目——开发一个能够和文件系统交互并处理命令行输入、输出的工具

基本功能实现

首先自然是新建一个项目,名为minigrep

1
cargo new minigrep

实现这一个工具的首要任务自然是接收命令行的参数,例如我们要实现在一个文件里搜索字符串,就得在运行时接收两个参数,一个待搜索的字符串,一个搜索的文件名,例如

1
cargo run string filename.txt

这种基础的功能自然是已经有一些现成的crate可以使用来实现这些功能的了,不过因为我们的主要目的是学习,所以接下来我们会从零开始实现这些功能。

读取参数值

这一部分我们需要用到标准库里的std::env::args函数,这个函数会返回一个命令行参数的迭代器,使得程序可以读取所有传递给它的命令行参数值,放到一个动态数组里。

用例:

1
2
3
4
5
6
use std::env;

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

这段代码简单来说就是通过env::args()读取命令行参数的迭代器,再通过collect()函数自动遍历迭代器把参数收集到一个动态数组里并返回。

接下来我们用终端运行代码,传入参数试试

1
2
3
4
5
cargo run hahha arg   
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 1.81s
Running `target\debug\minigrep.exe hahha arg`
["target\\debug\\minigrep.exe", "hahha", "arg"]

这里的输出的第一个参数是当前运行的程序的二进制文件入口路径,这一功能主要方便程序员打印程序名称/路径,后续才是我们传入的参数字符串,一般情况下我们忽略第一个参数只处理后面两个即可。

1
2
3
4
5
6
7
8
9
10
11
12
use std::env;

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

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

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

接下来我们再运行以下这段代码,输出一切正常的话,第一步接收命令行参数的任务我们就完成啦!

读取文件

既然是要在文件内容里搜索指定字符串,自然要读取文件的内容了。但在这之前我们要先有一个文件,现在我们在项目的根目录下,新建一个poem.txt文件,内容就选择书上给的这首诗吧:

1
2
3
4
5
6
7
8
9
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

接下来,我们就可以在代码里读取这个文件了,也很简单,用之前使用过的fs库即可,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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];

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

let contents = fs::read_to_string(filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

接下来,再运行以下这段代码,用命令行传递参数的方法传递我们的文件名poem.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cargo run bog poem.txt       
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target\debug\minigrep.exe bog poem.txt`
["target\\debug\\minigrep.exe", "bog", "poem.txt"]
Searching for bog
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.

How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

可见,顺利的输出了我们的文件内容,代码没有问题。

到此为止,我们的基本功能就实现完了。但目前为止,我们现在写的代码都一股脑的堆积在main.rs里,非常简单,但也不具备可扩展性以及可维护性,远远称不上一个”项目”。接下来我们就要用到之前学习的模块化管理的知识,重构一下我们的代码,让它变得更规范化。

模块化与错误处理

问题与计划

接下来我们将针对以下四个问题,来制定计划重构我们的代码:

  1. 功能如果都堆积在main函数里的话,随着我们的功能越来越多,这个文件的代码将会变得越来越复杂,越来越难让人理解,更难去测试,耦合程度很高。所以我们要把函数拆分开来,一个函数负责一个任务
  2. query和filename是存储程序配置的,contents是用于业务逻辑的,当一个作用域的变量越来越多时,我们就很难准确的追踪每个变量的实际用途;所以我们最好应该把用途相同的变量合并到一个结构体里,方便管理也明确用途。
  3. 错误处理方面处理的不够详细,读取文件失败可能有很多原因,我们应该准确定位到每个原因,抛出相关的提示,给用户提供更有效的排错信息。
  4. 错误管理也应该模块化,例如当用户没有指定运行参数时,底层报错是一个数组越位时,抛出错误”index out of bounds”,这无法帮助用户有效的理解问题本身,我们最好把用于错误处理的代码集中管理,更加方便的处理相关的错误逻辑,也方便为用户打印有意义,便于理解的报错信息。

二进制项目的组织结构

Rust社区对于程序的组织结构有一套自己的原则

  1. 程序拆分为main.rs和lib.rs,实际的逻辑放在lib.rs,main.rs只作简单调用
  2. 如果逻辑相对简单,再考虑留在main.rs,保留在main中的代码量应该小到可以一眼看出这段代码的正确性

也就是,main负责运行,lib负责实际逻辑,同时由上一章自动化测试可知,如果我们把逻辑放到lib.rs也更方便我们编写集成测试的代码。

所以,我们这个项目的main函数功能大概如下

  • 处理命令行参数
  • 程序配置的变量
  • 调用lib.rs中的run函数
  • 对run函数进行错误处理

接下来我们就可以开始按照以上原则重构了

分离基本功能逻辑

提取解析参数代码

首先,我们把解析参数做成一个函数,提供main函数调用,方便后面再把这个功能转移到lib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
use std::fs;

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

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

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

let contents = fs::read_to_string(filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

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

组合参数值

上面的代码我们选择返回一个元组,又把一个元组拆分成两个变量,这还是有点意义不明,不方便使用,所以我们选择把这两个值放到一个结构体里,再用两个明确的变量名存储这两个参数,使得这个函数的返回值更加明确。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::env;
use std::fs;

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

let config = parse_config(&args);

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

let contents = fs::read_to_string(config.filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

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

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

这里,我们定义了一个Config结构体来存储我们的配置值,同时,秉持着结构体最好还是持有自己变量所有权的理念,这里我们需要把字符串clone一下再存入config变量,这种做法虽然比起直接使用引用多用了一些时间和内存,但也省去写生命周期的麻烦,也增加了代码可读性,毕竟开发效率也是效率,该做的牺牲还得做。

可见,现在main函数里解析参数,使用参数的逻辑已经非常清晰了。

有人对运行时代价很敏感,就是不喜欢用clone解决所有权问题,这个后面在13章会学习一些其他方法更有效率的处理这种情形。但对于我们现在来说,clone是一点没有问题的,因为我们这里的字符串只有读取程序参数的时候用一次clone,而且还只是两个很短的字符串,这点性能真算不上浪费。

为Config创建一个构造器

这时候我们再回头看一下,其实parse_config这个函数和我们的Config结构体也是紧密相关的,因为parse_config的目的就是构造一个Config结构体变量,那我们为什么不在Config结构体里写一个构造函数来实现同样的功能,增强这个函数和Config结构体的相关性,更好理解呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use std::env;
use std::fs;

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

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

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

let contents = fs::read_to_string(config.filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

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

impl Config {
fn new(args: &Vec<String>) -> Config{
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}

于是我们的代码变成了上面这样,使用Config::new代替了之前的parse_config,并顺利运行。

分离主体逻辑

这里我们的项目主体运行逻辑就是打开文件并搜索字符串,目前我们只写了打开文件,就先把这部分拆出来,写到run函数里

1
2
3
4
5
fn run(config: Config){
let contents = fs::read_to_string(config.filename)
.expect("Error in reading file");
println!("With text:\n{}", contents);
}

错误处理

依照计划,现在我们开始修复一下错误处理相关的逻辑

改进错误提示信息

首先,第一个能想到的错误就是,用户输入的参数不够,例如没有输入参数时,程序会报错“index out of bounds: the len is 1 but the index is 1”,但这个错误会让用户不知所云,所以我们需要完善一下报错提示:

1
2
3
4
5
6
7
8
9
10
impl Config {
fn new(args: &Vec<String>) -> Config{
if args.len() < 3 {
panic!("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Config{query, filename}
}
}

在参数数量少于3个的时候,我们主动抛出panic,提示参数不够,这时的报错信息就更人性化了。

返回Result而不是直接Panic

对于函数里的错误,使用Result作为返回结果,让调用函数的用户决定是否panic的方法会更加友好一点。所以我们再修改一下这个new函数,让它在运行成功时返回一个Config变量,在失败时返回报错信息

1
2
3
4
5
6
7
8
9
10
impl Config {
fn new(args: &Vec<String>) -> Result<Config, &str>{
if args.len() < 3 {
return Err("Not enough arguments")
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config{query, filename})
}
}

因为修改了返回值,所以我们还需要对应修改一下main函数的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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!("Problem parsing args: {}", err);
process::exit(1);
});

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

let contents = fs::read_to_string(config.filename).expect("Error in reading file");
println!("With text:\n{}", contents)
}

在以上代码里

  1. 我们新use了一个process库用来退出程序
  2. 我们使用了之前没用过的错误处理函数unwrap_or_else,其大致工作内容也很简单,主要就是捕获错误信息,把报错信息传入闭包err中,作为参数传递进下面的花括号(匿名函数)里,运行花括号的代码。如果没有报错,则自动获取Ok里存储的变量返回。大致就是,和unwrap类似,不过多了一个可以执行额外错误处理代码的功能

现在我们再来跑一个错误的命令,来看看报错提示

1
2
3
4
5
cargo run             
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe`
Problem parsing args: Not enough arguments
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)

完美,提示简短且易懂。

此外,对于run函数我们也作同样的操作;

1
2
3
4
5
6
7
use std::error::Error;

fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}

对于run,我们使用了一种新的处理方式->问号表达式,之前学过,这可以自动为我们在有错误的时候返回错误,减少代码的编写量;然后我们use了一个新的东西std::error::Error,并且使用了一个叫做Box的trait,指定返回的值是一个Error的trait,同时使用了dyn关键词;这里的主要作用就是,因为问号表达式返回的错误类型不是我们容易知道的,所以使用trait的方法可以方便的,动态的帮我们捕获不同的错误类型并进行返回。最后,因为run函数不需要返回什么值,所以只需要Ok里包含空元组就可以了。

同样的,最后再修改一下main函数,处理返回的Result即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let args: Vec<String> = env::args().collect();

let config = Config::new(&args).unwrap_or_else(|err|{
println!("Problem parsing args: {}", 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);
}
}

这里我们也使用了一种新的处理方式,也就是在6. 枚举与模式匹配里学到的if let语句,用于匹配枚举类型中的其中一种情况。这里因为我们的run函数必定返回一个空元组,所以我们没必要通过unwrap_or_else提取这个空元组,所以简单对Err这种情况进行处理即可。

当然,语言是很灵活的,我们也可以选择使用match等语句处理我们的报错。

把代码分离成独立的包

代码基本已经分离完成了,现在我们只需要把分离的代码放到lib.rs里即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
use std::fs;
use std::error::Error;

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

impl Config {
pub fn new(args: &Vec<String>) -> Result<Config, &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>>{
let contents = fs::read_to_string(config.filename)?;
println!("With text:\n{}", contents);
Ok(())
}

别忘了声明接口为pub,让外部可以调用~以及相关的use语句也要搬过来。

搬迁完毕,接下来只要到main.rs里把我们的库use进来即可,或者不用use,直接使用顶层包进行绝对路径访问也没问题,详见7. 包、单元包和模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use std::env;
use std::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 args: {}", 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);
}
}

这里的Config就是通过use导入,run函数因为处于顶层,所以使用我们顶层包名minigrep直接调用也很方便(如果你创建项目的时候名字不是minigrep记得改成你的项目名字)

运行通过!我们的基础功能就基本完成了,后面再把用于测试用的println语句删除就好。

好了,以上代码基本结合了我们学习的大部分内容了,用模块化的思路设计完基本功能逻辑完成后,接下来就该结合一下我们测试的知识了

使用测试驱动开发来编写库功能

软件主要功能是搜索文件里的字符串,那还差一个搜索的功能。本小节就主要讲的是按照测试驱动开发(TDD)的流程来开发搜索的逻辑。大概是以下步骤

  1. 编写一个必然失败的测试,运行测试,确保它一定失败
  2. 编写或修改刚好足够多的代码,让测试通过
  3. 在保证测试通过的前提下,重构代码
  4. 回到步骤1,进行开发

这只是其中一种开发技术,主要思想是:优先编写测试,再编写能通过测试的代码,有助于开发过程中保持较高的测试覆盖率。

接下来,我们就开始第一步

编写一个会失败的测试

lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mod test{
use super::*;

#[test]
fn one_result(){
let query = "duct";
let content = "
Rust:
safe, fast, productive.
Pick three.";

assert_eq!(vec!["safe, fast, productive."], search(query, content));
}
}

这里我们编写了一个测试模块,给出了测试用的查询字符串,以及内容字符串,再调用了一下实现搜索的功能函数,断言搜索的结果是一个vec数组;

现在我们还没实现search功能,所以后面就来实现一下。但因为我们首先要编写一个必然失败的测试,所以我们先来试试写一个函数,必然返回空数组,这样就和断言的结果不同,导致失败。

1
2
3
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{
vec![]
}

这里因为我们选择传入字符串的引用,所以需要使用生命周期,这里返回的值主要存的是contents里的部分内容,所以只需要在contents和返回值里标上生命周期即可,query并不需要

接下来我们用cargo test运行一下测试吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
running 1 test
test test::one_result ... FAILED

failures:

---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left == right)`
left: `["safe, fast, productive."]`,
right: `[]`', src\lib.rs:41:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
test::one_result

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

输出测试失败,左右不相等,一切按我们计划的在走,那接下来就是第二步了,编写或修改代码,让测试通过。

编写可以通过测试的代码

接下来我们按照正常的思路实验一下search函数的功能:

  1. 遍历内容的每一行
  2. 搜索行里是否包含搜索的字符串
  3. 如果包含,就把这行添加到列表里;否则忽略
  4. 返回列表
1
2
3
4
5
6
7
8
9
pub fn search<'a>(query: & str, contents: &'a str) -> Vec<&'a str>{
let mut ret = Vec::new();
for eachline in contents.lines() {
if eachline.contains(query){
ret.push(eachline);
}
}
ret
}

大概解释一下:

  1. new了一个可变的动态列表ret,用来存返回值
  2. 用for循环遍历contents的每一行,lines()函数可以把字符串分割成若干行,返回一个迭代器
  3. 使用contains函数判断query是否是eachline的子串,如果是,push到ret里
  4. 返回ret

最后再运行一下测试,不出意外就通过了。

把search集成到run函数内

上面已经把所有要写的都写完啦,可以集成功能并试着运行了

1
2
3
4
5
6
7
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
for (i, line) in search(&config.query, &contents).iter().enumerate(){
println!("{}: {}\n", i, line);
}
Ok(())
}

也简单解释一下:

  1. 通过for循环遍历所有的搜索结果
  2. 通过.iter().enumerate()让迭代器生成的结果附带上当前的循环次数
  3. 输出搜索的结果

接下来就通过我们熟悉的命令行参数运行程序吧:cargo run frog poem.txt

1
2
3
4
5
cargo run frog poem.txt
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target\debug\minigrep.exe frog poem.txt`
0: How public, like a frog

完美。

处理环境变量

如果有这么一个参数,例如是否开启不分大小写搜索,那每次用户启动的时候都需要通过命令行来输入一串指令开启就有点太麻烦了。这一部分我们就基于此优化一下我们的代码,主要依赖的就是环境变量

同样基于TDD的计划流程来编写功能。

编写一个必然失败的不区分大小写搜索测试

首先是写测试,就不解释了,主要是实现了两个测试,一个大小写敏感和一个大小写不敏感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#[cfg(test)]
mod test{
use super::*;

#[test]
fn one_result(){
let query = "duct";
let content = "
Rust:
safe, fast, productive.
Pick three.";

assert_eq!(vec!["safe, fast, productive."], search(query, content));
}

#[test]
fn case_sensitive(){
let query = "duct";
let content = "
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

assert_eq!(vec!["safe, fast, productive."], search(query, content));
}

#[test]
fn case_insensitive(){
let query = "rUst";
let content = "
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(vec!["Rust:", "Trust me."], search_case_insensitive(query, content));
}

}

这里省略掉实现search_case_insensitive返回空数组,导致测试失败的部分。可以自己试试,观察是否确保测试失败。

编写能通过测试的代码

然后是实现大小写不敏感的搜索功能函数,所谓大小写不敏感,其实就是把所有字母转成小写/大小再来作比较。

1
2
3
4
5
6
7
8
9
10
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str>{
let mut ret = Vec::new();
let query = query.to_lowercase();
for eachline in contents.lines() {
if eachline.to_lowercase().contains(&query){
ret.push(eachline);
}
}
ret
}

以上代码需要注意的是:因为to_lowercase函数把字符串原来的数据更改了,所以会创建一个新的字符串存储新的数据,所以这里我们的query变成了拥有自己所有权的String变量,而不再是原来的字符串切片,所以后续传入contains时又使用了&来借用。

运行测试通过的话,就没有问题了。

把功能搬迁到run函数内

因为用户自行决定是否开启大小写敏感的功能,所以要新增一个配置项来存储这个结果,所以我们修改一个Config结构

1
2
3
4
5
pub struct Config{
pub query: String,
pub filename: String,
pub is_sensitive: bool,
}

接下来,再结合is_sensitive把search_case_insensitive的功能搬到run函数里就差不多了

1
2
3
4
5
6
7
8
9
10
11
12
13
pub fn run(config: Config) -> Result<(), Box<dyn Error>>{
let contents = fs::read_to_string(config.filename)?;
let result = if config.is_sensitive{
search(&config.query, &contents)
}
else{
search_case_insensitive(&config.query, &contents)
};
for (i, line) in result.iter().enumerate(){
println!("{}: {}\n", i, line);
}
Ok(())
}

不过这段代码还不能编译通过,因为我们还没修改Config的new函数。这里因为我们不希望通过之前的命令行参数解析的方法来得到is_sensitive的值,所以我们要使用新的方法——读取环境变量。

环境变量可以让一个参数的值再整个会话内都有效,就不需要每次运行都输入一遍了,很方便。

1
2
3
4
5
6
7
8
9
10
11
impl Config {
pub fn new(args: &Vec<String>) -> Result<Config, &str>{
if args.len() < 3 {
return Err("Not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let is_sensitive = !(env::var("SENSITIVE").is_err());
Ok(Config{query, filename, is_sensitive})
}
}
  1. 我们新use了env,用来读取环境变量
  2. 添加了新的一行,通过env的var函数,读取名为“SENSITIVE”的环境变量
  3. 用Result的is_err()来处理报错,因为我们这里的is_sensitive就是一个bool值,is_err也是返回一个布尔值,正确读取环境变量的时候返回False,读取失败的时候返回True,用取反之后整好对上我们想实现的逻辑,也就是设置了环境变量的时候,则为True大小写敏感,否则False大小写不敏感。因为我们不需要关注环境变量的值,只想知道有没有。

写完之后,我们就来运行一下看看吧

1
2
3
4
5
6
7
8
9
10
cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe to poem.txt`
0: Are you nobody, too?

1: How dreary to be somebody!

2: To tell your name the livelong day

3: To an admiring bog!

我们在poem.txt里搜索to,结果没有问题,大小写不敏感,搜索出了to和To的结果

接下来,我们设置一下环境变量,这对于每个系统方法都不一样,这里只说一下windows的方法,在终端里输入以下语句

1
$env:SENSITIVE=1

然后再运行代码

1
2
3
4
5
6
7
cargo run to poem.txt  
Compiling minigrep v0.1.0 (E:\Code\rust\minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.41s
Running `target\debug\minigrep.exe to poem.txt`
0: Are you nobody, too?

1: How dreary to be somebody!

也没有问题,只搜索出小写的to结果

现在,这一部分也完成了。

将错误提示信息打印到标准错误而不是标准输出

println是输出到标准输出流,也就是打印到屏幕上,另一种输出流是标准错误流,会将正常的输出保存到文件里,错误的输出依旧打印到屏幕上。

我觉得这部分不重要,就简单放代码了。

println改成eprintln即可输出到标准错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::env;
use std::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);
});

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

运行错误指令,并指定输出文件,查看输出

1
2
3
4
5
cargo run > output.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe`
Problem parsing arguments: Not enough arguments
error: process didn't exit successfully: `target\debug\minigrep.exe` (exit code: 1)

可以看见错误信息打印到了屏幕上,而output.txt没有内容

运行正确指令,查看输出

1
2
3
cargo run to poem.txt > output.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target\debug\minigrep.exe to poem.txt

可以发现终端没有任何内容输出,输出搜索结果都在output.txt里

总结

这章主要还是复习以前的内容吧,大概就是

  1. 结合基本的概念,如所有权,生命周期等编写基本逻辑
  2. 如何使用函数,结构体,模块等功能结构化管理程序逻辑
  3. 如何捕获并正确的处理程序错误
  4. 如何编写测试模块
  5. 一些乱七八糟的命令行处理等
  • 标题: 【Rust 学习记录】12. 编写一个命令行程序
  • 作者: TwoSix
  • 创建于 : 2023-05-12 21:34:53
  • 更新于 : 2024-07-04 23:52:28
  • 链接: https://twosix.page/2023/05/12/【Rust-学习记录】12-编写一个命令行程序/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论