[TOC]
通过 clap3学习rust derive宏
tldr: 本文主要介绍了 clap3 的 builder api, derive api 的使用;然后通过 derive api 源码为例学习 derive macro;
clap一个用来编写cli app的 rust 库,主要用来解析命令行的参数;下面是一个使用 clap3 builder api的例子:
clap3 builder api example (V1)
use clap::{Arg, App, Parser};
fn main() {
let matches = App::new("Novice Program")
.version("1.0")
.author("cywang.master@gmail.com")
.arg(Arg::new("hostname")
.short('h')
.long("hostname")
.help("Server hostname")
.takes_value(true)
)
.arg(Arg::new("port")
.short('p')
.long("port")
.help("Server port (default: 6379)")
.default_value("6379")
.takes_value(true)
)
.arg(Arg::new("prod")
.long("prod")
.help("Active prod mode")
.takes_value(false)
)
.get_matches();
let hostname = matches.value_of("hostname").unwrap();
let port = matches.value_of("port").unwrap();
let prod = matches.is_present("prod");
println!("hostname={}, port={}, prod={}", hostname, port, prod);
}
通过 clap::builder::command::App::new() 初始化一个App struct; 最后调用 get_matches() 方法去解析命令行参数到App struct;
App struct 定义如下: (只展示了常用的部分字段)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct App<'help> {
id: Id,
name: String,
author: Option<&'help str>,
version: Option<&'help str>,
usage_str: Option<&'help str>,
usage_name: Option<String>,
settings: AppFlags,
args: MKeyMap<'help>,
}
执行 cargo run -- --help 时显示结果如下图:
第一行是 name version;第二行是 author 信息;接下来是usage信息;最后的 OPTIONS 就是我们定义的args, --help, --version是clap默认定义的;
help.png
提升 builder api 的开发体验 (V2)
上面的builder api example已经能够把参数解析到App struct了, 但开发体验特别差;
试想一下你定义完args,合作开发的同伴还需要看上面的 Arg::new("name")...才能知道
该怎么使用你定义的args; 自然而然的一个提升开发体验的方法就是把所有定义的args放到
一个struct里面,同伴只需要使用实例化的struct就可以直接使用预先定义的args.
优化后的代码只是添加了一个 Config struct,然后手动从App args里面取值然后赋值给Config的成员;
use clap::{Arg, App, Parser};
#[derive(Debug)]
#[allow(dead_code)]
struct Config {
hostname: String,
port: String,
prod: bool,
}
fn main() {
let matches = App::new("Novice Program")
.version("1.0")
.author("cywang.master@gmail.com")
.arg(Arg::new("hostname")
.short('h')
.long("hostname")
.help("Server hostname")
.takes_value(true)
)
.arg(Arg::new("port")
.short('p')
.long("port")
.help("Server port (default: 6379)")
.default_value("6379")
.takes_value(true)
)
.arg(Arg::new("prod")
.long("prod")
.help("Active prod mode")
.takes_value(false)
)
.get_matches();
// let hostname = matches.value_of("hostname").unwrap();
// let port = matches.value_of("port").unwrap();
// let prod = matches.is_present("prod");
// println!("hostname={}, port={}, prod={}", hostname, port, prod);
let config = Config {
hostname: matches.value_of("hostname").unwrap().to_string(),
port: matches.value_of("port").unwrap().to_string(),
prod: matches.is_present("prod"),
};
println!("{:#?}", config)
}
使用 derive api 提升开发体验 (V3)
上面的写法太啰嗦了,定义args和定义struct是工作量的上的重复,从App struct取值手动赋值到Config也是一个很人肉的方法;于是就有了derive api来提升开发体验;代码如下:
use clap::{Arg, App, Parser};
#[derive(Debug, Parser)]
#[clap(version, author)]
struct DeriveConfig {
#[clap(short, long, help = "Server hostname")]
hostname: String,
#[clap(short, long, default_value = "6379", help = "Server port (default: 6379)")]
port: String,
#[clap(long, help = "Active prod mode")]
prod: bool,
}
fn main() {
let config: DeriveConfig = DeriveConfig::parse();
println!("{:#?}", config);
}
上面的代码非常简洁,怎么做到的呢?其实就是通过rust的 derive macro来实现上面V2版本中我们手动写的那堆builder api的代码;
上面的#[derive(Parser)] #[clap(long...)]... 相当于做了一些配置,用于编写derive宏时根据配置生成代码。
查看宏生成的代码
我们可以对 【使用 derive api 提升开发体验 (V3)】中的代码使用cargo expand 来查看rust 宏生成的代码;
这里有一个知识点,使用宏和手动解析赋值的性能完全一致,因为宏的代码生成发生在编译阶段;
通过宏代码查看整个解析赋值过程
main方法里面调用了parse方法,DeriveConfig::parse(),可以从宏生成代码里面发现这一行
impl clap::Parser for DeriveConfig {}
<span id="parse"></span>
这里给 DeriveConfig 实现了 Parser trait; 然后看看clap3源码的 parse()方法的默认实现如下:
fn parse() -> Self {
let mut matches = <Self as CommandFactory>::command().get_matches();
let res = <Self as FromArgMatches>::from_arg_matches_mut(&mut matches)
.map_err(format_error::<Self>);
match res {
Ok(s) => s,
Err(e) => {
// Since this is more of a development-time error, we aren't doing as fancy of a quit
// as `get_matches`
e.exit()
}
}
}
这里首先调用了clap::derive::CommandFactory的command方法,看看clap3的源码command方法的实现里面调用了自身的into_app方法
fn command<'help>() -> Command<'help> {
#[allow(deprecated)]
Self::into_app()
}
然后看derive宏生产的代码里面对into_app的实现
impl clap::CommandFactory for DeriveConfig {
fn into_app<'b>() -> clap::Command<'b> {
let __clap_app = clap::Command::new("clap3");
<Self as clap::Args>::augment_args(__clap_app)
}
fn into_app_for_update<'b>() -> clap::Command<'b> {
let __clap_app = clap::Command::new("clap3");
<Self as clap::Args>::augment_args_for_update(__clap_app)
}
}
into_app首先调用了clap::Command::new, 和我们builder api里面的App::new等价;
然后调用了augment_args方法,看看derive宏生产的augment_args方法
fn augment_args<'b>(__clap_app: clap::Command<'b>) -> clap::Command<'b> {
{
let __clap_app = __clap_app;
let __clap_app = __clap_app.arg({
#[allow(deprecated)]
let arg = clap::Arg::new("hostname")
.takes_value(true)
.value_name("HOSTNAME")
.required(true && clap::ArgAction::StoreValue.takes_values())
.validator(|s| ::std::str::FromStr::from_str(s).map(|_: String| ()))
.value_parser(clap::builder::ValueParser::string())
.action(clap::ArgAction::StoreValue);
let arg = arg.short('h').long("hostname").help("Server hostname");
arg
});
let __clap_app = __clap_app.arg({
#[allow(deprecated)]
let arg = clap::Arg::new("port")
.takes_value(true)
.value_name("PORT")
.required(false && clap::ArgAction::StoreValue.takes_values())
.validator(|s| ::std::str::FromStr::from_str(s).map(|_: String| ()))
.value_parser(clap::builder::ValueParser::string())
.action(clap::ArgAction::StoreValue);
let arg = arg
.short('p')
.long("port")
.default_value("6379")
.help("Server port (default: 6379)");
arg
});
let __clap_app = __clap_app.arg({
#[allow(deprecated)]
let arg = clap::Arg::new("prod").takes_value(false);
let arg = arg.long("prod").help("Active prod mode");
arg
});
__clap_app.version("0.1.0").author("gimmi7")
}
}
这段代码很熟悉把,就是我们通过builder api构造App struct时手动写的代码;
再看看 <a href="#parse">clap3源码的 parse()方法的默认实现</a>
执行完 command方法后和我们在 builder api 里面做的一样,调用get_matches()解析命令行参数;
按照我们优化版本的builder api,接下来需要做的事情,就是从App struct把arg取出来赋值给定义struct;
parse()方法接下来调用了 from_arg_matches_mut(&mut matches), 看看derive宏生成的from_arg_matches_mut;
fn from_arg_matches_mut(
__clap_arg_matches: &mut clap::ArgMatches,
) -> ::std::result::Result<Self, clap::Error> {
#![allow(deprecated)]
let v = DeriveConfig {
hostname: __clap_arg_matches
.get_one::<String>("hostname")
.map(|s| ::std::ops::Deref::deref(s))
.ok_or_else(|| {
clap::Error::raw(clap::ErrorKind::MissingRequiredArgument, {
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["The following required argument was not provided: "],
&[::core::fmt::ArgumentV1::new_display(&"hostname")],
));
res
})
})
.and_then(|s| {
::std::str::FromStr::from_str(s).map_err(|err| {
clap::Error::raw(clap::ErrorKind::ValueValidation, {
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["Invalid value for ", ": "],
&[
::core::fmt::ArgumentV1::new_display(&"hostname"),
::core::fmt::ArgumentV1::new_display(&err),
],
));
res
})
})
})?,
port: __clap_arg_matches
.get_one::<String>("port")
.map(|s| ::std::ops::Deref::deref(s))
.ok_or_else(|| {
clap::Error::raw(clap::ErrorKind::MissingRequiredArgument, {
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["The following required argument was not provided: "],
&[::core::fmt::ArgumentV1::new_display(&"port")],
));
res
})
})
.and_then(|s| {
::std::str::FromStr::from_str(s).map_err(|err| {
clap::Error::raw(clap::ErrorKind::ValueValidation, {
let res = ::alloc::fmt::format(::core::fmt::Arguments::new_v1(
&["Invalid value for ", ": "],
&[
::core::fmt::ArgumentV1::new_display(&"port"),
::core::fmt::ArgumentV1::new_display(&err),
],
));
res
})
})
})?,
prod: ::std::convert::From::from(__clap_arg_matches.is_present("prod")),
};
::std::result::Result::Ok(v)
}
这段代码还是很熟悉把,就是我们在优化版本的 builder api 实现里面从 matches 取出args赋值到定义的struct的过程;
至此,clap3 derive宏做了生么事情已经完全清晰了,接下来我们就要看看怎么利用derive宏来实现自动生成这些代码;
首先是clap::Parse的proc_macro_derive定义;将TokenStream解析为AST(abstract syntax tree),然后调用derives::derive_parser(&input),生成代码
#[proc_macro_derive(Parser, attributes(clap, structopt))]
#[proc_macro_error]
pub fn parser(input: TokenStream) -> TokenStream {
let input: DeriveInput = parse_macro_input!(input);
derives::derive_parser(&input).into()
}
derive_parse主要调用了 gen_for_struct 方法
fn gen_for_struct(
name: &Ident,
generics: &Generics,
fields: &Punctuated<Field, Comma>,
attrs: &[Attribute],
) -> TokenStream {
let into_app = into_app::gen_for_struct(name, generics, attrs);
let args = args::gen_for_struct(name, generics, fields, attrs);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics clap::Parser for #name #ty_generics #where_clause {}
#into_app
#args
}
}
let into_app = into_app::gen_for_struct(name, generics, attrs);生成了 clap::CommandFactory::into_app()的代码;
let args = args::gen_for_struct(name, generics, fields, attrs);生成了 clap::FromArgMatches::from_arg_matches_mut()以及clap::Args::augment_args的代码;
生成代码的逻辑其实就是根据我们定义的struct的 fields 以及在每个filed上面写的clap attribute,来构造App struct 以及为我们自定义的 struct 赋值;
整个宏的编写过程就是: 从TokenStream -> 通过syn::parse解析得到 AST -> 根据AST编写需要生成的rust代码 -> 通过 quote::quote!(rust代码) 将rust代码转换成TokenStream;
当然这里面的难点在于没有一个足够好用的IDE能够在编写宏代码时自动提示,自动补全,所以编写宏的过程开发体验不会太好。
好处当然就很大了,就像clap derive api,可以把一个50多行的代码压缩到20行解决,如果你自定义的命令行参数越多,压缩比也会越高。
网友评论