Rust 开发命令行工具(上)(三)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 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。它被称为envlogger,因为你可以使用环境变量来指定你想要记录的应用程序部分(以及你想要记录它们的级别)。它将在日志消息前加上时间戳和消息来源的模块。由于库也可以使用 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 等。
  1. --bin output-log: 这部分告诉 cargo 运行项目中名为 output-log 的二进制文件。Rust 项目通常包含多个二进制文件,这个选项指定要运行的二进制文件的名称。output-log 应该是你的 Rust 项目中一个二进制文件的名称。

综合起来,这行代码的作用是设置日志级别为 info,然后运行 Rust 项目中名为 output-log 的二进制文件。这有助于控制日志记录的详细程度,并查看项目中的输出日志。如果你的 Rust 项目使用了日志库,并且在代码中有相应的日志记录语句,那么设置日志级别为 info 会让你看到 info 级别的日志消息。

image.png


代码展示

我们上面通过几节的内容,从项目配置/参数获取/解析文件内容/处理错误信息/信息输出处理等方面。可以构建出在本地,兼容错误提示,并且有很好的输出形式的本地搜索工具。

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

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"

对应的运行结果如下:

image.png

在上文中我们手动创建了一个text.txt文件。我们只是创建了,没告诉它放置的位置。我们将与src目录同级。

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

image.png

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

如果,我们想看针对大文件的处理方式,我们可以新建一个更大的项目。用于做代码实验。


后记

分享是一种态度

参考资料:

  1. 用Rust写一个命令行工具

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


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
2月前
|
Rust 资源调度 安全
为什么使用 Rust over C++ 进行 IoT 解决方案开发
为什么使用 Rust over C++ 进行 IoT 解决方案开发
73 7
|
4月前
|
Rust 安全 JavaScript
探索Rust在系统编程领域的前景:虚拟机和编译器开发的新篇章
【8月更文挑战第31天】在系统编程领域,性能与安全性至关重要。Rust作为一种新兴语言,凭借其独特的内存安全和并发特性,正逐渐成为虚拟机和编译器开发的首选。本文通过案例分析,探讨Rust在这些领域的应用,例如Facebook的Compiler VM (CVM)项目和实验性的JavaScript JIT编译器Mithril。Rust的静态类型系统和所有权模型确保了高性能和安全性,而其强大的包管理和库生态则简化了虚拟机的开发。随着Rust社区的不断成熟,预计未来将有更多基于Rust的创新项目涌现,推动系统编程的发展。对于追求高性能和安全性的开发者而言,掌握Rust将成为一个重要战略方向。
81 1
|
5月前
|
Rust 程序员 开发者
使用 Rust 开发一款类似于 GitBook 的程序
**Rust新手开发者分享开源项目 Typikon**:模仿MDBook,致力于简单Markdown到在线书的渲染。[GitHub](https://github.com/auula/typikon)上可找到源码,欢迎初学者一同学习与贡献。体验轻松构建静态网站,探索Rust之旅。🌟 加入讨论,共建更易用的GitBook替代品。在线文档见[https://typikonbook.github.io](https://typikonbook.github.io)。
40 1
|
6月前
|
Rust Unix Windows
使用Cargo国内镜像提升Rust开发效率
使用Cargo国内镜像提升Rust开发效率
521 0
|
7月前
|
Rust 安全 程序员
拜登:“一切非 Rust 项目均为非法”,开发界要大变天?
白宫国家网络总监办公室(ONCD,以下简称网总办)在本周一发布的报告中说道:“程序员编写代码并非没有后果,他们的⼯作⽅式于国家利益而言至关重要。”
134 1
|
7月前
|
Rust 前端开发 JavaScript
Rust在前端与全栈开发中的实践探索
随着Rust语言的日渐成熟,其应用场景已经从后端扩展到前端和全栈开发领域。本文将深入探讨Rust语言在前端与全栈开发中的实际应用案例,分析Rust语言在这些领域的优势和面临的挑战,并展望Rust未来的发展趋势。
|
存储 Rust 测试技术
Rust 开发命令行工具(中)(三)
Rust 开发命令行工具(中)(三)
150 0
|
Rust 测试技术 人机交互
Rust 开发命令行工具(中)(二)
Rust 开发命令行工具(中)(二)
|
存储 Rust JavaScript
Rust 开发命令行工具(中)(一)
Rust 开发命令行工具(中)(一)
|
19天前
|
Rust 安全 Java
探索Rust语言的并发编程模型
探索Rust语言的并发编程模型