Rust Clap库学习

5,711 阅读7分钟

Clap学习

本片内容主要参考clap的官方文档

在使用Rust的库之前, 首先需要添加clap库:

cargo add clap --features derive

其他派生类型clap::_features - Rust (docs.rs)

运行这个命令行会在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的使用方式是:

  1. 先创建一个struct, 其中的字段就是命令行的参数名称.
  2. struct添加Parser的派生.
  3. 添加command, 为了控制命令行展示的行为, 也可以不添加.
  4. 给参数添加arg, 为了控制单个参数的信息, 也可以不添加. 不添加每个参数都是必填的
  5. 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 attributesMagic attributes. 在Clap官方文档中有句话

Raw attributes are forwarded directly to the underlying clap builder. Any Command, Arg, or PossibleValue method 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_helpcommand实现的一个方法.

对于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, argClap实现的类属性宏. 他们的作用就是为影响命令行参数的行为和提示信息.

command用来改变应用级别的行为,用来定义一个命令行界面. 比如整个应用的版本和作者, 上述提到的arg_required_else_help(没有参数就显示提示信息)方法.

arg是所有参数的抽象表示, 用于为程序定义所有有效参数的选项和关系.

从这个定义看, Command是包含Arg的概念的. 先有一个命令行的界面内容, 里面才有Arg参数. 而magic/raw attributes则是控制每一个具体的功能项目.

三. Command

Command关联的raw attributesmagic attributes.

Raw Attributes

Magic Attributes

  • name = <expr> 未设置时,取crate name(Parser中), 变量名(Subcommand中)
  • version [=<expr>]启用但未设置值时, crate version. 未启用为空
  • author [=<expr>] 启用但未设置值时, crate authors. 未启用为空
  • about [=<expr>] 启用但未设置值时, crate description. 未启用时为Doc comment
  • long_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

Argmagic 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()
        ))
    }
}