接受命令行参数

像往常一样,让我们使用 cargo new 创建一个新项目。我们将项目命名为 minigrep,以便将其与您可能已在系统上拥有的 grep 工具区分开来。

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

第一个任务是使 minigrep 接受两个命令行参数:文件路径和要搜索的字符串。也就是说,我们希望能够使用 cargo run 运行我们的程序,使用两个连字符来指示以下参数是给我们的程序而不是给 cargo 的,一个要搜索的字符串,以及一个要搜索的文件的路径,如下所示:

$ cargo run -- searchstring example-filename.txt

现在,cargo new 生成的程序无法处理我们给定的参数。 crates.io 上的一些现有库可以帮助编写接受命令行参数的程序,但是因为您只是在学习这个概念,所以让我们自己实现这个功能。

读取参数值

为了使 minigrep 能够读取我们传递给它的命令行参数的值,我们需要 Rust 标准库中提供的 std::env::args 函数。此函数返回传递给 minigrep 的命令行参数的迭代器。我们将在 第 13 章 中全面介绍迭代器。目前,您只需要了解关于迭代器的两个细节:迭代器产生一系列值,我们可以调用迭代器的 collect 方法将其转换为集合,例如一个包含迭代器产生的所有元素的向量。

清单 12-1 中的代码允许您的 minigrep 程序读取传递给它的任何命令行参数,然后将值收集到一个向量中。

文件名:src/main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}
清单 12-1:将命令行参数收集到一个向量并打印出来

首先,我们使用 use 语句将 std::env 模块引入作用域,以便可以使用其 args 函数。请注意,std::env::args 函数嵌套在两个模块级别中。正如我们在 第 7 章 中讨论的那样在所需函数嵌套在多个模块中的情况下,我们选择将父模块引入作用域,而不是函数。这样做,我们可以轻松地使用 std::env 中的其他函数。这比添加 use std::env::args 然后只用 args 调用函数更不含糊,因为 args 很容易被误认为是当前模块中定义的函数。

args 函数和无效 Unicode

请注意,如果任何参数包含无效 Unicode,std::env::args 将会 panic。如果您的程序需要接受包含无效 Unicode 的参数,请改用 std::env::args_os。该函数返回一个迭代器,该迭代器产生 OsString 值而不是 String 值。我们选择在这里使用 std::env::args 是为了简单起见,因为 OsString 值因平台而异,并且比 String 值更难处理。

main 的第一行,我们调用 env::args,然后立即使用 collect 将迭代器转换为包含迭代器产生的所有值的向量。我们可以使用 collect 函数创建多种类型的集合,因此我们显式地注释 args 的类型,以指定我们想要一个字符串向量。尽管您很少需要在 Rust 中注释类型,但 collect 是您经常需要注释的一个函数,因为 Rust 无法推断您想要的集合类型。

最后,我们使用调试宏打印向量。让我们先尝试在不带参数的情况下运行代码,然后再使用两个参数运行

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
    "target/debug/minigrep",
]
$ cargo run -- needle haystack
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
    "target/debug/minigrep",
    "needle",
    "haystack",
]

请注意,向量中的第一个值是 "target/debug/minigrep",这是我们的二进制文件的名称。这与 C 中的参数列表的行为相匹配,允许程序使用它们在执行时被调用的名称。如果您想在消息中打印它或根据用于调用程序的命令行别名更改程序的行为,那么访问程序名称通常很方便。但是,在本章中,我们将忽略它,只保存我们需要的那两个参数。

将参数值保存在变量中

该程序目前能够访问作为命令行参数指定的值。现在,我们需要将这两个参数的值保存在变量中,以便在程序的其余部分中使用这些值。我们在清单 12-2 中这样做。

文件名:src/main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {query}");
    println!("In file {file_path}");
}
清单 12-2:创建变量来保存查询参数和文件路径参数

正如我们在打印向量时看到的那样,程序的名称占据向量中 args[0] 的第一个值,因此我们从索引 1 开始获取参数。minigrep 获取的第一个参数是我们要搜索的字符串,因此我们将对第一个参数的引用放入变量 query 中。第二个参数将是文件路径,因此我们将对第二个参数的引用放入变量 file_path 中。

我们暂时打印这些变量的值,以证明代码正在按我们的预期工作。让我们再次使用参数 testsample.txt 运行此程序

$ cargo run -- test sample.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt

太棒了,程序正在工作!我们需要的值被保存到了正确的变量中。稍后我们将添加一些错误处理来处理某些潜在的错误情况,例如用户没有提供参数的情况;现在,我们将忽略这种情况,而专注于添加文件读取功能。