02-解析命令行参数

解析 CLI 参数(含 clap)

译文 · 基于 Command Line Applications in Rust

解析命令行参数

原文链接: https://rust-cli.github.io/book/tutorial/cli-args.html

我们的 CLI 工具一次典型调用会像这样:

1
$ grrs foobar test.txt

我们期望程序查看 test.txt, 并打印出包含 foobar 的行。 但这两个值该怎么拿到?

程序名后面的文本通常叫做 “命令行参数”(command-line arguments), 或“命令行标志”(command-line flags) (尤其是形如 --this 的时候)。 在操作系统内部,它们通常表示成 字符串列表。一般由空格分隔。

关于这些参数有很多种理解方式, 以及如何把它们解析成 更便于使用的形式。 你还需要告诉程序的用户 需要提供哪些参数, 以及期望的格式是什么。

获取参数

标准库提供了函数 std::env::args(),它会给你一个包含给定参数的[迭代器]。 第一项(索引 0)是用来启动程序的名称 (例如 grrs)。后面的才是用户随后写下的内容。

用这种方式拿到原始参数很直接(在文件 src/main.rs 中):

1
2
3
4
5
6
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 运行它, 在 -- 后面写上要传入的参数:

1
2
3
4
$ 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() { 之前):

1
2
3
4
struct Cli {
    pattern: String,
    path: std::path::PathBuf,
}

这定义了一个新结构体(struct), 有两个字段用来存数据:pattern 和 path。

现在,仍需把实际参数转换成这种形式。 一种做法是手动解析操作系统给我们的字符串列表, 然后自己构造这个结构体。 大概会像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 引进来:在 Cargo.toml 的 [dependencies] 段加入 clap = { version = "4.0", features = ["derive"] }。

然后可以在代码里写 use clap::Parser;, 并在 struct Cli 正上方加上 #[derive(Parser)]。 顺带也写一些文档注释。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use clap::Parser;

/// 在文件中搜索模式并显示包含它的行。
#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件路径
    path: std::path::PathBuf,
}

在 Cli 结构体正下方,模板里有它的 main 函数。 程序启动时会调用这个函数:

1
2
3
4
5
fn main() {
    let args = Cli::parse();

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

这会尝试把参数解析进我们的 Cli 结构体。

但如果失败了呢? 这正是这种方法的妙处: Clap 知道期望哪些字段 以及它们的格式。 它可以自动生成漂亮的 --help 信息, 也能在你写成 --putput 时给出很好的错误, 建议你改用 --output。

收尾

你的代码现在应如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use clap::Parser;

/// 在文件中搜索模式并显示包含它的行。
#[derive(Parser)]
struct Cli {
    /// 要查找的模式
    pattern: String,
    /// 要读取的文件路径
    path: std::path::PathBuf,
}

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

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

不带任何参数运行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ 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

传入参数运行:

1
2
3
4
$ 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 结构体。

最后修改 August 11, 2026: 更新 (70a5af133)