rust 的运行速度、安全性、单二进制文件输出和跨平台支持使其成为构建命令行程序的最佳选择。
实现一个命令行搜索工具grep,可以在指定文件中搜索指定的字符串。想实现这个功能呢,可以按照以下逻辑流程处理:
- 获取输入文件路径、需要搜索的字符串
- 读取文件;
- 在文件内容中查找字符串所在的行
- 打印包含字符串所在的行信息
创建项目ifun-grep
项目在运行时,可以获取到传递的参数。比如cargo run -- hboot hello.txt,在文件hello.txt查找字符串hboot
读取参数
首先要先获取到传入的参数。通过标准库std::env::args获取- use std::env;
- fn main() {
- let args: Vec<String> = env::args().collect();
- dbg!(args);
- }
复制代码 collect()方法可以将传入的参数转换为一个集合。对于变量args必须注明集合类型。

参数的第一个值是二进制文件的名称。可以用于程序调试或者打印出文件路径,取出另外两个参数,保存进对应的变量。方便后续传参数使用。- let search = &args[1];
- let file_path = &args[2];
- println!("will search {} in {}", search, file_path)
复制代码 读取文件
首先创建测试文件hello.txt,并写入一段文字。- 独立寒秋,湘江北去,橘子洲头。
- 看万山红遍,层林尽染;漫江碧透,百舸争流。
- 鹰击长空,鱼翔浅底,万类霜天竞自由。
- 怅寥廓,问苍茫大地,谁主沉浮?
- 携来百侣曾游,忆往昔峥嵘岁月稠。
- 恰同学少年,风华正茂;书生意气,挥斥方遒。
- 指点江山,激扬文字,粪土当年万户侯。
- 曾记否,到中流击水,浪遏飞舟
复制代码 读取文件,并打印出文件中的内容。- let content = fs::read_to_string(file_path).expect("you should permission to read the file");
- println!("read the content:\n{content}")
复制代码 通过fs模块的read_to_string方法读取文件内容。expect则用于处理读取文件时发生的错误的提示信息,这在下面的错误处理会有说明。
模块拆分与错误处理
现在所有的处理业务都放在src/main.rs中。取参和读取文件是两个不同功能的逻辑处理,当功能越来越复杂的时候,就应该关注分离。这在我们设计时可提前考虑好
main.rs只被用来处理程序的执行。其他需要处理的逻辑则可以放在srr/lib.rs中。
定义一个解析取参的函数parse_args,现在仍然定义在src/main.rs中。- fn parse_args(args: &Vec<String>) -> (&str, &str) {
- let search = &args[1];
- let file_path = &args[2];
- (search, file_path)
- }
- fn main(){
- let args: Vec<String> = env::args().collect();
- let (search, file_path) = parse_args(&args);
- println!("will search {} in {}", search, file_path);
- }
复制代码 这样main函数不再处理哪个参数对应哪个变量。
我们可以将这一组相关的变量通过结构体定义相互关联起来。这样函数返回将不再使用元组,并且可以通过结构体实例可以访问到每一个属性。- struct Config {
- search: String,
- file_path: String,
- }
- fn parse_args(args: &Vec<String>) -> Config {
- let search = args[1].clone();
- let file_path = args[2].clone();
- Config { search, file_path }
- }
- fn main(){
- let args: Vec<String> = env::args().collect();
- let config = parse_args(&args);
- println!("will search {} in {}", search, file_path);
- }
复制代码 在结构体中,实例化赋值需要拥有这些变量值的所有权。而变量args是所有权的拥有者,通过clone()方法拷贝一份数据。
可以看到parse_args返回来一个结构体 Config 的实例,可以通过定义结构体的内部方法来创建实例。- impl Config {
- fn new(args: &Vec<String>) -> Self {
- let search = args[1].clone();
- let file_path = args[2].clone();
- Config { search, file_path }
- }
- }
- fn main(){
- let args: Vec<String> = env::args().collect();
- let config = Config::new(&args);
- println!("will search {} in {}", search, file_path);
- }
复制代码 这样,就不需要parse_args函数了,通过结构体的内部方法实例化实例。
错误处理
如果我们执行cargo run时,不传递任何参数,则程序会报错。这样的提示对于用户并不友好。
首先可以通过判断参数需要的参数信息,说明错误信息。- impl Config {
- fn new(args: &Vec<String>) -> Self {
- if args.len() < 3 {
- panic!("至少传入2个参数")
- }
- // ...
- }
- }
复制代码 提示用户必须传入 2 个从参数,因为有一个默认的路径参数。所以判断不能少于3
除了直接提示错误信息并中断程序,也可以使用Result传递错误,让主函数做决定如何去处理。- impl Config {
- fn build(args: &Vec<String>) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("至少传入2个参数");
- }
- let search = args[1].clone();
- let file_path = args[2].clone();
- Ok(Config { search, file_path })
- }
- }
复制代码 可以看到通过pub将这些结构体、函数都公有化。包括结构里的字段,这就是一个可以测试的公有 API 的 crate 库。
然后再src/main.rs需要导入- use std::{env, fs, process};
- fn main(){
- let config = Config::build(&args).unwrap_or_else(|err| {
- println!("error occurred parseing args:{err}");
- process::exit(1);
- });
- // ...
- }
复制代码 通过use引入作用域。ifun-grep是项目名称,作为前缀。
增加测试
通过测试驱动开发的模式来逐渐增加逻辑。期望从给定的内容中查找出字符串,并打印出所在行。
在src/lib.rs增加测试示例- use std::error::Error;
- fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let content = fs::read_to_string(config.file_path)?;
- println!("read the content:\n{content}");
- Ok(())
- }
复制代码 搜索字符串hboot,它在文本的第二行。所以期待搜索输出结果为I'm hboot.。
提供一个find函数,用于处理搜索逻辑,先不写搜索逻辑,返回一个空的结果值。- fn main (){
- // ...
- if let Err(e) = run(config) {
- println!("something error:{e}");
- process::exit(1);
- }
- }
复制代码 env::var()返回值为 Result 类型,通过它自己的方法is_ok()判断什么状态,如果设置值则返回 true;未设置则返回 false。
进行测试,不设置变量时,查询小写的let是查询不到的,因为首写的因为单词字母是大些的。- use std::error::Error;
- use std::fs;
- pub struct Config {
- pub search: String,
- pub file_path: String,
- }
- impl Config {
- pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("至少传入2个参数");
- }
- let search = args[1].clone();
- let file_path = args[2].clone();
- Ok(Config { search, file_path })
- }
- }
- pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let content = fs::read_to_string(config.file_path)?;
- println!("read the content:\n{content}");
- Ok(())
- }
复制代码
通过设置环境变量,执行程序- use ifun_grep::{run, Config};
复制代码 可以查到目标文本内容。

错误信息处理
我们所预先知道的错误信息都通过程序执行println!打印在控制台,这是一种标准输出.
对于出现错误信息,希望它即时打印输出,而对于程序执行的结果记录下来,保存到文件中,方便查看。
现在使用println!标准输出流重定向到文件中,它会将错误信息也保存到起来,且不会打印。- #[cfg(test)]
- mod test {
- use super::*;
- #[test]
- fn on_result() {
- let search = "hboot";
- let content = "\
- nice. rust
- I'm hboot.
- hello world.
- ";
- assert_eq!(vec!["I'm hboot."], find(search, content));
- }
- }
复制代码 屏幕上没有任何输出,以为程序执行正常,其实文件中的内容是error occurred parseing args:至少传入2个参数。
这就造成了一个问题,不管成功、失败,只有打开文件才能看到。错误输出使用标准错误展示用于错误信息,将错误打印的println!改为eprintln!- pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
- vec![]
- }
复制代码 重新执行cargo run >output.txt,错误打印到控制台,而文件output.txt没有输出。
再执行,可以查到数据的命令cargo run -- Let hello.txt > output.txt,查看output.txt,可以看到预期的查找到的内容在文件中。
发布 crate 到Crate.io
crates.io 库,可以这里找找想要的功能库,也可以将自己的 crate 发布到这里。
Rust 的发布配置都有一套默认的、可定制的配置。
- cargo build 采用的是 dev 配置构建程序
- cargo build --release 是 release 配置,有更好的发布构建的配置
可以在文件Cargo.toml中通过[profile.*]修改设置默认值。- pub fn find<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
- let mut result = vec![];
- for line in content.lines() {
- if line.contains(search) {
- // 符合,包含了指定字符串
- result.push(line);
- }
- }
- result
- }
复制代码 dev 构建和发布构建定义不同的优化等级。opt-level定义何种程度优化,0-3可配置值。dev 默认为 0,release 默认为 3.
如果想 dev 模式下需要一些优化,则可以更改为- pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let content = fs::read_to_string(config.file_path)?;
- // println!("read the content:\n{content}");
- for line in find(&config.search, &content) {
- println!("{line}");
- }
- Ok(())
- }
复制代码 增加文档注释
一个好的模块包,是有很好的文档说明,以方便其他人轻易上手。通过文档注释///已支持 markdown 格式化文本。
给每一个函数增加注释说明,这里只展示部分。- Let life be beautiful like summer flowers.
- The world has kissed my soul with its pain.
- Eyes are raining for her.
- you also miss the stars.
复制代码 在使用 vscode 时,注释文档上方会有一个执行操作 run doctest。可以单独执行当前写的测试示例是否可以通过执行。
也可以通过cargo test来测试所有的测试示例。不仅会执行mod test的测试示例,也会执行doc test的注释测试示例。
通过命令cargo doc --open来生成在线文档。- $> cargo run -- let hello.txt
复制代码
可以通过//!对当前文件进行注释说明,必须是在第一行。- #[cfg(test)]
- mod test {
- use super::*;
- #[test]
- fn case_sensitive() {
- let search = "rust";
- let content = "\
- nice. rust
- I'm hboot.
- hello world.
- Rust
- ";
- assert_eq!(vec!["nice. rust"], find(search, content));
- }
- #[test]
- fn case_insensitive() {
- let search = "rust";
- let content = "\
- nice. rust
- I'm hboot.
- hello world.
- Rust
- ";
- assert_eq!(vec!["nice. rust", "Rust"], find_insensitive(search, content));
- }
- }
复制代码 注册 crate.io 账户并发布
目前只能使用 github 账号进行授权登录。在个人账号信息中,API Tokens生成 token 授权操作。- pub fn find_insensitive<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
- let mut result = vec![];
- // 搜索 字符串转小写
- let search = search.to_lowercase();
- for line in content.lines() {
- // 文本行内容转小写
- if line.to_lowercase().contains(&search) {
- // 符合,包含了指定字符串
- result.push(line);
- }
- }
- result
- }
复制代码 如果登录不成功,看下提示错误,我是加了参数--registry crates-io才成功的。- pub fn find_insensitive<'a>(search: &str, content: &'a str) -> Vec<&'a str> {
- let mut result = vec![];
- // 搜索 字符串转小写
- let search = search.to_lowercase();
- for line in content.lines() {
- // 文本行内容转小写
- if line.to_lowercase().contains(&search) {
- // 符合,包含了指定字符串
- result.push(line);
- }
- }
- result
- } --registry crates-io
复制代码 登录之后就可以发布了,通过Cargo.toml增加一些仓库元信息,比如仓库名、作者、开源协议、描述等等。- pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
- let content = fs::read_to_string(config.file_path)?;
- // println!("read the content:\n{content}");
- let mut result = vec![];
- if config.ignore_case {
- result = find_insensitive(&config.search, &content)
- } else {
- result = find(&config.search, &content)
- }
- for line in result {
- println!("{line}");
- }
- Ok(())
- }
复制代码 发布之前需要验证你登录的账号邮箱,不然发布不了。个人的元信息有几项是必填的,包括name\version\description\license
发布时,如果发布不成功,看错误提示,可能还需要加--registry crates-io
撤销某个版本
如果你发布的版本有很大的问题,可以撤销改版本。不能删除仓库,已发布的代码时永久存在的,只能通过撤销来阻止其他项目引用它。- impl Config {
- pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("至少传入2个参数");
- }
- let search = args[1].clone();
- let file_path = args[2].clone();
- let ignore_case = env::var("IGNORE_CASE").is_ok();
- Ok(Config {
- search,
- file_path,
- ignore_case,
- })
- }
- }
复制代码 使得当前版本不可用。也可以恢复当前版本的使用- impl Config {
- pub fn build(args: &Vec<String>) -> Result<Config, &'static str> {
- if args.len() < 3 {
- return Err("至少传入2个参数");
- }
- let search = args[1].clone();
- let file_path = args[2].clone();
- let ignore_case = env::var("IGNORE_CASE").is_ok();
- Ok(Config {
- search,
- file_path,
- ignore_case,
- })
- }
- } --undo
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |