Rust每周一库: structopt

标准库提供了std::env::args()用来获取命令行的参数,第一个值是程序的名称,这和其它语言中的获取参数的方式类似:

1
2
3
4
5
6
7
let args: Vec<String> = env::args().collect();
let query = &args[1];
let filename = &args[2];
println!("Searching for {}", query);
println!("In file {}", filename);

但是在产品开发的过程中,我们需要比较多的程序参数,并且需要一定的规则和校验,这个时候我们就需要使用其它的一些库对这些参数进行解析,比如structopt库。

structopt可以方便的将命令行参数解析为一个struct。

下面是官方的一个例子,可以cargo run -- --help测试一下:

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
41
42
43
44
45
46
use std::path::PathBuf;
use structopt::StructOpt;
/// A basic example
#[derive(StructOpt, Debug)]
#[structopt(name = "basic")]
struct Opt {
// A flag, true if used in the command line. Note doc comment will
// be used for the help message of the flag. The name of the
// argument will be, by default, based on the name of the field.
/// Activate debug mode
#[structopt(short, long)]
debug: bool,
// The number of occurrences of the `v/verbose` flag
/// Verbose mode (-v, -vv, -vvv, etc.)
#[structopt(short, long, parse(from_occurrences))]
verbose: u8,
/// Set speed
#[structopt(short, long, default_value = "42")]
speed: f64,
/// Output file
#[structopt(short, long, parse(from_os_str))]
output: PathBuf,
// the long option will be translated by default to kebab case,
// i.e. `--nb-cars`.
/// Number of cars
#[structopt(short = "c", long)]
nb_cars: Option<i32>,
/// admin_level to consider
#[structopt(short, long)]
level: Vec<String>,
/// Files to process
#[structopt(name = "FILE", parse(from_os_str))]
files: Vec<PathBuf>,
}
fn main() {
let opt = Opt::from_args();
println!("{:#?}", opt);
}

我们定义一个struct:来保存命令行参数: Opt, 这个struct的定义的时候使用来宏来定义参数的一些属性。
然后通过一行代码let opt = Opt::from_args();就可以把命令行参数解析为Opt的一个实例。

官方库提供了很多的例子,可以用来了解和学习structopt的功能和使用方法。

structopt 使用作为参数的解析,但是通过宏的方式,大大简化了clap的使用难度。

首先我们看看structopt通过宏对上面的例子做了什么处理。

structopt 宏的花招

structoptOpt 实现了 structopt::StructOpt trait:

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
#[allow(unused_variables)]
impl ::structopt::StructOpt for Opt {
fn clap<'a, 'b>() -> ::structopt::clap::App<'a, 'b> {
let app = ::structopt::clap::App::new("basic")
.about("A basic example")
.version("0.1.0");
Self::augment_clap(app)
}
fn from_clap(matches: &::structopt::clap::ArgMatches) -> Self {
Opt {
debug: matches.is_present("debug"),
verbose: { |v| v as _ }(matches.occurrences_of("verbose")),
speed: matches
.value_of("speed")
.map(|s| ::std::str::FromStr::from_str(s).unwrap())
.unwrap(),
output: matches
.value_of_os("output")
.map(::std::convert::From::from)
.unwrap(),
nb_cars: matches
.value_of("nb-cars")
.map(|s| ::std::str::FromStr::from_str(s).unwrap()),
level: matches
.values_of("level")
.map(|v| {
v.map(|s| ::std::str::FromStr::from_str(s).unwrap())
.collect()
})
.unwrap_or_else(Vec::new),
files: matches
.values_of_os("file")
.map(|v| v.map(::std::convert::From::from).collect())
.unwrap_or_else(Vec::new),
}
}
}

fn clap<'a, 'b>() -> clap::App<'a, 'b> 生成一个clap::App,这个App的名称就是我们定义的basic,我们把没有定义about属性,所以这里它取注释作为about描述信息,这是clap库使用的姿势,clap应用定义一些属性和参数:

但是我们使用structopt并不是要创建一个clap应用,只是用来解析参数,映射成一个struct,所以这里创建的clap app只是一个辅助处理参数的对象。

clap()方法中还调用了augment_clap(app)函数,这个函数在下面定义,定义了App的参数。

fn from_clap(matches: &ArgMatches) -> Self是将clap的App对象中的参数映射成Opt的字段。

比如speed字段:

1
2
3
4
speed: matches
.value_of("speed")
.map(|s| ::std::str::FromStr::from_str(s).unwrap())
.unwrap(),

配置clap app Args的方法在函数augment_clap中:

参数的属性配置依照Opt中各个字段的定义而生成。

这样,当我们在代码中调用let opt = Opt::from_args()时候,实际调用from_clap(&Self::clap().get_matches())

整体可以看到,structopt其实就是把宏的各种定义转换成clap的配置,我们可以学习它的宏的复杂的运用。

属性

你定义的struct会映射成 clap::App, 而这个struct的非子命令字段会映射成clap::Arg。
通过属性#[structopt(...)]进行设置,所以让我们看看它的属性。

structopt的属性可以分为两类:

  • structopt自己的magical method: structopt自己使用,attr = ["whatever"]或者attr(args...)格式
  • raw attributes: 映射成clap::Arg/App的方法调用, #[structopt(raw(...))]格式

raw 属性/方法

属性和 clap::App/clap::Arg一一对应。

格式:

#[structopt(method_name = single_arg)] 或者 #[structopt(method_name(arg1, arg2))]

magical 属性/方法

比如nameversionno_versionauthoraboutshortlongrename_allparseskipflattensubcommand

详细信息可以参考: Magical methods

类型

定义了一些内置类型,以及对应的clap方法。

  • bool: .takes_value(false).multiple(false)
  • Option: .takes_value(true).multiple(false)
  • Option>:.takes_value(true).multiple(false).min_values(0).max_values(1)
  • Vec: .takes_value(true).multiple(true)
  • Option: .takes_values(true).multiple(true).min_values(0)
  • T: FromStr: .takes_value(true).multiple(false).required(!has_default)

子命令

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
#[derive(StructOpt)]
#[structopt(about = "the stupid content tracker")]
enum Git {
Add {
#[structopt(short)]
interactive: bool,
#[structopt(short)]
patch: bool,
#[structopt(parse(from_os_str))]
files: Vec<PathBuf>
},
Fetch {
#[structopt(long)]
dry_run: bool,
#[structopt(long)]
all: bool,
repository: Option<String>
},
Commit {
#[structopt(short)]
message: Option<String>,
#[structopt(short)]
all: bool
}
}

定制字符串解析

如果类型没有实现FromStr trait, 或者你就想定制解析方式,你可以设置自定义的解析方法。

更多的信息可以查看structopt的文档doc.rs/structopt

在开发cli/terminal应用程序的时候,如果你不想这种声明式的获取参数的方式,那么你可以直接使用clap库, 这个库功能强大,也被广泛使用。

也有一些基于structopt的扩展库:

paw将rust main函数转换成c风格的带传入参数的main函数,也可以结合structopt使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use std::io::prelude::*;
use std::net::TcpListener;
// With the "paw" feature enabled in structopt
#[derive(structopt::StructOpt)]
struct Args {
/// Port to listen on.
#[structopt(short = "p", long = "port", env = "PORT", default_value = "8080")]
port: u16,
/// Address to listen on.
#[structopt(short = "a", long = "address", default_value = "127.0.0.1")]
address: String,
}
#[paw::main]
fn main(args: Args) -> Result<(), std::io::Error> {
let listener = TcpListener::bind((&*args.address, args.port))?;
println!("listening on {}", listener.local_addr()?);
for stream in listener.incoming() {
stream?.write(b"hello world!")?;
}
Ok(())
}