解析命令行参数

我们 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(要查找的文件)。

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

在 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: 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 结构中。