0%

Rust每日一库之Clap

缘起

主要对go每日一库Rust每周一库的拾人牙慧,不过学习嘛,不寒碜。哈哈。

今天是第一天,我们从命令行参数解析库——Clap开始。

简介

相较于golangrust的解析命令参数等非语言核心能力并非是在核心库中直接提供的,而是由三方库提供。

三方库就会有一个选择问题,特别是对于不熟悉的新手。

我们可以通过lib.rs网站中Categories中选择Command-line interface来查看对应的库排名来初步选择。

如:https://lib.rs/keywords/argument

对于参数解析,使用量最大的就是Clap

仓库:https://github.com/clap-rs/clap

文档:https://docs.rs/clap/3.0.0-beta.5/clap/

视频教程:https://www.youtube.com/playlist?list=PLza5oFLQGTl2Z5T8g1pRkIynR3E0_pc7U

快速使用

clap作为依赖项加进Cargo.toml

1
2
[dependencies]
clap="2.33.3"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extern crate clap;

use clap::{App, Arg};

fn main() {
let matched = App::new("MyApp")
.arg(Arg::with_name("config")
.short("c")
.long("config")
.value_name("FILE")
.help("demo arg for cmd")
)
.get_matches();
if let Some(c) = matched.value_of("config") {
println!("value of config: {}", c);
}

println!("hello");
}

不用参数运行

1
2
$ cargo run
hello

带参数运行

1
2
3
$ cargo run -- --config haha
value of config: haha
hello

自带的帮助信息

1
2
3
4
5
6
7
8
9
10
11
12
$ cargo run -- -h
MyApp

USAGE:
basic [OPTIONS]

FLAGS:
-h, --help Prints help information
-V, --version Prints version information

OPTIONS:
-c, --config <FILE> demo arg for cmd

可以看出来自带的帮助信息还可以安放更多的元信息,接下来就让我们来尽量尝试一下(篇幅过长,请选择性使用)

1
2
3
4
5
6
7
App::new("MyApp")
.version("1.0")
.author("Chaos Mo")
.before_help("===haha before help===")
.after_help("===haha after help===")
.about("This is demo app for clap")
.help_message("Print custom help information")

运行效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cargo run --bin basicmeta -- -h
===haha before help===

MyApp 1.0
Chaos Mo
This is demo app for clap

USAGE:
basicmeta [OPTIONS]

FLAGS:
-h, --help Print custom help information
-V, --version Prints version information

OPTIONS:
-c, --config <FILE> demo arg for cmd

===haha after help===
$ cargo run --bin basicmeta -- -V
MyApp 1.0

更多的方法,请参看文档。

参数类型

Rust是一门强类型且类型安全的静态语言

clap中,只要你的目标参数类型是实现了std::str::FromStr,就可以使用转换宏将其转换成目标类型。同时支持单个,多个(Vec<T>)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
extern crate clap;

use clap::{App, value_t, values_t};

fn main() {
let matches = App::new("myapp")
// 等价于
// .args(
// &[
// Arg::with_name("seq")
// .multiple(true)
// .help("A sequence of whole positive numbers, i.e. 20 25 30"),
// Arg::with_name("len")
// .short("l")
// .long("len")
// .value_name("len")
// .help("A length to use, defaults to 10 when omitted")
// ]
// )
.args_from_usage(
"<seq>... 'A sequence of whole positive numbers, i.e. 20 25 30'
--len -l [len] 'A length to use, defaults to 10 when omitted'
--offline -o [isoffline] 'is computer offline'",
)
.get_matches();

let len: u32 = value_t!(matches, "len", u32).unwrap_or(10);
println!("len ({}) + 2 = {}", len, len + 2);
let online: bool = value_t!(matches, "offline", bool).unwrap_or(false);
if online {
println!("computer is online");
} else {
println!("computer is offline");
}
// 如果类型不满足,可以中断程序退出
for v in values_t!(matches, "seq", u32).unwrap_or_else(|e| e.exit()) {
println!("Sequence part {} + 2: {}", v, v + 2);
}
}

运行结果

1
2
3
4
5
6
$ cargo run --bin typedvalue -- 1 2 3 --len 12 -o true
len (12) + 2 = 14
computer is online
Sequence part 1 + 2: 3
Sequence part 2 + 2: 4
Sequence part 3 + 2: 5

测试不满足的类型安全的输入参数

1
2
3
4
$ cargo run --bin typedvalue -- 1 2 3.1 --len 12 -o true
len (12) + 2 = 14
computer is online
error: Invalid value: The argument '3.1' isn't a valid value

总结:

利用value_tvalues_t可以将对应的参数转化为需要的类型。若类型不满足,则需要通过对Result进行判断。

选项格式

clap库支持三种命令

1
2
3
-o
--optimize=3
--optimize 3

-表示短选项,–表示长选项,同时支持多个重复参数,重复参数格式支持-o -o -o-oo等多种形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
extern crate clap;
use clap::{App, Arg};

fn main() {
let matches = App::new("DemoApp")
.version("1.0")
.author("Chaos Mo")
.about("Demo App About Information")
.arg(
Arg::with_name("config")
.short("c")
.long("config")
.value_name("FILE")
.help("demo arg for cmd"),
)
.arg(
Arg::with_name("optimize")
.short("o")
.long("optimize")
.multiple(true)
.help("Sets the level of optimize"),
)
.get_matches();
if let Some(c) = matches.value_of("config") {
println!("value of config: {}", c);
}

match matches.occurrences_of("optimize") {
0 => println!("No optimize"),
1 => println!("Basic optimize"),
2 => println!("Better optimize"),
3 | _ => println!("Are you kidding me?"),
}
}

运行效果

1
2
3
$ cargo run --bin styledargs -- --config=haha -o --optimize
value of config: haha
Better optimize

clap支持多重子参数嵌套

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
extern crate clap;

use clap::{App, Arg, SubCommand};

fn main() {
let matches = App::new("DemoApp")
.subcommand(
SubCommand::with_name("install")
.about("controls install features")
.version("1.2")
.author("Chaos Mo")
.arg(
Arg::with_name("force")
.short("f")
.long("force")
.help("install with force arg"),
),
)
.get_matches();
if let Some(matches) = matches.subcommand_matches("install") {
if matches.is_present("force") {
println!("force is not right...");
} else {
println!("general install...");
}
}
}

运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cargo run --bin subdemo -- install -h
subdemo-install 1.2
Chaos Mo
controls install features

USAGE:
subdemo install [FLAGS]

FLAGS:
-f, --force install with force arg
-h, --help Prints help information
-V, --version Prints version information
$ cargo run --bin subdemo -- install -f
force is not right...

另外,clap也支持直接使用dsl形式的参数字符串解析模式。

1
[explicit name] [short] [long] [value names] [help string]

另一种定义选项的方式

clap支持使用YAML文件来构建CLI并保持Rust源代码整洁或者通过为每个本地化使用不同的YAML文件来支持多个本地化翻译,但是此方法会有略微损失一些性能。

这里的依赖与其他例子有稍许区别

1
clap={version="2.33.3", features=["yaml"]}

先创建一个cli.yml保存选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
name: ymldemo
version: "1.1"
author: Chaos Mo
about: Demo App for Yaml Cli
args:
- config:
short: c
long: config
value_name: FILE
help: demo arg for cmd
takes_value: true
subcommands:
- install:
about: controls install features
version: "1.2"
author: Chaos Mo
args:
- force:
short: f
long: force
help: force to install features
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[macro_use]
extern crate clap;
use clap::App;

fn main() {
let yaml = load_yaml!("cli.yml");
let matches = App::from_yaml(yaml).get_matches();

let config = matches.value_of("config").unwrap_or("default.conf");
println!("Value for config: {}", config);

if let Some(matches) = matches.subcommand_matches("install") {
if matches.is_present("force") {
println!("force is not right...");
} else {
println!("general install...");
}
}
}

测试

一个优秀的代码,都是可以进行良好测试的。

虽然解析逻辑应该不属于业务核心逻辑,但Clap提供了良好的测试方式来进行必要的测试途径。

默认的get_matches实际上是接受 env::args_os输入作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
extern crate clap;

use std::{error::Error, str::Matches};

use clap::{App, Arg};

fn main() {
let app = construct_app();
let matches = app.get_matches();
let config = matches.value_of("config").unwrap_or("default.conf");
println!("Value for config: {}", config);
match matches.occurrences_of("optimize") {
0 => println!("No optimize"),
1 => println!("Basic optimize"),
2 => println!("Better optimize"),
3 | _ => println!("Are you kidding me?"),
}
println!("hello");
}

fn construct_app() -> App<'static, 'static> {
App::new("testclap")
.about("about test clap information")
.author("Chaos Mo")
.arg(
Arg::with_name("config")
.short("c")
.long("config")
.takes_value(true)
.value_name("FILE")
.help("config information"),
)
.arg(
Arg::with_name("optimize")
.short("o")
.long("optimize")
.multiple(true)
.help("optimize level"),
)
}

对于这块代码的单元测试,我们可以使用get_matches_from来进行手动灌注参数调用。

1
2
3
4
5
6
7
8
#[test]
fn test_parse() {
let arg_vec = vec!["--", "-oo", "-c", "haha"];
let matches = construct_app().get_matches_from(arg_vec);
assert!(matches.is_present("optimize"));
assert_eq!(matches.occurrences_of("optimize"), 2);
assert_eq!(matches.value_of("config"), Some("haha"));
}

参考

【Rust每周一库】Clap - 强大的命令行参数解析+帮助说明生成库