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
和 Clap
自定义的Magic attributes
. 其中 derive
, command
, arg
就相当于Raw attributes
, command
下面的author
, version
就是Magic attributes
. 在Clap官方文档中有句话
Raw attributes are forwarded directly to the underlying clap builder. Any
Command
,Arg
, orPossibleValue
method can be used as an attribute.
说是raw attributes
会被转发给底层的clap builder
, 并且Command
方法等会被用作属性. 我理解的是: Command
, Arg
这些raw attributes
会作为 #[]
语法来使用, 这个转换是由clap builder
完成的. 官方给的例子是:
#[arg(
global = true, // name = arg form, neat for one-arg methods
required_if_eq("out", "file") // name(arg1, arg2, ...) form.
)]
当点开clap builder
的连接,在其中再点开Command 的连接, 再看官方给的例子:
let m = Command::new("My Program")
.author("Me, me@mail.com")
.version("1.0.2")
.about("Explains in brief what the program does")
.arg(
Arg::new("in_file")
)
.after_help("Longer explanation to appear after the options when \
displaying the help information from --help or -h")
.get_matches();
// Your program logic starts here...
我们可以理解成
#[command(author, version, about, long_about=None)]
被转化为
let m = Command::new("struct的名").author("").about("").long_about(None);
总结
command, arg, SubCommand 被称作Raw attributes
.
author, long, short, version等被称作Magic attributes
.
(如果可以这样理解的话)
这样我们就可以理解官方文档中的一些概念了.
三. Magic Attributes
首先要说一下Arg
和Command
的区别:
Arg
的定义是:命令行参数的抽象表示,用于设置定义程序有效参数的所有选项和关系.Command
是:建一个命令行界面.
从这个定义看, Command
是包含Arg
的概念的. 先有一个命令行的界面内容, 里面才有Arg
参数. 而magic attributes
则是控制每一个具体的功能项目.
接下来看一下clap提供的magic attributes
(只说一部分).
Command 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时最小化预处理
从Command
的定义出发, 这些magic attributes
涉及的是程序版本, 作者, 介绍等, 整体和宏观的控制(比如后面例子中的next_line_help
).
Arg Attributes
id = <expr>
未设置时, 取struct中的字段名字,指定了就用指定名字value_parser [=<expr>]
未设置时, 根据类型使用value_parser!
的行为action [=<expr>]
未设置时, 使用ArgAction
的默认行为.help=<expr>
未设置时,使用文档注释内容
Arg
的magic attributes
是设置每个参数的属性和功能.
四.derive使用例子
依赖官方文件的例子理解.
例子1 简单使用
use clap::Parser;
#[derive(Parser)]
#[command(name="MyApp", author="AName", version="1.0", about="Does awesome things", long_about=None)]
struct Cli {
#[arg(long)]
two: String,
#[arg(long)]
one: String,
}
fn main() {
let cli = Cli::parse();
println!("two: {:?}", cli.two);
println!("one: {:?}", cli.one);
}
官方的例子可以改成一行command
属性, 这和分开写是一样的. one
和two
是必传的参数.
把command
中的值取消掉就会使用Cargo.toml
中的值
#[command(name, author, version, about, long_about=None)]
例子2 加入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
例子3 位置参数
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
文档例子中#[arg(short, long)]
的作用是为name
参数设置单字母选项和长选项. 同时设置#[arg]
后会将name
放在Option
选项中. 否则是Arguments
中.
$ ./practice -h
>> Usage: practice [NAME]
Arguments:
[NAME]
Options:
-h, --help Print help
-V, --version Print version
$ ./practice -h
>> Usage: practice [OPTIONS]
Options:
-n, --name <NAME>
-h, --help Print help
-V, --version Print version
例子4 可选参数
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());
}
可选参数就是通过对变量类型的指定实现的, 如果是Option
的就是可选参数, 如果不赋值, 就使用空值None
.
例子5 标志位
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
例子6 子命令
子命令可以是另一套参数集合, 比如git
命令有自己的参数, git config
也有自己的参数, config
就是子命令.
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:?}")
}
}
}
设置子命令是可选的
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Option<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 {
Some(Commands::Add { name }) => {
println!("'myapp add' was used, name is: {name:?}")
},
None => {
println!("'myapp add' don't used")
}
}
}
例子7 设置默认值
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);
}
例子8 校验输入值, 使用枚举校验有限的值
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");
}
}
}
要列举出可以输入的值, 需要使用枚举类型, clap
提供的是Arg::value_enum
将参数值解析为ValueEnum
.
例子9 使用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);
}
例子10 自定义验证逻辑
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()
))
}
}
文档中给的解析器的类型是
value_parser!(T) for auto-selecting a value parser for a given type
Or range expressions like 0..=1 as a shorthand for RangedI64ValueParser
Fn(&str) -> Result<T, E>
[&str] and PossibleValuesParser for static enumerated values
BoolishValueParser, and FalseyValueParser for alternative bool implementations
NonEmptyStringValueParser for basic validation for strings
or any other TypedValueParser implementation
解析函数的签名是Fn(&str) -> Result<T, E>
自定义的解析函数需要符合这个函数签名.