当前位置: 首页 > news >正文

Rust 从零到精通:构建一个专业级命令行工具 greprs

前言

欢迎来到 Rust 的世界!Rust 是一门现代化的系统编程语言,专注于性能、内存安全和并发性。它没有垃圾回收器,却能通过强大的编译器和所有权系统在编译时就保证内存安全。这使得 Rust 成为构建高性能、高可靠性软件的绝佳选择,从底层的操作系统、游戏引擎,到高性能 Web 后端和云原生应用,无处不见其身影。

本教程将不仅仅是“Hello, world!”。我们的目标是带您走过一段完整的旅程:从在 Linux 环境下安装 Rust,到亲手构建一个功能完整、结构清晰、经过专业测试的命令行文本搜索工具——greprs。在这个过程中,您将亲身体验到 Rust 强大的构建系统 Cargo、模块化的项目设计、优雅的错误处理机制以及内置的测试框架。

准备好了吗?让我们启航,一同领略 Rust “内存安全、高性能、并发可靠”的独特魅力!

第一部分:环境搭建 —— 在 Linux 上安装 Rust

在编写任何代码之前,我们首先需要一个“炼钢炉”——也就是 Rust 的编译环境。我们将使用官方推荐的工具 rustup 来安装。rustup 是一个 Rust 工具链安装器,它可以帮助我们安装、管理和更新不同版本的 Rust 编译器 (rustc) 和包管理器 (cargo)。

步骤一:配置国内镜像源

为了确保下载速度,尤其是在中国大陆地区,我们首先将 rustup 的下载源配置为清华大学的镜像服务器。这是一个非常推荐的预备步骤,可以大大节约安装时间。

在终端中,完整地复制并粘贴以下两行命令,然后按回车:

export RUSTUP_DIST_SERVER=https://mirrors.tuna.tsinghua.edu.cn/rustup
export RUSTUP_UPDATE_ROOT=https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup
  • export 命令:这是 Linux/macOS 系统中用来设置环境变量的命令。环境变量是操作系统范围内共享的变量,可以被所有程序读取。
  • RUSTUP_DIST_SERVER:这个环境变量告诉 rustup 从哪里下载 Rust 编译器、标准库等核心组件。
  • RUSTUP_UPDATE_ROOT:这个环境变量则指定了 rustup 工具自身更新的来源地址。

执行这两行命令后,当前终端会话的环境变量就被设置好了。

image.png
图1:在终端中设置 rustup 镜像源环境变量。

步骤二:验证环境变量

为了确保我们的设置已经生效,可以运行以下命令来检查:

env | grep RUSTUP
  • env 命令:此命令会打印出当前会话中所有的环境变量。
  • | (管道符):它会将前一个命令 (env) 的输出,作为后一个命令 (grep) 的输入。
  • grep RUSTUPgrep 是一个强大的文本搜索工具,这里我们用它来筛选出所有包含 “RUSTUP” 字符串的行。

运行后, 您应该能看到刚刚设置的两行变量被清晰地打印出来,这证明我们的配置已成功。

image.png
图2:使用 env | grep RUSTUP 命令确认环境变量已成功设置。

步骤三:执行安装脚本

万事俱备,现在我们可以正式开始安装了。在终端中输入以下命令:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

这行命令看起来有些复杂,我们来分解一下:

  • curl:是一个用于发送网络请求的工具,这里用它来下载安装脚本。
  • --proto '=https' --tlsv1.2`:这些是安全相关的参数,确保下载过程使用安全的 HTTPS (TLS 1.2) 协议进行。
  • -sSf:这是三个参数的简写:
    • -s (silent):静默模式,不显示进度条。
    • -S (show-error):即使在静默模式下,如果发生错误,也显示错误信息。
    • -f (fail):当服务器返回 HTTP 错误时,静默地失败,而不是输出错误页面。
  • https://sh.rustup.rs:这是 rustup 官方安装脚本的地址。
  • | sh:管道符再次登场,它将 curl 下载好的脚本内容,直接传送给 sh (Shell) 解释器来执行。

脚本运行后,会显示欢迎信息并提供三个选项。我们直接输入 1 并按回车,选择默认的标准安装。

image.png
图3:选择 1) Proceed with installation (default) 进行标准安装。

安装过程会持续几分钟,rustup 会下载并安装 rustccargorust-std (标准库) 等核心组件。安装成功后,您会看到一条“Rust is installed now. Great!”的消息。

image.png
图4:安装成功的提示信息。

步骤四:安装验证与环境刷新

安装程序提示,它已经修改了您的 PATH 环境变量,但这个改动只对新打开的终端窗口生效。PATH 是一个非常重要的环境变量,它告诉操作系统去哪些目录下查找可执行程序。这就是为什么我们马上验证时会遇到问题的原因。

让我们来验证一下。输入:

rustc --version
cargo --version

您很可能会看到 Command 'rustc' not found 的错误提示。这是一个非常常见的现象,请完全不用担心!这只是因为当前的终端窗口还没有加载最新的 PATH 配置。

image.png
图5:初次验证时,系统提示找不到命令。

要解决这个问题,您有两个选择:

  1. 关闭当前的终端窗口,然后重新打开一个。
  2. 执行下面这行命令,手动刷新当前终端的环境变量配置。

我们选择第二种,因为它更直接:

source "$HOME/.cargo/env"
  • source 命令:这是 Shell 的一个内置命令,用于在当前 Shell 会话中执行指定文件里的命令。
  • $HOME/.cargo/env:这是 rustup 安装程序自动创建的一个脚本文件。$HOME 是指向您个人主目录的环境变量 (例如 /home/your_username)。这个脚本的作用就是将 Cargo 的 bin 目录 (通常是 $HOME/.cargo/bin) 添加到 PATH 环境变量中。

执行完这行命令之后,请再试一次检查版本号:

rustc --version

这一次,您应该能成功看到 Rust 编译器的版本号被打印出来了,这标志着我们的 Rust 开发环境已经完全准备就绪!

image.png
图6:刷新环境后,成功显示 rustc 版本号。

第二部分:初试牛刀 —— Cargo 与 “Hello, world!”

环境就绪,是时候编写我们的第一个 Rust 程序了。我们将使用 Cargo,这个 Rust 的瑞士军刀,来创建、编译和运行一个经典的 “Hello, world!” 项目。

步骤一:创建新项目

使用 Cargo 创建一个新项目。 Cargo 会为我们自动生成项目的标准目录结构和代码模板。

cargo new hello_world

执行后,您会看到一条消息:Created binary (application) hello_world package

image.png
图7:cargo new 成功创建项目。

这条命令做了什么?

  • cargo new:告诉 Cargo 创建一个新项目。
  • hello_world:是我们项目的名字。
  • binary (application):Cargo 默认创建的是一个二进制可执行文件项目。与之对应的是库项目 (library),我们稍后会接触到。

现在,您的本地目录下多出了一个名为 hello_world 的文件夹。让我们进入这个文件夹看看它的结构:

cd hello_world
ls -R

您会看到如下的目录结构:

.
├── Cargo.toml
└── src└── main.rs
  • Cargo.toml:这是 Cargo 的清单文件,采用 TOML 格式。它包含了项目的所有元数据,比如项目名称、版本、作者以及项目依赖的外部库(我们称之为 “crates”)。
  • src/main.rs:这是项目的源代码主文件。Cargo 期望将所有的源代码都放在 src 目录下。

image.png
图8:新创建的 hello_world 项目目录结构。

步骤二:编译并运行项目

现在,让我们编译并运行项目。 只需在项目根目录下执行一个简单的命令:

cargo run

image.png
图9:cargo run 成功编译并运行项目,输出 “Hello, world!”。

cargo run 是一个复合命令,它会依次执行以下操作:

  1. 编译 (Compile):它会调用 rustc 编译器来编译 src/main.rs 以及其他所有源代码文件。如果这是您第一次编译,Cargo 会先下载并编译项目的所有依赖(目前还没有)。编译成功后,会在 target/debug/ 目录下生成一个名为 hello_world 的可执行文件。
  2. 运行 (Run):编译成功后,Cargo 会立即执行这个生成的可执行文件。

于是,您在终端上看到了那句经典的输出:Hello, world!

…到这里我们就完成了在服务器上 Rust 的初始化的相关操作了。恭喜!您已经成功编译并运行了您的第一个 Rust 程序。

第三部分:实战进阶 —— 构建增强版 grep 工具 greprs

“Hello, world!” 只是起点。为了真正感受 Rust “内存安全、高性能、并发可靠”的魅力,最好的方式就是亲手构建一个有用的东西。官方教程经常从一个叫 grep 的经典命令行工具入手,它用于在文件中搜索文本。

今天,我们不满足于简单复刻,我们要构建一个更酷的“增强版”:greprs

greprs 的目标功能:

  1. grep 一样,接收一个“搜索词”和一个“文件名”作为参数。
  2. 读取文件,找出包含“搜索词”的行,并打印出来。
  3. 【亮点功能】:通过设置一个名为 IGNORE_CASE 的环境变量,可以随时切换“大小写敏感”和“大小写不敏感”两种搜索模式。
  4. 【专业实践】:我们将采用专业的软件工程实践,比如代码重构、优雅的错误处理,甚至为核心功能编写单元测试,让你一窥 Rust 在构建可靠软件方面的强大之处。

准备好了吗?让我们发车!


第一步:创建项目并解析参数

首先,我们需要一个新项目,并学会如何读取用户从命令行传入的参数。

  1. 使用 Cargo 创建 greprs 项目:
    cargo new greprs
    cd greprs
    

image.png
图10:创建新的 greprs 项目并进入目录。

  1. 编写初步的参数读取代码:
    打开 src/main.rs 文件,用下面的代码替换 Cargo 为我们生成的 “Hello, world!” 模板。
use std::env; // 引入标准库中的 env 模块,用于处理环境变量和命令行参数fn main() {// env::args() 返回一个迭代器,其中包含了所有的命令行参数// .collect() 方法将这个迭代器转换成一个 Vec<String> 集合let args: Vec<String> = env::args().collect();// 打印出所有参数,用于调试// dbg!(args); // dbg! 是一个非常方便的调试宏,它会接管值的所有权,打印文件名、行号和值,然后返回值的所有权// args 的第一个元素 (索引为0) 永远是程序的路径let query = &args;      // 第二个元素是我们期望的搜索词let file_path = &args;  // 第三个元素是我们期望的文件名println!("搜索关键词: {}", query);println!("目标文件: {}", file_path);
}

代码深度解析

  • use std::env;use 关键字用于将外部模块的功能引入到当前作用域。std 是 Rust 的标准库 (standard library),env 是其中的一个模块,专门用于处理与环境相关的功能,比如命令行参数和环境变量。
  • env::args():这是一个函数,它会返回一个迭代器 (Iterator)。迭代器是 Rust 中一个核心的抽象概念,代表一个可以逐项产生的序列。在这里,这个序列就是用户在命令行中输入的参数,每个参数都是一个字符串。
  • .collect():这是一个非常强大的方法,可以消费一个迭代器并将其中的所有项收集到一个集合类型中。我们通过 let args: Vec<String> 显式地告诉 Rust,我们希望将这些字符串收集到一个 Vec<String>(一个可增长的字符串向量/数组)中。
  • let query = &args[1];:我们通过索引来访问 Vec 中的元素。args[0] 总是程序的名称本身(例如 ./target/debug/greprs)。因此,我们期望的第一个参数(搜索词)在索引 1 的位置,第二个参数(文件名)在索引 2 的位置。
  • &args[1]:这里的 & 符号非常关键。它表示我们正在创建一个引用 (reference),或者说借用 (borrowing) args 向量中第二个元素的值。我们只是想读取这个值,并不需要获得它的所有权。这是 Rust 所有权系统的核心体现,它能有效防止数据竞争和悬垂指针等问题。

image.png
图11:在 src/main.rs 中编写参数解析代码。

  1. 运行测试:
    现在的问题是,如何向 cargo run 传递我们自己程序的参数呢?答案是使用 -- 分隔符。cargo run 会将 -- 之后的所有内容都当作参数传递给我们的程序。
cargo run -- searchstring example.txt

执行后,你会看到类似下面的输出,这证明我们成功地从命令行拿到了参数:

搜索关键词: searchstring
目标文件: example.txt

image.png
图12:使用 --cargo run 传递程序参数并成功解析。


第二步:读取文件内容

我们已经拿到了文件名,下一步自然就是读取这个文件的内容。

修改 src/main.rs,加入文件读取的逻辑。

use std::env;
use std::fs; // 引入文件系统 (file system) 模块fn main()
{let args: Vec<String> = env::args().collect();let query = &args;let file_path = &args;println!("搜索关键词: {}", query);println!("目标文件: {}", file_path);// fs::read_to_string 会尝试读取文件并返回一个 Result<String, Error>// .expect() 是一个快捷方法,如果 Result 是 Ok,它会返回值;如果是 Err,程序会 panic 并显示我们提供的错误信息let contents = fs::read_to_string(file_path).expect("读取文件时发生错误,请检查文件路径是否正确");println!("\n文件内容:\n{}", contents);
}

代码深度解析

  • use std::fs;:我们引入了标准库中的 fs 模块,它包含了所有与文件系统交互所需的功能。
  • fs::read_to_string(file_path):这个函数接收一个文件路径作为参数,并尝试将整个文件的内容读取到一个 String 中。
  • Result<String, Error>:这是 read_to_string 的返回类型,也是 Rust 错误处理的核心。Result 是一个枚举 (enum),它有两个变体:
    • Ok(T):表示操作成功,并包含成功的值(这里是 String 类型的文件内容)。
    • Err(E):表示操作失败,并包含一个错误值(这里是 std::io::Error 类型)。
  • .expect(...):这是一个定义在 Result 上的辅助方法。它是一种简单粗暴的错误处理方式:
    • 如果 ResultOk(value)expect 会“解包”它,并返回里面的 value
    • 如果 ResultErr(error)expect 会让程序崩溃 (panic),并打印出我们提供给它的错误信息字符串。
      虽然 expect 在原型开发和示例代码中很方便,但在生产级代码中,我们通常会使用更优雅的方式来处理 Err,例如 match 表达式或 ? 操作符,我们稍后会进行重构。

image.png
图13:添加文件读取逻辑后的 src/main.rs

运行测试:

  1. 在你的 greprs 项目根目录下,创建一个名为 poem.txt 的文件。我们可以使用 touch 命令。

    touch poem.txt
    


    图14:使用 touch 命令创建测试文件。

  2. 使用文本编辑器(如 vimnano 或图形化编辑器)编辑 poem.txt,粘贴以下来自艾米莉·狄金森的诗歌:

    I'm nobody! Who are you?
    Are you nobody, too?
    Then there's a pair of us - don't tell!
    They'd banish us, you know.How dreary to be somebody!
    How public, like a frog
    To tell your name the livelong day
    To an admiring bog!
    

    image.png
    图15:编辑 poem.txt 并粘贴内容。

  3. 现在,用这个新文件来运行你的程序:

    cargo run -- nobody poem.txt
    

    执行命令后,你应该能看到文件的全部内容被成功地打印到了终端上。这证明我们的文件读取逻辑是正确的。

    image.png
    图16:程序成功读取并打印 poem.txt 的内容。


第三步:代码重构与专业化

目前,我们所有的逻辑——参数解析、文件读取、打印输出——都挤在 main 函数里。对于一个小程序来说这没什么问题,但随着程序功能的增加,这会变得难以维护和测试。一个好的程序应该结构清晰,关注点分离。

我们将进行一次关键的重构:把配置解析的逻辑和程序核心运行的逻辑分开。这是体现 Rust 工程化思想的关键一步!

  1. 逻辑拆分:main.rs vs lib.rs
    在 Cargo 的约定中:

    • src/main.rs二进制包 (binary crate) 的根文件。它的主要职责是启动程序、处理命令行参数和顶层错误。它是一个“壳”。
    • src/lib.rs库包 (library crate) 的根文件。它应该包含程序的核心逻辑,这些逻辑可以被 main.rs 调用,甚至可以被其他外部项目作为依赖库来使用。它是“核”。

    现在,在 src 目录下创建一个新文件 lib.rs。Cargo 会自动识别它,并把它当作一个可以被 main.rs 调用的“库”。

  2. 创建配置结构体与核心逻辑到 lib.rs
    将下面的代码完整地粘贴到新建的 src/lib.rs 文件中。

use std::error::Error;
use std::fs;// pub 关键字表示这个结构体是公共的,可以被外部模块访问
pub struct Config {pub query: String,pub file_path: String,pub ignore_case: bool,
}impl Config {// 关联函数 (类似于静态方法),负责解析参数并构造 Config 实例pub fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {// 返回一个包含字符串字面量的 Errreturn Err("参数不足,需要提供 <搜索词> 和 <文件名>");}// 注意这里的 .clone()let query = args.clone();let file_path = args.clone();// 检查名为 IGNORE_CASE 的环境变量是否存在// std::env::var 返回一个 Result,is_ok() 在 Result 为 Ok 时返回 truelet ignore_case = std::env::var("IGNORE_CASE").is_ok();Ok(Config { query, file_path, ignore_case })}
}// 主运行逻辑,接收一个 Config 实例
// Box<dyn Error> 是一个 trait object,代表“任何实现了 Error trait 的类型”
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {// `?` 操作符是错误处理的语法糖let contents = fs::read_to_string(config.file_path)?;let results = if config.ignore_case {search_case_insensitive(&config.query, &contents)} else {search(&config.query, &contents)};for line in results {println!("{}", line);}Ok(()) // 函数成功结束时返回一个 Ok 的空元组
}// 区分大小写的搜索逻辑
// 'a 是一个生命周期参数,它将返回值的生命周期与 contents 的生命周期关联起来
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let mut results = Vec::new(); // 创建一个可变的空向量for line in contents.lines() { // 遍历文件内容的每一行if line.contains(query) { // 如果行包含搜索词results.push(line);   // 将该行的引用推入结果向量}}results // 返回结果向量
}// 不区分大小写的搜索逻辑
pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {let query = query.to_lowercase(); // 将搜索词转换为小写let mut results = Vec::new();for line in contents.lines() {// 将每一行也转换为小写再进行比较if line.to_lowercase().contains(&query) {results.push(line);}}results
}

代码深度解析 (lib.rs)

  • pub struct Config:我们定义了一个公共的结构体 Config 来存放所有配置信息,这比零散的变量更清晰。
  • impl Config { pub fn build(...) }impl 块用于为结构体实现方法。build 是一个关联函数(因为它不接收 &self 参数),它的作用类似于构造函数。它负责解析命令行参数并返回一个 Result<Config, ...>。这里的错误处理比 expect 更优雅,它将错误返回给调用者处理。
  • args[1].clone():之前我们使用的是借用 &args[1],但现在我们需要将值的所有权转移到 Config 结构体中。clone() 会创建一个数据的完整副本,这会带来一些性能开销,但在这里是简单且安全的选择。
  • std::env::var("IGNORE_CASE").is_ok():这是我们实现亮点功能的核心。std::env::var 尝试读取一个环境变量,它也返回一个 Result。如果环境变量存在,它返回 Ok(value);如果不存在,返回 Err(...)。我们不关心它的具体值,只关心它是否存在,所以 .is_ok() 方法完美地满足了我们的需求。
  • pub fn run(config: Config) -> Result<(), Box<dyn Error>>:这是程序的核心运行逻辑。
    • 返回类型Result<(), Box<dyn Error>> 是 Rust 中用于可能失败的函数的一个惯用返回类型。() 是一个空元组,表示成功时没有具体的值返回。Box<dyn Error> 是一种“trait 对象”,它代表“任何实现了 Error trait 的类型”,这让我们的函数可以返回多种不同类型的错误。
    • ? 操作符:注意 fs::read_to_string(...) 后面的 ?。这是 Rust 错误处理的强大语法糖。它等价于一个 match 表达式:如果 ResultOk(value),它会解包并返回 value;如果 ResultErr(error),它会立即从当前函数返回这个 error。这极大地简化了错误传播的代码。
  • 生命周期 'a:在 searchsearch_case_insensitive 函数签名中,<'a> 是一个生命周期注解。它告诉编译器,函数返回的 Vec 中包含的字符串切片 (&'a str) 是从输入的 contents (&'a str) 中借用来的,并且它们的存活时间不能超过 contents 的存活时间。这是 Rust 保证内存安全、避免悬垂引用的核心机制之一。

image.png
图17:src/lib.rs 文件包含了所有核心逻辑。

  1. 改造 main.rs 来调用库:
    现在,main.rs 的职责变得非常纯粹:启动程序、调用核心逻辑、处理最终的错误。用以下代码替换 src/main.rs 的全部内容。
use std::env;
use std::process;// `greprs` 是我们的库包名,与项目名相同
// 我们从中引入公共的 Config 结构体
use greprs::Config;fn main() {let args: Vec<String> = env::args().collect();// 调用 Config::build 来解析参数// unwrap_or_else 是一个比 unwrap 或 expect 更健壮的错误处理方法let config = Config::build(&args).unwrap_or_else(|err| {// eprintln! 宏会将错误信息打印到标准错误流 (stderr)eprintln!("参数解析错误: {}", err);// 使用非零状态码退出程序,表示发生了错误process::exit(1);});// 调用核心运行逻辑,并处理可能返回的错误if let Err(e) = greprs::run(config) {eprintln!("应用运行时错误: {}", e);process::exit(1);}
}

代码深度解析 (main.rs)

  • use greprs::Config;:我们通过包名 greprs 来引用库中的 Config。Cargo 会自动处理好这一切。
  • .unwrap_or_else(|err| { ... }):这是一个非常优雅的错误处理方式。它接收一个闭包(匿名函数)作为参数。
    • 如果 Config::build 返回 Ok(config),它会解包并返回 config
    • 如果返回 Err(err),它会执行我们提供的闭包,并将 err 作为参数传入。在闭包中,我们打印错误信息并退出程序。
  • eprintln!:这个宏与 println! 类似,但它会将内容打印到标准错误流 (stderr),而不是标准输出流 (stdout)。这是命令行工具的最佳实践,因为它允许用户将正常的程序输出重定向到文件,同时仍然能在终端上看到错误信息。
  • process::exit(1):这会立即终止程序。按照惯例,状态码 0 表示成功,任何非零状态码都表示失败。
  • if let Err(e) = greprs::run(config):这是处理 run 函数返回的 Result 的另一种惯用方式。if let 是一种模式匹配的语法糖,这里它的意思是:“如果 greprs::run 的返回值可以匹配 Err(e) 模式,那么就执行这个代码块,并将错误值绑定到变量 e 上”。

image.png
图18:重构后的 src/main.rs 变得简洁而清晰。

现在你的代码结构非常清晰了!main.rs 负责“壳”,lib.rs 负责“核”。这种分离使得核心逻辑更容易被测试、复用和维护。


第四步:测试驱动开发 (TDD)

如何保证我们的 searchsearch_case_insensitive 函数逻辑是正确的?每次都手动运行 cargo run 来验证吗?这显然是低效且不可靠的。专业的做法是编写单元测试。Rust 的美妙之处在于,它内置了强大而易用的测试框架,无需任何第三方工具。

将下面的测试代码追加src/lib.rs 文件的末尾。

// #[cfg(test)] 告诉 Rust 只有在执行 `cargo test` 时才编译和运行这段代码
// 这意味着测试代码不会被包含在最终的发行版可执行文件中
#[cfg(test)]
mod tests {use super::*; // `super` 关键字代表父模块,`*` 通配符引入外部模块的所有内容// #[test] 属性将一个函数标记为测试函数#[test]fn case_sensitive() {let query = "duct";let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";// assert_eq! 宏用于断言两个值是否相等,如果不相等,测试就会失败并打印出两个值assert_eq!(vec!["safe, fast, productive."], search(query, contents));}#[test]fn case_insensitive() {let query = "rUsT";let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";assert_eq!(vec!["Rust:", "Trust me."],search_case_insensitive(query, contents));}
}

代码深度解析 (测试模块)

  • #[cfg(test)]:这是一个条件编译属性。cfg 代表 configuration。这行代码告诉 Rust 编译器:只有在为测试 (test) 配置进行编译时,才包含下面的 mod tests 模块。当你运行 cargo buildcargo run 时,这部分代码会被完全忽略。
  • mod tests:在 Rust 中,通常将单元测试放在一个名为 tests 的子模块中。
  • use super::*;:在 tests 模块内部,我们需要访问外部模块(也就是 lib.rs 的主模块)定义的函数,如 searchsuper 关键字可以让我们访问父模块的作用域,* (glob operator) 则引入了父模块中所有公共的项。
  • #[test]:这个属性将一个普通的函数转换成一个测试函数。当运行 cargo test 时,测试运行器会自动查找并执行所有带有此属性的函数。
  • assert_eq!(left, right):这是一个断言宏。它检查 leftright 两个参数是否相等。如果相等,测试通过;如果不相等,测试会 panic,并被测试框架捕获为一次失败。它会友好地打印出左右两边的值,方便我们调试。

image.png
图19:将测试代码追加到 src/lib.rs 的末尾。

现在,在终端运行测试命令:

cargo test

Cargo 会自动找到并运行所有测试。你应该能看到测试通过的绿色 ok 提示!这给了我们极大的信心,我们的核心搜索逻辑是完全正确的。有了测试的保护,我们未来就可以“无畏重构”,大胆地优化代码,因为只要测试仍然通过,就说明我们没有破坏原有的功能。

image.png
图20:cargo test 显示所有测试都成功通过。


第五步:见证奇迹的时刻

万事俱备,我们的 greprs 工具已经功能完备、结构清晰、并且经过了单元测试的验证。让我们来验收最终成果!

  1. 默认模式(大小写敏感):
    我们将搜索 poem.txt 中包含 “to” 的行。

    cargo run -- to poem.txt
    

    根据诗歌内容,只有 “How dreary to be somebody!” 这一行是小写的 “to”。因此,输出应该是:

    How dreary to be somebody!
    ```![image.png](https://i-blog.csdnimg.cn/img_convert/f0c592f7eb7b90282f7044029290bb2b.png)
    *图21:在默认的大小写敏感模式下运行 `greprs`。*
  2. 亮点功能(大小写不敏感):
    现在,让我们激活环境变量。我们在 cargo run 命令前加上 IGNORE_CASE=1 来临时设置这个环境变量(它的值是什么不重要,只要存在即可)。

    IGNORE_CASE=1 cargo run -- to poem.txt
    

    这次,我们的程序会进行大小写不敏感的搜索。诗歌中包含 “to” 或 “To” 的行都会被匹配到:

    • Are you nobody, **too**?
    • How dreary **to** be somebody!
    • **To** tell your name the livelong day
    • **To** an admiring bog!

    所以,这次的输出应该是:

    Are you nobody, too?
    How dreary to be somebody!
    To tell your name the livelong day
    To an admiring bog!
    

    看!完全符合预期!程序的行为被环境变量动态地改变了。我们成功构建了一个灵活且强大的命令行工具。

    image.png
    图22:设置 IGNORE_CASE 环境变量,成功切换到大小写不敏感搜索模式。


总结与展望:您已踏上 Rust 大师之路

回顾我们刚刚完成的旅程,它不仅仅是复刻了一个 grep。在这个过程中,您已经深入体验并实践了 Rust 生态中最核心、最强大的几个概念:

  • Cargo 的威力:您已经看到,Cargo 不仅仅是一个编译器前端。它是 Rust 的构建系统、包管理器、测试运行器和文档生成器。从 cargo new 创建项目,到 cargo run 编译运行,再到 cargo test 保证质量,Cargo 为开发者提供了一流的、无缝衔接的开发体验。

  • 专业的模块化设计:通过 main.rslib.rs 的分离,我们实践了清晰的软件架构思想——关注点分离。这使得我们的代码不仅更易于理解和维护,而且核心逻辑(在 lib.rs 中)具备了被其他项目复用的潜力。

  • 零成本抽象的魅力:我们编写了 Config 结构体、impl 代码块、Result 枚举等高级抽象。在许多其他语言中,这些抽象可能会带来运行时开销。但在 Rust 中,由于强大的编译器优化,这些抽象在编译后会被“拍平”,最终生成不输于 C 语言的、极致高效的机器码。这就是所谓的“零成本抽象”。

  • 健壮的错误处理:我们从简单的 .expect() 开始,逐步演进到使用 Result 枚举、? 操作符和 unwrap_or_else 闭包。您亲身体会到 Rust 如何通过其类型系统,迫使开发者在编译时就必须考虑并处理可能发生的错误,从而写出健壮、不易崩溃的高可靠性代码。

  • 内置测试框架的自信:无需配置任何第三方测试库,您就能直接在代码旁边编写并运行单元测试。这是 Rust “无畏重构”精神的底气所在。强大的测试支持鼓励开发者持续改进代码,而不必担心引入新的 Bug。

从一个简单的安装命令,到一个功能完整、结构清晰、经过专业测试的命令行工具,您已经迈出了一大步,真正踏入了 Rust 的大门。这正是 Rust 的魅力所在:它不仅让你写出运行快的代码,更通过其语言设计和工具链,引导你写出质量高、可维护的代码。

Rust 的世界远不止于此。它的征途是星辰大海:高性能 Web 后端(Actix, Axum)、前端 WebAssembly、嵌入式系统、操作系统、云原生工具(如 Linkerd)… 掌握了今天学到的基础,您就拥有了探索这些广阔领域的钥匙。

希望这篇详尽的入门教程能为您打开一扇通往新世界的大门。欢迎来到 Rust 的世界,祝您编码愉快!

http://www.dtcms.com/a/609541.html

相关文章:

  • 大足网站建设网络营销市场调研的内容
  • CSS3 分页技术解析
  • HTMLElement 与MouseEvent 事件对象属性详解
  • 建设网站都要学些什么手续拍卖网站模板下载
  • 【火语言RPA实战案例】根据ISBN 编码批量查询孔夫子书籍信息,自动导出本地 Excel(附完整脚本)
  • 从零开始理解状态机:C语言与Verilog的双重视角
  • 做软件常用的网站有哪些软件微信怎么做网站推广
  • 设计模式面试题(14道含答案)
  • [智能体设计模式] 第9章 :学习与适应
  • 肇庆市建设局网站西双版纳建设厅网站
  • LingJing(灵境)桌面级靶场平台新增:真实入侵复刻,知攻善防实验室-Linux应急响应靶机2,通关挑战
  • 融合尺度感知注意力、多模态提示学习与融合适配器的RGBT跟踪
  • 基于脚手架微服务的视频点播系统-脚手架开发部分Fast-dfs,redis++,odb的简单使用与二次封装
  • 构建高可用Redis:哨兵模式深度解析与Nacos微服务适配实践
  • Linux -- 线程同步、POSIX信号量与生产者消费者模型
  • 微服务重要知识点
  • 东莞seo建站排名昆山有名的网站建设公司
  • 主从服务器
  • Linux 文件缓冲区
  • Node.js中常见的事件类型
  • Nacos的三层缓存是什么
  • 交通事故自动识别_YOLO11分割_DRB实现
  • 用flex做的网站空间注册网站
  • Vue + Axios + Node.js(Express)如何实现无感刷新Token?
  • 重大更新!Ubuntu Pro 现提供长达 15 年的安全支持
  • 重庆做学校网站公司农村服务建设有限公司网站
  • 尝试本地部署 Stable Diffusion
  • 网站前置审批专项好的用户体验网站
  • 【动规】背包问题
  • js:网页屏幕尺寸小于768时,切换到移动端页面