clap是一个非常方便地能够用来构建命令行工具的一个crate,在rust中的定位类似于go语言中的cobra。
clap提供了两种使用方式,一种是使用代码编写的builder方式,另一种则是借助rust强大的宏能力提供的derive方式。
这里只介绍derive方式,使用起来比较简单,优雅。
快速启动
引入clap并启用derive
cargo add clap --features derive初始化命令结构
程序本身就是一个命令,我们从这个命令作为起始,如下
#[derive(Parser)]
#[command(
author,
version,
about = "A http utility",
long_about = "Httpurl is a http utility for simple http requests"
)]
pub struct Cli {}简单进行一下说明
#[derive(Parser)]是一个派生宏,为Cli实现了Parse()方法。只要我们调用这个方法,就可以将命令行参数进行解析#[command]是一个属性宏,用来生成代码,辅助派生宏Parser来提供解析命令参数的能力。还可以定义程序级别的说明。
在程序当中的使用也非常简单,命令就会解析到返回的cli对象当中了。
let cli = Cli::parse();命令的结构
在命令行程序当中,整个程序都可以看作是一个命令,由以下部分组成
- 子命令:每个命令下可以再嵌套子命令
- 选项:分为
-开头的短选项和--开头的长选项 - 参数:即跟在命令/子命令后面,非选项的部分
Note
这里需要额外提一下。
[command]会默认给命令添加两个选项version和help,用来打印版本信息和帮助信息的。 version可以在宏声明中指定,否则使用Cargo.toml中的版本。帮助信息是根据每个命令的信息生成的。
clap中命令的定义
首先在一个命令当中,子命令,选项,参数这三部分都是可选项,在rust中,也就是都可以使用Option进行声明的,在缺失的情况下不会报错,不是Option的话就是必需的参数,缺失是会报错的。
- 子命令:使用
#[command(subcommand)]在一个结构体中修饰Enum,当然Enum自身要求是被派生宏SubCommand修饰的。 - 选项:使用
#[arg]修饰字段 - 参数:不使用任何属性宏修饰,默认就是参数 在这些字段或者结构体,Enum上面添加文档注释,会用来生成帮助信息。
接下来按照使用场景来分别说明如何定义命令。
选项的定义
使用#[arg]用来定义选项,我们可以在这个属性宏中定义各种属性来完成选项的构建,可以使用的属性其实都是Arg这个结构体实现的方法,属性宏帮助我们将设置的值作为参数传递给了这个方法来完成Arg结构体的构建。
所以需要知道可以使用哪些属性去文档查看一下Arg提供了哪些方法就行,这样builder模式和derive模式达成了一致的逻辑。
定义选项的长短形式
使用short和long属性即可,默认短标识为字段首字母,长标识为字段名。也可以手动指定需要的值
#[arg(short, long)]
config: Option<PathBuf>,修改help信息中的选项值的名称
方便让我们理解要传入的选项的含义,可以在help信息中看到这个信息
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,接收多个值的选项
默认选项是接收动作ArgAction是Set,对于需要接受多个值的场景,可以将字段类型设置为Vec,这样就将接收动作覆盖为Append了。
#[arg(short, long)]
name: Vec<String>,例如这个示例,就可以通过--name alice --name bob传递多个选项值。
用作开关的选项
就是字段类型为布尔类型的选项,根据是否传递选项来设置为true和false,而不需要传值
#[arg(short, long)]
verbose: bool,布尔类型的接收器为SetTrue,传递了这个选项字段值就设置为true。
不过这个用作开关的选项只能传递一次,多次传递可以使用下面的方式
累加计数的选项
可以累积某个选项传递的次数,通过设置接收器为Count
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,子命令的选项
子命令中选项的定义是一样的逻辑,没有什么特别的。
#[derive(Subcommand)]
enum Commands {
/// Adds files to myapp
Add { name: Option<String> },
}子命令的选项单独抽离
如果子命令有很多的话,都写在每个枚举对象当中显然不太合适,可以将他们单独抽离出来,抽离出来之后和子命令相似,需要派生宏来为这个结构体实现arg trait
#[derive(Subcommand)]
enum Commands {
/// Adds files to myapp
Add(AddArgs),
}
#[derive(Args)]
struct AddArgs {
name: Option<String>,
}version选项传播到子命令
#[command(propagate_version = true)]可以通过设置这个属性来将version选项传递给所有的子命令,当然子命令自身也可以指定自己的version。
选项设置默认值
我们知道可以使用Option来设置选项为可选,但是我们希望不希望选项值不存在,而是存在默认值就可以使用
#[arg(default_value_t = 2020)]
port: u16,选项的可选值为Enum
限制了选项的值只能为Enum中的一个值,使用value_enum属性,并在作为选项值的Enum上使用派生类ValueEnum
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// What mode to run the program in
#[arg(value_enum)]
mode: Mode,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
/// Run swiftly
Fast,
/// Crawl slowly but steadily
///
/// This paragraph is ignored because there is no long help text for possible values.
Slow,
}检验选项值
通过value_parse可以校验和解析选项的值
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// Network port to use
#[arg(value_parser = clap::value_parser!(u16).range(1..))]
port: u16,
}自定义选项值解析
还是通过value_parse可以将原始的选项字符串按照自己的要求解析为自己要求的类型
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// Network port to use
#[arg(value_parser = port_in_range)]
port: u16,
}
const PORT_RANGE: RangeInclusive<usize> = 1..=65535;
fn port_in_range(s: &str) -> Result<u16, String> {
let port: usize = s
.parse()
.map_err(|_| format!("`{s}` isn't a port number"))?;
if PORT_RANGE.contains(&port) {
Ok(port as u16)
} else {
Err(format!(
"port not in range {}-{}",
PORT_RANGE.start(),
PORT_RANGE.end()
))
}
}当然了,自定义的解析函数需要接受一个字符串切片作为参数,返回一个解析结果的Result类型。
选项的分组和关联
在一个分组中的选项同时只能传递一个,不能同时传递。关联则是制定了选项的依赖关系
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
/// some regular input
#[arg(group = "input")]
input_file: Option<String>,
/// some special input argument
#[arg(long, group = "input")]
spec_in: Option<String>,
#[arg(short, requires = "input")]
config: Option<String>,
}这里的input_file和spec_in是同一个分组,不能够同时传递。config依赖input这个分组,要求传递了config的话,那么input分组也必须传递了一个选项才行。
将一个分组的选项单独提取
可以将一个分组的选项单独提取,方便管理。最后再展平就可以
#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
#[command(flatten)]
vers: Vers,
...
}
#[derive(Args)]
#[group(required = true, multiple = false)]
struct Vers {
/// set version manually
#[arg(long, value_name = "VER")]
set_ver: Option<String>,
/// auto inc major
#[arg(long)]
major: bool,
/// auto inc minor
#[arg(long)]
minor: bool,
/// auto inc patch
#[arg(long)]
patch: bool,
}测试
用来报告开发错误信息,进行测试调试
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert();
}