Clap学习
本片内容主要参考clap的官方文档
在使用Rust的库之前, 首先需要添加clap库:
cargo add clap --features derive
运行这个命令行会在Cargo.toml中添加
clap = { version = "4.2.1", features = ["derive"] }
关于为什么要加features,可以阅读 Rust语言圣经中的内容.
或者可以直接手动在Cargo.toml中添加clap库的配置.
从我添加的这个配置可以看到, 这篇文章是根据clap==4.2.1版本写的. 之前的版本我也不了解, 先不写了.
一.基础用法例子
use clap::Parser;
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Name of the person to greet
#[arg(short, long)]
name: String,
/// Number of times to greet
#[arg(short, long, default_value_t = 1)]
count: u8,
}
fn main() {
let args = Args::parse();
for _ in 0..args.count {
println!("Hello {}!", args.name)
}
}
这个是官方文档中的例子, 这是使用derive派生的方式实现.
从这个例子中可以看到, clap的使用方式是:
- 先创建一个
struct, 其中的字段就是命令行的参数名称. - 给
struct添加Parser的派生. - 添加
command, 为了控制命令行展示的行为, 也可以不添加. - 给参数添加
arg, 为了控制单个参数的信息, 也可以不添加.不添加每个参数都是必填的 - 在
main函数中解析参数(Args::parse())
在这个例子中可以看到command, arg, short, long, default_value_t这些名字, 下来先了解一下这些名字的含义,和为什么要使用这些名词.
二. clap概念
Attributes
官方文档中所说的Attributes是指
- #[derive(Debug, Parser)]
- #[command(author, version, about, long_about = None)]
- #[arg(short, long, default_value_t = 1)]
中使用 #[]语法定义的类属性宏(目的是: 用于为目标添加自定义的属性).
这其中分为Raw attributes 和 Magic attributes. 在Clap官方文档中有句话
Raw attributes are forwarded directly to the underlying clap builder. Any
Command,Arg, orPossibleValuemethod can be used as an attribute.
说是raw attributes会被转发给底层的clap builder, 并且Command等的方法会被用作attribute. 从文档中给的例子是
#[command(arg_required_else_help(true))] would translate to cmd.arg_required_else_help(true)
这里的arg_required_else_help是command实现的一个方法.
对于Magic attributes文档中的描述是
Magic attributes have post-processing done to them, whether that is
- Providing of defaults
- Special behavior is triggered off of it
所以Magic attributes是会提供默认值或引发特殊行为的方法.
综上总结就是,raw attributes是普通的方法, 而Magic attributes是有特殊行为的方法.magic是raw的升级.
command和arg
command, arg是Clap实现的类属性宏. 他们的作用就是为影响命令行参数的行为和提示信息.
command用来改变应用级别的行为,用来定义一个命令行界面. 比如整个应用的版本和作者, 上述提到的arg_required_else_help(没有参数就显示提示信息)方法.
arg是所有参数的抽象表示, 用于为程序定义所有有效参数的选项和关系.
从这个定义看, Command是包含Arg的概念的. 先有一个命令行的界面内容, 里面才有Arg参数. 而magic/raw attributes则是控制每一个具体的功能项目.
三. Command
与Command关联的raw attributes和magic attributes.
Raw Attributes
Magic Attributes
name = <expr>未设置时,取crate name(Parser中), 变量名(Subcommand中)version [=<expr>]启用但未设置值时, crate version. 未启用为空author [=<expr>]启用但未设置值时, crate authors. 未启用为空about [=<expr>]启用但未设置值时, crate description. 未启用时为Doc commentlong_about [=<expr>]启用但未设置值时, 使用Doc comment. 未启用时没有值verbatim_doc_comment在将doc注释转换为about/long_about时最小化预处理
四. Arg
Magic Attributes
id = <expr>未设置时, 取struct中的字段名字,指定了就用指定名字value_parser [=<expr>]未设置时, 根据类型使用value_parser!的行为action [=<expr>]未设置时, 使用ArgAction的默认行为.help=<expr>未设置时,使用文档注释内容- long_help
- verbatim_doc_comment
- short
Arg的magic attributes是设置每个参数的属性和功能.
五 如何设置
位置参数
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
name: Option<String>,
}
fn main() {
let cli = Cli::parse();
println!("name: {:?}", cli.name.as_deref());
}
name 只能接收一个参数值, 输入多个就会报错, ArgAction的默认行为是Set只能设置一个值, 如果想要收集多个值要将name改为name: Vec<String>.
$ ./practice bob
>> name: Some("bob")
$ ./practice bob tom
>> error: unexpected argument 'tom' found
$ ./practice bob tom
>> name: ["bob", "tom"] <- 改成Vec<String>后
可选参数
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
name: Option<String>,
}
fn main() {
let cli = Cli::parse();
println!("name: {:?}", cli.name.as_deref());
}
文档例子中#[arg(short, long)]的作用是为name参数设置单字母选项和长选项. 设置#[arg]后会将name放在Option选项中(变成了相当于关键字参数). 否则是Arguments中.
$ ./practice -h <- 未加 #[arg(short, long)]
>> Usage: practice [NAME]
Arguments:
[NAME]
Options:
-h, --help Print help
-V, --version Print version
$ ./practice -h <- 加了 #[arg(short, long)]
>> Usage: practice [OPTIONS]
Options:
-n, --name <NAME>
-h, --help Print help
-V, --version Print version
结果是: 必须使用长/短选项才能设置值, 否则不能设置该参数值.
$ ./practice --name bob
>> name: Some("bob")
$ ./practice -n bob
>> name: Some("bob")
$ ./practice
>> name: None <- name的类型是Option<String>, 没有设置时就是None
$ ./practice bob
>> error: unexpected argument 'bob' found <- 意思没有bob这个参数
同样这里设置值的默认行为是ArgAction::Set只能设置一个值, 如果想要收集多个值要将name改为name: Vec<String>.
标志
标志是设置有和无(True/False). 这里也可以设置为计数的模式, 看设置了多少次True.
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
println!("verbose: {:?}", cli.verbose);
}
使用标志位的方式就是, 将元素的类型设置成布尔值, 但是布尔值的特性是, 只能被设置一次, 第二次设置时会报错.
$ ./practice --verbose
>> verbose: true
$ ./practice --verbose --verbose
>> error: the argument '--verbose' cannot be used multiple times
使用action = clap::ArgAction::Count, 同时将元素类型设置为int可以对参数的数量进行计数.
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
fn main() {
let cli = Cli::parse();
println!("verbose: {:?}", cli.verbose);
}
$ ./practice --verbose -v -v
>> name: 3
子命令
子命令可以是另一套参数集合, 比如git命令有自己的参数, git config也有自己的参数, config就是子命令.
子命令需要使用派生#[derive(Subcommand)], 并且要设置#[command(subcommand)]
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Adds files to myapp
Add { name: Option<String> },
}
fn main() {
let cli = Cli::parse();
// You can check for the existence of subcommands, and if found use their
// matches just as you would the top level cmd
match &cli.command {
Commands::Add { name } => {
println!("'myapp add' was used, name is: {name:?}")
}
}
}
$ ./practice help
>> Usage: practice <COMMAND> <- 此时子命令是必填的,否则程序不运行.
>> Commands:
>> add Adds files to myapp
>> help Print this message or the help of the given subcommand(s)
>>
>> Options:
>> -h, --help Print help
>> -V, --version Print version
$ ./practice add -h
>> Adds files to myapp
>>
>> Usage: practice add [NAME] <- add中name的参数是可以没有值的, 没有值就是None
>>
>> Arguments:
>> [NAME]
>>
>> Options:
>> -h, --help Print help
>> -V, --version Print version
如果在Cli中再加一个参数name, 并设置为必填的参数, 那么如果不设置这个值, 就无法正常运行
struct Cli {
#[command(subcommand)]
command: Commands,
name: String,
}
$ ./practice
>> Usage: practice <NAME> <COMMAND>
>>
>> Commands:
>> add Adds files to myapp
>> help Print this message or the help of the given subcommand(s)
>>
>> Arguments:
>> <NAME>
>>
>> Options:
>> -h, --help Print help
>> -V, --version Print version
$ ./practice add bob
>> error: the following required arguments were not provided:
>> <NAME>
如果将name设置成Option就可以
struct Cli {
#[command(subcommand)]
command: Commands,
name: Option<String>,
}
设置子命令是可选的
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
name: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
/// Adds files to myapp
Add { name: Option<String> },
}
fn main() {
let cli = Cli::parse();
// You can check for the existence of subcommands, and if found use their
// matches just as you would the top level cmd
match &cli.command {
Some(Commands::Add { name }) => {
println!("'myapp add' was used, name is: {name:?}")
},
None => {
println!("'myapp add' don't used")
}
}
}
$ ./practice
>> Usage: practice <NAME> [COMMAND]
>>
>> Commands:
>> add Adds files to myapp
>> help Print this message or the help of the given subcommand(s)
>>
>> Arguments:
>> <NAME>
>>
>> Options:
>> -h, --help Print help
>> -V, --version Print version
枚举值
提供可选择的值给用户使用, 如果用户没有使用给定的值, 就会给出提示信息. 这里要使用ValueEnum
use clap::{Parser, ValueEnum};
#[derive(Parser)]
#[command(author, 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, | | |
} | | |
| | |
fn main() { | | |
let cli = Cli::parse(); | | |
| | |
match cli.mode { | | |
Mode::Fast => { | | |
println!("Hare"); | | |
} | | |
Mode::Slow => { | | |
println!("Tortoise"); | | |
} | | |
} | | |
} | | |
==================================================|==|====|=======
| | |
$ ./practice --help | | |
>> Usage: practice <MODE> | | |
>> | | |
>> Arguments: | | |
>> <MODE> | | |
>> What mode to run the program in ------------>|
>> | |
>> Possible values: | |
>> - fast: Run swiftly ------>| |
>> - slow: Crawl slowly but steadily------>|
>>
>> Options:
>> -h, --help
>> Print help (see a summary with '-h')
>>
>> -V, --version
>> Print version
mode参数必填, 并且只能选择给定的值.
经过验证的值
通常, 可以使用Arg::value_parser验证和解析任何数据类型.value_parser!不是支持所有的类型, 只是支持下列的类型:
- bool, String, OsString, PathBuf
- u8, i8, u16, i16, u32, i32, u64, i64
- ValueEnum
- From From<&OsStr>
- From From<&str>
- FromStr
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Network port to use
#[arg(value_parser = clap::value_parser!(u16).range(1..))] <---这里添加
port: u16,
}
fn main() {
let cli = Cli::parse();
println!("PORT = {}", cli.port);
}
从代码中可以看出, 要求数据从1开始, 如果输入0 则会报错
$ ./practice --help
>> A simple to use, efficient, and full-featured Command Line Argument Parser
>>
>> Usage: 04_02_parse_derive[EXE] <PORT>
>>
>> Arguments:
>> <PORT> Network port to use
>>
>> Options:
>> -h, --help Print help
>> -V, --version Print version
自定义解析器可用于改进错误消息或提供附加验证
use std::ops::RangeInclusive;
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Network port to use
#[arg(value_parser = port_in_range)]
port: u16,
}
fn main() {
let cli = Cli::parse();
println!("PORT = {}", cli.port);
}
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()
))
}
}
参数关系
可以在 Args 甚至 ArgGroup 之间声明依赖项或冲突。ArgGroup 使声明关系变得更加容易, 而不必单独列出每个关系, 或者当您希望规则应用于“任何但不是全部”参数时.也许 ArgGroup 最常见的用法是要求在给定的集合中只存在一个参数(同时只能使用其中一个参数的情况). 假设您有多个参数, 您希望其中一个参数是必需的, 但是将所有参数都设置为必需是不可行的, 因为它们可能相互冲突.
use clap::{Args, Parser};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(flatten)] <--flatten相当于将vers中的参数,提升到当前和input_file等到一个级别
vers: Vers,
/// some regular input
#[arg(group = "input")]
input_file: Option<String>,
/// some special input argument
#[arg(long, group = "input")] <-- group=input将参数划归input分组
spec_in: Option<String>,
#[arg(short, requires = "input")] <-- requires=input,如果使用config,则input_file或者spec_in必须有一个
config: Option<String>,
}
#[derive(Args)]
#[group(required = true, multiple = false)]
<-- required 要求在解析时显示组中的参数, 组将以<arg|arg2|arg3>格式显示在应用程序的使用字符串中
<-- multiple=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,
}
fn main() {
let cli = Cli::parse();
// Let's assume the old version 1.2.3
let mut major = 1;
let mut minor = 2;
let mut patch = 3;
// See if --set_ver was used to set the version manually
let vers = &cli.vers;
let version = if let Some(ver) = vers.set_ver.as_deref() {
ver.to_string() <-- 当存在set_ver的时候, 其他三个选项就不能用
} else {
// Increment the one requested (in a real program, we'd reset the lower numbers)
let (maj, min, pat) = (vers.major, vers.minor, vers.patch);
match (maj, min, pat) { <-- 三个值里也只有一个能用
(true, _, _) => major += 1,
(_, true, _) => minor += 1,
(_, _, true) => patch += 1,
_ => unreachable!(),
};
format!("{major}.{minor}.{patch}")
};
println!("Version: {version}");
// Check for usage of -c
if let Some(config) = cli.config.as_deref() { <--如果设置了config, input_file或spec_in就必须有一个
let input = cli
.input_file
.as_deref()
.unwrap_or_else(|| cli.spec_in.as_deref().unwrap());
println!("Doing work using input {input} and config {config}");
}
}
四.derive使用例子
例子1 加入next_line_help
添加#[command(next_line_help = true)]
use clap::Parser;
#[derive(Parser)]
#[command(name="MyApp", author="AName", version="1.0", about="Does awesome things", long_about=None)]
#[command(next_line_help = true)]
struct Cli {
/// 123456
#[arg(long)]
two: String,
/// 123456
#[arg(long)]
one: String,
}
fn main() {
let cli = Cli::parse();
println!("two: {:?}", cli.two);
println!("one: {:?}", cli.one);
}
效果是使用-h/-help. 添加next_line_help和不添加,输出效果不同.
$ ./practice -h
>> Does awesome things
Usage: practice --two <TWO> --one <ONE>
Options:
--two <TWO>
123456 <-这里, 加入next_line_help
--one <ONE>
123456 <-这里, 加入next_line_help
-h, --help
Print help <-这里, 加入next_line_help
-V, --version
Print version <-这里, 加入next_line_help
$ ./practice -h
>> Does awesome things
Usage: practice --two <TWO> --one <ONE>
Options:
--two <TWO> 123456 <- 没加next_line_help
--one <ONE> 123456 <- 没加next_line_help
-h, --help Print help <- 没加next_line_help
-V, --version Print version <- 没加next_line_help
例子2 设置默认值
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(default_value_t = 2020)]
port: u16,
}
fn main() {
let cli = Cli::parse();
println!("port: {:?}", cli.port);
}
例子3 自定义验证逻辑
use std::ops::RangeInclusive;
use clap::Parser;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// Network port to use
#[arg(value_parser = port_in_range)] -> 这里添加自定义的函数入口
port: u16,
}
fn main() {
let cli = Cli::parse();
println!("PORT = {}", cli.port);
}
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()
))
}
}