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

简介: 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中,任何块的最后一个表达式都是它的返回值,习惯上省略不必要的返回。

相关文章
|
2月前
|
Rust 安全 程序员
拜登:“一切非 Rust 项目均为非法”,开发界要大变天?
白宫国家网络总监办公室(ONCD,以下简称网总办)在本周一发布的报告中说道:“程序员编写代码并非没有后果,他们的⼯作⽅式于国家利益而言至关重要。”
35 1
|
3月前
|
Rust 前端开发 JavaScript
Rust在前端与全栈开发中的实践探索
随着Rust语言的日渐成熟,其应用场景已经从后端扩展到前端和全栈开发领域。本文将深入探讨Rust语言在前端与全栈开发中的实际应用案例,分析Rust语言在这些领域的优势和面临的挑战,并展望Rust未来的发展趋势。
|
5月前
|
存储 Rust 测试技术
Rust 开发命令行工具(中)(三)
Rust 开发命令行工具(中)(三)
|
5月前
|
Rust 测试技术 人机交互
Rust 开发命令行工具(中)(二)
Rust 开发命令行工具(中)(二)
|
5月前
|
存储 Rust JavaScript
Rust 开发命令行工具(中)(一)
Rust 开发命令行工具(中)(一)
|
5月前
|
存储 Rust Linux
Rust 开发命令行工具(上)(三)
Rust 开发命令行工具(上)(三)
|
5月前
|
存储 人工智能 Rust
Rust 开发命令行工具(上)(一)
Rust 开发命令行工具(上)(一)
|
18天前
|
Rust 安全 程序员
|
18天前
|
Rust 安全 程序员
Rust vs Go:解析两者的独特特性和适用场景
在讨论 Rust 与 Go 两种编程语言哪种更优秀时,我们将探讨它们在性能、简易性、安全性、功能、规模和并发处理等方面的比较。同时,我们看看它们有什么共同点和根本的差异。现在就来看看这个友好而公平的对比。
|
9月前
|
Rust Go C++
Rust vs Go:常用语法对比(十三)(1)
Rust vs Go:常用语法对比(十三)(1)
64 0