解析命令行参数
我们的 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");
CLI 参数的数据类型
与其将它们视为单纯的一堆文本,不如将 CLI 参数看成程序输入的自定义的数据类型。
注意 grrs foobar test.txt
:这里有两个参数,首先是 pattern
(查看的字符串),
而后是 path
(查找的文件路径)。
那么,关于这些参数,首先,这两个参数都是程序所必须的,因为我们并未提供默认值,
所以用户需要在使用此程序时提供这两个参数。此外,关于参数的类型:pattern
应该是一个字符串;第二个参数则应是文件的路径。
在 Rust 中,根据所处理的数据去构建程序是很常见的,
因此这种看待参数的方法对我们接下来的工作很有帮助,让我们以此开始
(在 src/main.rs
,fn main()
之前):
struct Cli {
pattern: String,
path: std::path::PathBuf,
}
这定义了一个新的,存储了 pattern
和 path
两项数据的结构体(struct
)。
我们并没有获得程序运行所需的参数,而这正是现在需要去做的。 一种做法是,我们可以手动解析从系统获取的参数列表并以此生成一个结构体,像这样:
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),
};
这种方式能正常工作,用起来却不是很方便。如何去满足像 --pattern="foo"
或
--pattern "foo"
这种参数输入?又如何实现 --help
?
使用 StructOpt 解析 CLI 参数
使用现成的库来实现参数的解析,这是更明智的选择。
clap
是当前最受欢迎的解析命令行参数的库。它提供了所有你需要的功能,
如子命令、自动补全和完善的帮助信息。
structopt
库基于 clap
,并提供了一个“派生”宏来为结构体生成 clap
代码。
这可太棒了:我们只需要去声明一个结构体,它就会生成将参数解析为结构体字段的代码!
首先,我们需要在 Cargo.toml
文件的 [dependencies]
字段里添加上
structopt = "0.3.13"
来导入 structopt
。
现在,在我们的代码中,导入,use structopt::StructOpt
,在之前创建的
struct Cli
的正上方添加上 #[derive(StructOpt)]
,并顺便写一些文档注释。
它现在看起来是这样的:
use structopt::StructOpt;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(StructOpt)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
#[structopt(parse(from_os_str))]
path: std::path::PathBuf,
}
本例中,在我们的 Cli
结构体下方即是 main
函数。
当运行程序时,就会调用这个函数,第一行如下:
fn main() {
let args = Cli::from_args();
}
这将尝试解析参数并存储到 Cli
结构体中。
但如果解析失败会怎样?这就是使用此方法的美妙之处:Clap 知道它需要什么字段,
及所需字段的类型。它可以自动生成一个不错的 --help
信息,
并会依错误给出一些建议——输入的参数应该是 --output
而你输入的是 --putput
。
Wrapping up
你的代码现在看起来应该是这样的:
#![allow(unused)]
use structopt::StructOpt;
/// Search for a pattern in a file and display the lines that contain it.
#[derive(StructOpt)]
struct Cli {
/// The pattern to look for
pattern: String,
/// The path to the file to read
#[structopt(parse(from_os_str))]
path: std::path::PathBuf,
}
fn main() {
let args = Cli::from_args();
}
无参数运行时:
$ 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
后添加 --
,并在其后跟上参数来传递:
$ cargo run -- some-pattern some-file
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/grrs some-pattern some-file`
如你所见,没有任何输出。这非常好,意味着我们的程序运行完成且没有错误!