关于rust:Rust-开发命令行工具上

3次阅读

共计 17579 个字符,预计需要花费 44 分钟才能阅读完成。

你必须按所想去生存,否则只能按生存去想。– 王小波

大家好,我是 柒八九

作为一个 前端 /Rust/AI 常识博主,之前的文章中,大部分篇幅都是对于前端的常识分享,而对 RustAI的内容只是做了几篇内容梳理和介绍。

而,咱们今后的重心也会逐步偏移,势必能达到 前端 /Rust/AI 三足鼎立 的场面。

这里也和很多 精力股东 做一次简短的汇报,之前许可大家多出一些 Rust 相干的文章,因为工作和集体事务强占大部分 学习和总结 的工夫,所以迟迟没有兑现承诺。也很感激大部分 老粉 能不离不弃,在这里先叩谢大家了。

你们的反对也是我输出内容的 精神支柱 ,同时也很感激有些 远在天南地北 的敌人,不停的给出倡议和改良意见,Last but not least, 因为有些技术能力的无限,在一些表达方式和技术深度方向上,有很多瑕疵。也心愿当前大家,互相学习,共同进步。

好了,预计大家不想听我在这里一个人聒噪了,那么就进入咱们明天的主题。


前言

在上一篇致所有渴望学习 Rust 的人的信中咱们介绍了 Rust 能够在命令行工具上也大有建树。

当初就是咱们兑现承诺的时候了。

Rust是一种 动态编译 的、疾速的语言,具备杰出的工具反对和迅速增长的生态系统。这使它非常适合编写命令行应用程序。

通过编写具备简略 CLI 的程序,对于那些初学者来说是一个很好的练习,也是咱们须要 循序渐进 的一个过程。毕竟,大家刚开始接触一个新的语言都是从 Hello World 的动手的,然而这种 Demo 级别的程序,能够说是闭门造车,没有任何的理论价值。并且这种程序是 难登大雅之堂 的。

所以,咱们明天来通过一个简略的 CLI 来坚固之前的内容,并且写出的货色也能够在公司利用场景中有用武之地。

所以说抉择很重要,咱们不要成为他人口中说的 你之所以穷,是因为你不够致力 的人。

咱们在解说代码中,有一些根底语法会一带而过,也就是说,曾经默认大家曾经有 Rust 根底了。如果,你是一个 Rust 初学者,咱们也提供了 Rust 学习笔记系列,能够疾速把握根底语法。当然,外面的有一些内容也会做一些简略的梳理和解说。这个就因人而异了,看大家理论状况吧。

因为篇幅的起因,咱们打算写三篇文章(上 / 中 / 下 ),来介绍如何用Rust 来编写属于本人的命令行工具。明天是第一篇文章,咱们次要的目标是用 Rust 写出一个可用的命令行工具。属于本地利用级别,当初先不要 不屑一顾,咱们前面的 2 篇文章,会逐渐优化这个我的项目,而后达到最初发版供他人应用的级别。


你能所学到的知识点

  1. 前置知识点
  2. 我的项目设置
  3. 解析命令行参数
  4. 解析文件内容
  5. 更人性化的错误报告
  6. 信息输入解决
  7. 代码展现 (这个狠重要) 👈 徐志胜语音包

好了,天不早了,干点闲事哇。


1. 前置知识点

前置知识点 ,只是做一个概念的介绍,不会做深度解释。因为,这些概念在上面文章中会有呈现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到后面来。 如果大家对这些概念相熟,能够间接疏忽

同时,因为浏览我文章的群体有很多,所以有些知识点可能 我视之若瑰宝,尔视只如草芥,弃之如敝履 。以下知识点,请 酌情应用

grep 简介

grep 是一个罕用的命令行工具,用于在文本文件中搜寻指定的 文本模式 返回匹配的行 。其名称来源于 global regular expression print(全局正则表达式打印),它最后是在UNIX 操作系统中开发的,当初曾经成为大多数 Unix-like 零碎(包含Linux)的规范工具之一。grep 的次要性能是查找文件中蕴含特定文本的行,并将这些行打印到规范输入(通常是终端)上。

以下是 grep 命令的根本语法:

grep [选项] 模式 [文件...]
  • 选项:能够是一些管制搜寻行为的可选标记,例如 -i(疏忽大小写)、-r(递归搜寻目录)、-l(仅显示蕴含匹配项的文件名)等。
  • 模式:要搜寻的文本模式,通常应用正则表达式来指定。
  • 文件:要搜寻的文件列表。如果不指定文件,则 grep 将从规范输出中读取数据。

一些常见的 grep 用法示例:

  1. 在文件中搜寻特定字符串(不辨别大小写):

    grep -i "search_text" file.txt
  2. 在多个文件中递归搜寻特定字符串并显示蕴含匹配项的文件名:

    grep -r -l "search_text" directory/
  3. 应用正则表达式搜寻匹配模式:

    grep "pattern.*text" file.txt
  4. 统计匹配的行数:

    grep -c "pattern" file.txt

grep 是一个弱小的 文本搜寻工具,能够在各种状况下用于过滤、查找和解决文本数据。它的灵活性和正则表达式反对使得它在命令行中十分有用。


让咱们编写一个小型的相似 grep 的工具。给它起一个霸气侧漏的名称,那就叫它 – f789吧。

咱们能够在咱们本地,创立一个文件夹,作为我的项目的工作目录。(这就看集体爱好,自行决断了)

最终,咱们心愿可能像这样运行咱们的工具:

// 创立一个 text.txt 文件,并向其写入指定内容
echo "front:789" > text.txt
echo "province: 山西" >> text.txt
echo "rust: hello" >> text.txt


$ f789 rust test.txt
rust: hello
$ f789 --help
// 提供一些帮忙选项

本文中 rustc 采纳的是 1.72.0 (5680fa18f 2023-08-23) 的版本。并且在 Cargo.toml 文件的 [package] 局部中设置edition = "2021"

如果,版本不对 会有一些库的兼容性问题,所以最好大家在运行代码前,做一下代码配置和相干的解决。具体的配置和降级能够参考 Rust 环境配置和入门指南.

在应用对应命令降级之前,这里有一个小的提醒,如果你在 Mac 中应用 brew 装置过 Rust,你最好检测一下对应的版本信息。能够应用rustc --version 命令,会返回指定版本信息。例如:rustc 1.68.2 (9eb3afe9e 2023-03-27) (built from a source tarball)

然而,(built from a source tarball)这一部分示意 Rust 编译器不是通过二进制公布版装置的,而是从 Rust 源代码中编译生成的。这通常是因为咱们手动构建 Rust 或从源代码仓库中获取 Rust 的最新版本。这种状况的话,在应用 rustup update 进行版本更新的时候,会有问题。所以我举荐装置官网的二进制公布版。(也就是官网的解决形式)


2. 我的项目设置

如果你尚未装置Rust,能够参考咱们之前的文章 Rust 环境配置和入门指南。而后,关上一个终端并导航到咱们想要搁置利用程序代码的目录。

首先,在存储编程我的项目的目录中运行以下命令:cargo new f789。如果咱们查看新创建的 f789 目录,咱们将会找到一个典型的 Rust 我的项目设置:

咱们用 erdtree 进行页面构造展现。当然,咱们也能够用 tree 命令。所有的理所应当都是命运的暗中撮合 。因为erdtree 也是 Rust 写的。

  • 一个 Cargo.toml 文件,其中蕴含咱们我的项目的 元数据,包含咱们应用的依赖 / 内部库列表。
  • 一个 src/main.rs 文件,它是咱们二进制文件的 入口点

如果咱们能够在 f789 目录中执行 cargo run 并取得一个Hello World,那么咱们曾经设置好了。

我的项目运行

$ cargo new f789
     Created binary (application) `f789` package
$ cd f789/
$ cargo run
   Compiling f789 v0.1.0 (我的项目存储门路)
    Finished dev [unoptimized + debuginfo] target(s) in 0.70s
     Running `target/debug/f789`
Hello, world!


3. 解析命令行参数

个别的 CLI 都反对参数的输出:例如 tree -a -L 2 或者咱们之前的erd -i -I -L 2 -y inverted

咱们也想让咱们的 CLI 具备这个性能:

$ f789 front test.txt

咱们冀望咱们的程序查看 test.txt 并打印出蕴含 front 的行。然而咱们如何获取这两个值呢?

程序名称前面的文本通常被称为 命令行参数 命令行标记 (特地是当它们看起来像-- 这样时)。

在操作系统外部通常将它们示意为 字符串列表 – 简而言之,它们由空格分隔。

有许多办法能够探查和辨认这些参数,以及如何将它们解析成更容易解决的模式。咱们还须要通知应用咱们程序的用户须要提供哪些参数以及它们冀望的格局是什么。


取得参数

规范库 中蕴含了函数 std::env::args(),它提供了给定参数的迭代器。第一项( 索引为 0 )是咱们程序被调用的名称(例如,f789),其后的项是用户在前面写的内容。

通过这种形式获取原始参数非常容易(在文件 src/main.rs 中,在 fn main() { 之后):

let pattern = std::env::args().nth(1).expect("未提供模式");
let path = std::env::args().nth(2).expect("未提供门路");

这里,pattern将蕴含用户输出的第一个参数,path将蕴含用户输出的第二个参数。如果用户没有提供这些参数,程序将会报错并显示相应的谬误音讯。


将 CLI 参数自定义数据类型

与将 CLI 参数视为一堆文本相比,将其视为示意程序输出的 自定义数据类型 通常更有帮忙。

看看 f789 front test.txt:有两个参数,首先是 模式 (要查找的字符串),而后是 门路(要查找的文件)。

此外还有其它须要留神的点?首先,它们都是必须的。咱们还没有探讨默认值,因而咱们冀望用户始终提供两个值。此外,咱们还能够谈谈它们的类型:模式应该是一个 字符串 ,而第二个参数应该是 文件的门路

Rust 中,通常以解决的数据为核心来构建程序,因而以这种形式对待 CLI 参数十分适合。让咱们做一层数据抽象(在文件 src/main.rs 中,在 fn main() { 之前):

struct Cli {
    pattern: String,
    path: std::path::PathBuf,
}

这定义了一个新的构造体(struct),它有两个字段来存储数据:patternpath

留神:PathBuf相似于String,但用于跨平台的文件系统门路。

当初,咱们须要将咱们的程序接管到的理论参数转换为这种模式。一种选项是 手动解析 操作系统获取的字符串列表并本人构建构造。代码可能如下所示:

let pattern = std::env::args().nth(1).expect("未提供模式");
let path = std::env::args().nth(2).expect("未提供门路");
let args = Cli {
    pattern: pattern,
    path: std::path::PathBuf::from(path),
};

这种办法是可行的,但不够不便。下面的形式无奈满足,用户 天马行空 的创造力。例如:遇到相似 --pattern="front"--pattern "front"--help 的参数模式下面的代码就 顾此失彼 了。

也就是说,下面的代码不够优雅。


应用 Clap 解析 CLI 参数

站在伟人的肩膀上,你会看的更高。是不是很相熟的名言警句,是否勾起你儿时那种贴满走廊的校园回顾。

咱们能够应用他人写好的工具库。而用于解析命令行参数的最风行库称为 clap。它具备咱们所冀望的所有性能,包含反对子命令、Shell 主动实现以及杰出的帮忙音讯。

首先,通过将 clap = {version = "4.0", features = ["derive"] } 增加到咱们的 Cargo.toml 文件的 [dependencies] 局部来导入clap

[dependencies]
clap = {version = "4.4.2", features = ["derive"] }

当初,咱们能够在代码中应用 use clap::Parser;,并在咱们的struct Cli 上方增加#[derive(Parser)]。让咱们还顺便写一些文档正文。

代码看起来像这样(在文件 src/main.rs 中,在 fn main() { 之前):

use clap::Parser;

/// 在文件中搜寻模式并显示蕴含它的行。#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件的门路
    path: std::path::PathBuf,
}

简略解释其中的要害局部:

  1. use clap::Parser;: 这是导入 clap 库中的 Parser trait,它用于定义命令行参数和解析命令行输出。
  2. #[derive(Parser)]: 这是一个自定义属性(attribute),用于主动实现 Parser trait。通过这个属性,咱们能够在构造体上应用 Parser 的性能,使其成为一个能够解析命令行参数的类型。

通过应用 clap 库中的 Parser trait,咱们能够轻松地为咱们的命令行工具定义参数和解析用户提供的命令行输出。这有助于使命令行工具更加灵便和易于应用,同时提供了主动生成帮忙文档和解析命令行参数的性能。

对于 trait 能够参考咱们之前的 Rust 泛型、trait 与生命周期中的内容

留神:咱们能够在字段上增加许多自定义属性。例如,要示意咱们心愿将此字段用作 -o--output之后的参数,咱们能够增加#[arg(short = 'o', long = "output")]。无关更多信息,请参阅 clap 文档。

Cli 构造体下方,咱们的模板蕴含了其main 函数。当程序启动时,将调用此函数。第一行是:

fn main() {let args = Cli::parse();
}

这将尝试将参数解析为咱们的 Cli 构造。

但如果失败怎么办?这就是这种办法的美好之处:Clap晓得冀望哪些字段以及它们的预期格局。它能够主动生成丑陋的 --help 音讯,并提供一些杰出的谬误提醒,以倡议咱们在写 --putput 时传递--output


代码实操

咱们的代码当初应该如下所示:

#![allow(unused)]

use clap::Parser;

/// 在文件中搜寻模式并显示蕴含它的行。#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件的门路
    path: std::path::PathBuf,
}

fn main() {let args = Cli::parse();
}

在没有任何参数的状况下运行它:

$ cargo run
   Compiling f789 v0.1.0 (/Users/xxxx/RustWorkSpace/cli/f789)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/f789`
error: the following required arguments were not provided:
  <PATTERN>
  <PATH>

Usage: f789 <PATTERN> <PATH>

For more information, try '--help'.

咱们能够在应用 cargo run 时通过在 -- 前面写参数来传递参数:

$ cargo run -- some-pattern some-file
    Finished dev [unoptimized + debuginfo] target(s) in 0.09s
     Running `target/debug/f789 some-pattern some-file`

如咱们所见,没有输入。这是坏事:这意味着没有谬误,咱们的程序曾经完结。


4. 解析文件内容

利用 Clap 进行参数解决后,咱们轻而易举能够获取到用户输出数据。能够实现 f789 的外部逻辑了。咱们的 main 函数当初只蕴含以下这行代码:

let args = Cli::parse();

接下来,咱们逐步完善咱们的外部逻辑,当初从关上咱们失去的文件开始:

let content = std::fs::read_to_string(&args.path).expect("无奈读取文件");

留神:看到这里的 .expect 办法了吗?这是一个疾速退出的快捷函数,当值(在这种状况下是输出文件)无奈读取时,它会立刻使程序退出。具体的应用状况,参看 Rust 错误处理。

而后,让咱们迭代每一行,并打印蕴含咱们模式的每一行:

for line in content.lines() {if line.contains(&args.pattern) {println!("{}", line);
    }
}

代码实操

咱们的代码当初应该如下所示:

#![allow(unused)]

use clap::Parser;

/// 在文件中搜寻模式并显示蕴含它的行。#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件的门路
    path: std::path::PathBuf,
}

fn main() {let args = Cli::parse();
    let content = std::fs::read_to_string(&args.path).expect("无奈读取文件");

    for line in content.lines() {if line.contains(&args.pattern) {println!("{}", line);
        }
    }
}

试一试:cargo run -- main src/main.rs 当初应该能够工作了!

下面的代码,尽管能满足咱们的业务需要,然而还不够完满。有一个弊病:它会将整个文件读入内存 – 无论文件有多大。如果咱们想在一个 硕大无朋 中搜寻咱们须要的内容,那就有点不爽了。

咱们能够应用 BufReader 来优化下面的代码:

#![allow(unused)]

use clap::Parser;
use std::io::{self, BufRead};
use std::fs::File;

/// 在文件中搜寻模式并显示蕴含它的行。#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件的门路
    path: std::path::PathBuf,
}

fn main() {let args = Cli::parse();

    // 关上文件并创立一个 BufReader 来逐行读取
    let file = File::open(&args.path).expect("无奈关上文件");
    let reader = io::BufReader::new(file);

    for line in reader.lines() {let line = line.expect("无奈读取行");
        if line.contains(&args.pattern) {println!("{}", line);
        }
    }
}

这个版本的代码应用 BufReader 来逐行读取文件,而不是一次性读取整个文件内容,这样能够更无效地解决大文件。BufReader 在外部缓冲读取的数据,以进步性能,并且适宜用于逐行解决文本文件。


5. 更人性化的错误报告

应用其它语言时候,咱们时刻会放心会存在莫名其妙的谬误,从而使得咱们自夸强壮的代码,变得一文不值。而 Rust 不一样,当应用 Rust 时,咱们能够释怀的去写相干逻辑。因为 它没有异样,所有可能的谬误状态通常都编码在函数的返回类型中

Result

read_to_string 这样的函数不会返回一个字符串。相同,它返回一个 Result,其中蕴含一个String 或某种类型的谬误(在这种状况下是std::io::Error)。

Result是一个 枚举 ,咱们能够应用match 来查看它是哪个变体:

let result = std::fs::read_to_string("test.txt");
match result {Ok(content) => {println!("文件内容: {}", content); }
    Err(error) => {println!("出错了: {}", error); }
}

想理解 Rust 中枚举和它如何工作的,能够参考 Rust 枚举和匹配模式。

Unwrapping

当初,咱们曾经可能拜访文件的内容,但实际上咱们无奈在 match 块之后对其进行任何操作。为此,咱们须要以某种形式处理错误状况。挑战在于 match 块的所有分支都须要 返回雷同类型的内容。但有一个奇妙的技巧能够绕过这个问题:

let result = std::fs::read_to_string("test.txt");
let content = match result {Ok(content) => {content},
    Err(error) => {panic!("无奈处理错误:{},在这里退出", error); }
};
println!("文件内容:{}", content);

match 块之后,咱们能够应用 content 中的 String。如果result 是一个谬误,String将不存在。但因为程序在达到应用 content 的中央之前会退出,所以没问题。

Rust 将谬误组合成两个次要类别:<span style=”font-weight:800;color:#FFA500;font-size:18px”>{可复原谬误 |recoverable}</span> 和 <span style=”font-weight:800;color:#FFA500;font-size:18px”>{不可复原谬误 |unrecoverable}</span>。

  • 可复原谬误 通常代表向用户报告谬误和重试操作是正当的状况,比方未找到文件
  • 不可复原谬误 通常是 bug 的同义词,比方尝试拜访超过数组结尾的地位。

    • Rustpanic! 宏。<span style=”font-weight:800;color:red;font-size:18px”> 当执行这个宏时,程序会打印出一个错误信息,开展并清理栈数据,而后接着退出 </span>

这可能看起来有点激进,但十分不便。如果咱们的程序须要读取该文件,如果文件不存在无奈执行任何操作,那么退出是一种无效的策略。甚至在 Result 上还有一个快捷办法,称为unwrap

let content = std::fs::read_to_string("test.txt").unwrap();

panic 的代替计划

当然,停止程序并不是处理错误的惟一办法。除了应用 panic! 之外,咱们也能够轻松地应用return

let result = std::fs::read_to_string("test.txt");
let content = match result {Ok(content) => {content},
    Err(error) => {return Err(error.into()); }
};

然而,这 扭转了咱们的函数须要的返回类型。所以,咱们须要解决一下函数签名。

以下是残缺示例:

fn main() -> Result<(), Box<dyn std::error::Error>> {let result = std::fs::read_to_string("test.txt");
    let content = match result {Ok(content) => {content},
        Err(error) => {return Err(error.into()); }
    };
    println!("文件内容:{}", content);
    Ok(())
}

咱们来简略对每行代码做一次解释:

  1. fn main() -> Result<(), Box<dyn std::error::Error>>: 这是程序的入口点 main 函数的签名。它返回一个 Result 类型,示意程序的执行后果。

    • Result 的胜利值是 (),示意胜利执行而没有返回值。
    • 谬误值是一个包装了实现了 std::error::Error trait 的谬误对象的 Box
  2. let result = std::fs::read_to_string("test.txt");: 这行代码尝试关上并读取文件 “test.txt” 的内容。它应用了规范库中的 std::fs::read_to_string 函数,该函数返回一个 Result<String, std::io::Error>,示意读取文件内容的后果。
  3. let content = match result {...}: 这是一个模式匹配语句,用于解决文件读取的后果 result

    • 如果读取胜利 (Ok(content)),则将读取的内容存储在 content 变量中。
    • 如果读取失败 (Err(error)),则将谬误转换为 Result,并将其返回作为程序的谬误后果。
  4. println!("文件内容:{}", content);: 如果胜利读取文件内容,程序将打印文件的内容到规范输入,应用 {} 占位符来插入 content 变量的值。
  5. Ok(()): 最初,程序返回一个胜利的 Result,示意程序执行胜利。

留神:为什么这不写作 return Ok(());?它齐全能够这样写, 这也是齐全无效的。在Rust 中,任何块的最初一个表达式都是它的返回值,习惯上省略不必要的返回。


? 操作

就像调用 .unwrap() 是与 panic! 在谬误分支中的匹配的快捷方式一样,咱们还有另一个与在谬误分支返回的匹配的快捷方式:?

你没有看错,就是一个 问号 。咱们能够将此操作符附加到Result 类型的值上,Rust 将在外部将其扩大为与咱们刚刚编写的 match 十分类似的货色

能够将对应的代码局部改成如下格局:

fn main() -> Result<(), Box<dyn std::error::Error>> {let content = std::fs::read_to_string("test.txt")?;
    println!("文件内容:{}", content);
    Ok(())
}

难道这就是传说中,从天而降的掌法嘛。这也太丝滑了。

这里有一些 Rust 开发中的 潜规则 。例如,咱们main 函数中的谬误类型是 Box<dyn std::error::Error>。然而咱们曾经看到read_to_string 返回的是 std::io::Error。这是因为? 扩大为转换谬误类型的代码。

同时,Box<dyn std::error::Error>也是一个乏味的类型。它是一个 Box,能够蕴含任何实现规范Error trait 的类型。这意味着基本上所有谬误都能够放入这个 Box 中,因而咱们能够在所有通常返回 Result 的函数上应用?

无关 Box 的应用原理和介绍能够参考 Rust 智能指针


为谬误提供适合的语境提醒

应用 ? 在主函数中时,失去的谬误是能够承受的,但不是很好。例如:当咱们运行 std::fs::read_to_string("test.txt")? 但文件 test.txt 不存在时,咱们会失去以下输入:

Error: Os {code: 2, kind: NotFound, message: "No such file or directory"}

在代码中不蕴含文件名的状况下,很难确定哪个文件是NotFound。有多种解决形式。

创立本人的谬误类型

咱们能够创立本人的谬误类型,而后应用它来构建自定义谬误音讯:

#[derive(Debug)]
struct CustomError(String);

fn main() -> Result<(), CustomError> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("在读取 `{}` 时: {}", path, err)))?;
    println!("文件内容:{}", content);
    Ok(())
}

咱们来简略解释一下下面的代码

  1. #[derive(Debug)] struct CustomError(String);: 这个代码定义了一个自定义的谬误类型 CustomError,它蕴含一个字符串字段用于存储谬误音讯。#[derive(Debug)] 属性宏为这个构造体主动生成了 Debug trait 的实现,以便在打印谬误时更容易调试。
  2. fn main() -> Result<(), CustomError> {...}: 这是程序的入口点 main 函数的签名。与之前的代码不同,它返回一个 Result,其中胜利值是 (),示意胜利执行而没有返回值,谬误值是自定义谬误类型 CustomError
  3. let content = std::fs::read_to_string(path) ... ?;: 与之前的代码不同,这里应用了 map_err 办法来解决可能的谬误状况。

    • .map_err(|err| CustomError(format!("在读取 {} 时: {}", path, err))): 这部分应用 map_err 办法来解决可能的谬误状况。map_err 办法承受一个 闭包(匿名函数),该闭包承受一个谬误对象 err,并返回一个新的谬误对象。在这个闭包中,它将原始的 std::io::Error 谬误转换为自定义的 CustomError 谬误类型,并增加了一条蕴含错误信息的自定义谬误音讯。
    • ?: 这个问号 ?Rust 中的错误处理操作符。它用于解决 Result 类型的返回值。如果 Result 是一个 Ok,则 ? 不会执行任何操作,它会将胜利的值提取进去。如果 Result 是一个 Err,则 ? 会立刻将谬误返回给调用者,作为整个函数的返回值,就如同应用 return Err(...) 一样。

当初,运行这个程序将会失去咱们自定义的谬误音讯:

Error: CustomError("在读取 `test.txt` 时: No such file or directory (os error 2)")

尽管不太好看,但咱们能够稍后轻松调整咱们类型的调试输入。

应用 anyhow 库

下面的模式十分常见。但它有一个问题:咱们没有存储 原始谬误,只有它的字符串示意 。咱们能够应用 anyhow 库对此有一个奇妙的解决方案:与咱们的CustomError 类型相似,它的Context trait 能够用来增加形容。此外,它还保留了原始谬误,因而咱们失去一个指出根本原因的谬误音讯“链”。

首先,通过在 Cargo.toml 文件的 [dependencies] 局部增加 anyhow = "1.0.75" 来导入anyhow crate

而后,残缺的示例将如下所示:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("无奈读取文件 `{}`", path))?;
    println!("文件内容:{}", content);
    Ok(())
}

这将打印一个谬误:

Error: 无奈读取文件 `test.txt`

Caused by:
    No such file or directory (os error 2)

6. 信息输入解决

应用 println!

咱们简直能够应用 println! 宏 打印所有咱们喜爱的内容。这个宏具备一些十分惊人的性能,但也有非凡的语法。它心愿咱们 将一个字符串字面量作为第一个参数,该字符串蕴含占位符,这些占位符将由前面的参数的值作为进一步的参数填充

例如:

let x = 789;
println!("我的侥幸数字是 {}。", x);

将打印:

我的侥幸数字是 789。

上述字符串中的 花括号 {})是其中的一个 占位符 。这是默认的占位符类型,它尝试以人机敌对的形式打印给定的值。对于 数字和字符串 ,这个办法十分无效,但并不是所有类型都能够这样做。这就是为什么还有一种 调试模式(debug representation) —{:?}

例如:

let xs = vec![1, 2, 3];
println!("列表是:{:?}", xs);

将打印:

列表是:[1, 2, 3]

如果心愿咱们本人的数据类型可能用于调试和记录,大多数状况下能够在它们的 定义之上 增加#[derive(Debug)]

用户敌对 (User-friendly) 打印应用 Display trait 调试输入 (面向开发人员的输入)应用Debug trait。咱们能够在 std::fmt 模块的文档中找到无关能够在println! 中应用的语法的更多信息。


打印错误信息

通过 stderr 来打印谬误,以使用户和其余工具更容易将其输入重定向到文件或其余工具。

在大多数操作系统上,程序能够写入两个输入流,stdoutstderr

  • stdout用于程序的理论输入
  • stderr容许将谬误和其余音讯与 stdout 离开

这样,能够将输入存储到文件或将其管道传输到另一个程序,而谬误将显示给用户。

Rust 中,能够通过 println!eprintln!来实现这一点,前者打印到stdout,后者打印到stderr

println!("这是失常信息");
eprintln!("这是一个谬误!:(");

在打印 本义代码 时,会使用户的终端处于奇怪景象,所以,当解决原始本义代码时,应该应用像 ansi_term 这样的 crate 来使咱们的输入更加顺畅。


打印优化

向终端打印的速度出奇地慢!如果在循环中调用相似 println! 的函数,它可能成为程序运行的瓶颈。为了加快速度,有两件事件能够做。

1. 缩小写入次数

首先,咱们可能心愿缩小理论 刷新 到终端的写入次数。

println!在每次调用时都会通知零碎刷新到终端,因为通常会打印每一行。

如果咱们不须要这样做,能够将 stdout 句柄 包装在默认状况下 缓冲最多 8 KBBufWriter 中。(当咱们想立刻打印时,依然能够在此 BufWriter 上调用.flush()。)

use std::io::{self, Write};

let stdout = io::stdout(); // 获取全局 stdout 实体
let mut handle = io::BufWriter::new(stdout); // 可选:将该句柄包装在缓冲区中
writeln!(handle, "front: {}", 789); // 如果咱们关怀此处的谬误,请增加 `?`

2. 应用锁

其次,能够获取 stdout(或stderr)的锁,并应用writeln! 间接打印到它。这能够避免零碎一遍又一遍地锁定和解锁stdout

use std::io::{self, Write};

let stdout = io::stdout(); // 获取全局 stdout 实体
let mut handle = stdout.lock(); // 获取它的锁
writeln!(handle, "front: {}", 789); // 如果咱们关怀此处的谬误,请增加 `?`

咱们还能够联合两种办法。

具体代码如下:

use std::io::{self, Write};

fn main() -> io::Result<()> {let stdout = io::stdout(); // 获取全局 stdout 实体
    let stdout_lock = stdout.lock(); // 获取 stdout 的锁
    
    // 将锁包装在 BufWriter 中
    let mut handle = io::BufWriter::new(stdout_lock);
    
    writeln!(handle, "front: {}", 789)?; // 如果咱们关怀此处的谬误,请增加 `?`

    Ok(())
}

在这个示例中,首先获取了 stdout 的锁,而后将锁传递给 io::BufWriter,最初应用 writeln!handle 写入数据。


显示一个进度条

某些 CLI 运行工夫不到一秒,而其余一些可能须要几分钟或几小时。如果咱们正在编写后者类型的程序,咱们可能心愿向用户显示正在产生的事件。为此,咱们能够尝试打印有用的状态更新,最好以易于耗费的模式出现。

应用indicatif crate,咱们能够向咱们的程序增加进度条和小的旋转器。

在应用之前,咱们须要在 Cargo.toml 中引入对应的库。

[dependencies]
indicatif = {version = "*", features = ["rayon"] }

上面是应用 indicatif 的一个小示例。

fn main() {let pb = indicatif::ProgressBar::new(100);
    for i in 0..100 {do_hard_work();
        pb.println(format!("[+] 实现了第 #{}项", i));
        pb.inc(1);
    }
    pb.finish_with_message("工作实现");
}
fn do_hard_work() {
    use std::thread;
    use std::time::Duration;

    thread::sleep(Duration::from_millis(250));
}

无关更多信息,请参阅 indicatif 文档和示例。

日志

为了更容易了解程序中产生的状况,咱们可能想要增加一些日志语句。通常在编写应用程序时这很容易。但在半年后再次运行此程序时,日志将变得十分有帮忙。在某种程度上,日志记录与应用 println! 雷同,只是你能够指定音讯的重要性

通常能够应用的日志级别有 errorwarninfodebugtraceerror 优先级最高,trace 优先级最低)。

要向应用程序增加日志记录,你须要两样货色:

  1. log crate(其中蕴含了依据日志级别命名的宏)
  2. 一个理论将日志输入写到有用地位的适配器

因为咱们当初只关怀编写一个 CLI,一个易于应用的适配器是 env_logger。它被称为 env logger,因为你能够 应用环境变量来指定你想要记录的应用程序局部(以及你想要记录它们的级别)。它将在日志音讯前加上工夫戳和消息来源的模块。因为库也能够应用 log,因而咱们能够轻松配置它们的日志输入。

以下是简略示例:

配置Cargo.toml

[dependencies]
log = "0.4.20"
env_logger = "0.10.0"
use log::{info, warn};

fn main() {env_logger::init();
    info!("我的项目启动");
    warn!("这是一个正告信息");
}

假如你将此文件保留为 src/bin/output-log.rs,在 LinuxmacOS 上,你能够这样运行它:

$ env RUST_LOG=info cargo run --bin output-log

Windows PowerShell 中,你能够这样运行:

$ $env:RUST_LOG="info"
$ cargo run --bin output-log

Windows CMD 中,你能够这样运行:

$ set RUST_LOG=info
$ cargo run --bin output-log

下面的代码是在运行 Rust 我的项目中的二进制文件(通过指定 --bin 标记)并设置日志级别(通过 RUST_LOG 环境变量)。

针对次要的代码,做一下解释:

  1. env RUST_LOG=info: 这部分设置了一个环境变量 RUST_LOG,用于管制 Rust 我的项目中的日志记录级别。具体来说,它将日志级别设置为 info

    • Rust 我的项目通常应用日志库(例如 logenv_logger)来记录不同级别的日志音讯。
    • info 是一个中等具体的级别,它会记录一些有用的信息,但不会过于简短。你能够依据须要将日志级别设置为不同的值,如 debugwarnerror 等。
  2. --bin output-log: 这部分通知 cargo 运行我的项目中名为 output-log 的二进制文件。Rust 我的项目通常蕴含多个二进制文件,这个选项指定要运行的二进制文件的名称。output-log 应该是你的 Rust 我的项目中一个二进制文件的名称。

综合起来,这行代码的作用是设置日志级别为 info,而后运行 Rust 我的项目中名为 output-log 的二进制文件。这有助于管制日志记录的具体水平,并查看我的项目中的输入日志。如果你的 Rust 我的项目应用了日志库,并且在代码中有相应的日志记录语句,那么设置日志级别为 info 会让你看到 info 级别的日志音讯。


代码展现

咱们下面通过几节的内容,从 我的项目配置 / 参数获取 / 解析文件内容 / 处理错误信息 / 信息输入解决 等方面。能够构建出在本地,兼容谬误提醒,并且有很好的输入模式的本地搜寻工具。

让咱们就下面的内容,从代码上做一次梳理和汇总。

use anyhow::{Context, Result};
use clap::Parser;
use indicatif::ProgressBar;
use std::fs::File;
use std::io::{self, BufRead, Write};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;

/// 在文件中搜寻模式并显示蕴含它的行。#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件的门路
    path: PathBuf,
}

fn main() -> Result<()> {let args = Cli::parse();

    // 关上文件并创立一个 BufReader 来逐行读取
    let file = File::open(&args.path).with_context(|| format!("无奈关上文件 {:?}", &args.path))?;
    let reader = io::BufReader::new(file);

    let stdout = io::stdout();
    let stdout_lock = stdout.lock();
    let mut handle = io::BufWriter::new(stdout_lock);
    let pb = ProgressBar::new(100);
    for line in reader.lines() {do_hard_work();
        pb.println(format!("[+] 查找到了 #{:?}项", line));
        pb.inc(1);
        let line = line.with_context(|| "无奈读取行")?;
        if line.contains(&args.pattern) {writeln!(handle, "{}", line)?;
        }
    }

    Ok(())
}

fn do_hard_work() {thread::sleep(Duration::from_millis(250));
}

对应的 Cargo.toml 如下

[package]
name = "f789"
version = "0.1.0"
edition = "2021"


[dependencies]
clap = {version = "4.4.2", features = ["derive"] }
anyhow = "1.0.75"
indicatif = {version = "0.17.6", features = ["rayon"] }
log = "0.4.20"
env_logger = "0.10.0"

对应的运行后果如下:

在上文中咱们手动创立了一个 text.txt 文件。咱们只是创立了,没通知它搁置的地位。咱们将与 src 目录同级。

应用 erd -L 1 -y inverted 命令查看目录信息

Cargo会默认把 所有的源代码文件 保留到 src 目录下,而 我的项目根目录 只被用来存储诸如 README 文档/ 许可申明 / 配置文件等与源代码 无关 的文件。

如果,咱们想看针对大文件的解决形式,咱们能够新建一个更大的我的项目。用于做代码试验。


后记

分享是一种态度

全文完,既然看到这里了,如果感觉不错,顺手点个赞和“在看”吧。

正文完
 0