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

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Rust 开发命令行工具(上)(二)

使用 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 库中的 Parsertrait,它用于定义命令行参数和解析命令行输入。
  2. #[derive(Parser)]: 这是一个自定义属性(attribute),用于自动实现 Parser trait。通过这个属性,我们可以在结构体上使用 Parser 的功能,使其成为一个可以解析命令行参数的类型。

通过使用 clap 库中的 Parsertrait,我们可以轻松地为我们的命令行工具定义参数和解析用户提供的命令行输入。这有助于使命令行工具更加灵活和易于使用,同时提供了自动生成帮助文档和解析命令行参数的功能。

关于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'.

如我们所见,没有输出。这是好事:这意味着没有错误,我们的程序已经结束。


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 现在应该可以工作了!

image.png

上面的代码,虽然能满足我们的业务需求,但是还不够完美。有一个弊端:它会将整个文件读入内存 - 无论文件有多大。如果我们想在一个庞然大物中搜索我们需要的内容,那就有点不爽了。

我们可以使用 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 将错误组合成两个主要类别:{可恢复错误|recoverable}{不可恢复错误|unrecoverable}

  • 可恢复错误通常代表向用户报告错误和重试操作是合理的情况,比如未找到文件
  • 不可恢复错误通常是bug的同义词,比如尝试访问超过数组结尾的位置。
  • Rustpanic!宏当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出

这可能看起来有点激进,但非常方便。如果我们的程序需要读取该文件,如果文件不存在无法执行任何操作,那么退出是一种有效的策略。甚至在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
  1. let result = std::fs::read_to_string("test.txt");: 这行代码尝试打开并读取文件 "test.txt" 的内容。它使用了标准库中的 std::fs::read_to_string 函数,该函数返回一个 Result<String, std::io::Error>,表示读取文件内容的结果。
  2. let content = match result { ... }: 这是一个模式匹配语句,用于处理文件读取的结果result
  • 如果读取成功 (Ok(content)),则将读取的内容存储在 content 变量中。
  • 如果读取失败 (Err(error)),则将错误转换为 Result,并将其返回作为程序的错误结果。
  1. println!("文件内容:{}", content);: 如果成功读取文件内容,程序将打印文件的内容到标准输出,使用 {} 占位符来插入 content 变量的值。
  2. Ok(()): 最后,程序返回一个成功的 Result,表示程序执行成功。

注意:为什么这不写作return Ok(());?它完全可以这样写,这也是完全有效的。在Rust中,任何块的最后一个表达式都是它的返回值,习惯上省略不必要的返回。

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