解析命令行参数
我们的 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
),它有两个字段用于存储数据:pattern
和 path
。
现在,我们仍然需要将程序实际获取的参数转换为这种形式。一种选择是手动解析我们从操作系统获得的字符串列表,并自己构建结构体。它看起来会像这样
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
结构体中。