Rust Clap库学习

发布时间 2023-10-11 17:34:12作者: 睡觉督导员

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 attributesClap自定义的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, or PossibleValue 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

首先要说一下ArgCommand的区别:

  • 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 comment
  • long_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> 未设置时,使用文档注释内容

Argmagic 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属性, 这和分开写是一样的. onetwo是必传的参数.

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> 自定义的解析函数需要符合这个函数签名.