解析命令行参数

我们的 CLI 工具的典型调用如下所示

$ grrs foobar test.txt

我们期望我们的程序查看 test.txt 并打印出包含 foobar 的行。但是我们如何获取这两个值呢?

程序名称后面的文本通常被称为“命令行参数”,或“命令行标志”(特别是当它们看起来像 --this 时)。在内部,操作系统通常将它们表示为字符串列表——粗略地说,它们通过空格分隔。

关于这些参数,以及如何将它们解析成更容易处理的东西,有很多思考方式。你还需要告诉你的程序的用户他们需要提供哪些参数,以及期望的格式是什么。

获取参数

标准库包含函数 std::env::args(),它会为你提供给定参数的 迭代器。第一个条目(索引为 0)将是你的程序被调用时的名称(例如, grrs),后续的条目是用户之后写的内容。

以这种方式获取原始参数非常容易(在文件 src/main.rs 中)

fn main() {
    let pattern = std::env::args().nth(1).expect("no pattern given");
    let path = std::env::args().nth(2).expect("no path given");

    println!("pattern: {:?}, path: {:?}", pattern, path)
}

我们可以使用 cargo run 运行它,通过在 -- 之后写入参数来传递参数

$ cargo run -- some-pattern some-file
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target/debug/grrs some-pattern some-file`
pattern: "some-pattern", path: "some-file"

作为数据类型的 CLI 参数

与其将它们视为一堆文本,不如将 CLI 参数视为表示程序输入的自定义数据类型,这通常是有益的。

看看 grrs foobar test.txt:有两个参数,首先是 pattern(要查找的字符串),然后是 path(要查找的文件)。

关于它们我们还能说些什么呢?首先,两者都是必需的。我们还没有讨论任何默认值,因此我们希望用户始终提供两个值。此外,我们可以说一些关于它们类型的信息:pattern 应该是一个字符串,而第二个参数应该是一个文件路径。

在 Rust 中,围绕它们处理的数据来构建程序是很常见的,所以这种看待 CLI 参数的方式非常合适。让我们从这个开始(在文件 src/main.rs 中,在 fn main() { 之前)

struct Cli {
    pattern: String,
    path: std::path::PathBuf,
}

这定义了一个新的结构体(struct),它有两个字段用于存储数据:patternpath

现在,我们仍然需要将程序实际获取的参数转换为这种形式。一种选择是手动解析我们从操作系统获得的字符串列表,并自己构建结构体。它看起来会像这样

fn main() {
    let pattern = std::env::args().nth(1).expect("no pattern given");
    let path = std::env::args().nth(2).expect("no path given");

    let args = Cli {
        pattern,
        path: std::path::PathBuf::from(path),
    };

    println!("pattern: {:?}, path: {:?}", args.pattern, args.path);
}

这可以工作,但不是很方便。你将如何处理支持 --pattern="foo"--pattern "foo" 的要求?你将如何实现 --help

使用 Clap 解析 CLI 参数

一种更好的方法是使用许多可用的库之一。用于解析命令行参数的最流行的库称为 clap。它具有你期望的所有功能,包括对子命令、shell 补全和优秀的帮助消息的支持。

让我们首先通过将 clap = { version = "4.0", features = ["derive"] } 添加到我们的 Cargo.toml 文件的 [dependencies] 部分来导入 clap

现在,我们可以在我们的代码中编写 use clap::Parser;,并在我们的 struct Cli 正上方添加 #[derive(Parser)]。 让我们沿途也写一些文档注释。

它看起来会像这样(在文件 src/main.rs 中,在 fn main() { 之前)

use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

Cli 结构体下面,我们的模板包含它的 main 函数。 当程序启动时,它将调用此函数

fn main() {
    let args = Cli::parse();

    println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}

这将尝试将参数解析为我们的 Cli 结构体。

但是如果失败了呢?这就是这种方法的优点:Clap 知道需要哪些字段,以及它们的期望格式是什么。它可以自动生成一个漂亮的 --help 消息,以及给出一些很好的错误信息,建议你在写 --putput 时传递 --output

总结

你的代码现在应该看起来像

use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();

    println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}

不带任何参数运行它

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 10.16s
     Running `target/debug/grrs`
error: The following required arguments were not provided:
    <pattern>
    <path>

USAGE:
    grrs <pattern> <path>

For more information try --help

传递参数运行它

$ cargo run -- some-pattern some-file
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target/debug/grrs some-pattern some-file`
pattern: "some-pattern", path: "some-file"

输出演示了我们的程序成功将参数解析到了 Cli 结构体中。