跳过正文

clap

·
目录
rust crate - 这篇文章属于一个选集。
§ 12: 本文

两种使用方式:

  1. derive macro
  2. builder

builder 模式:

fn main() {
    let cmd = clap::Command::new("cargo")
        .bin_name("cargo")
        .subcommand_required(true)
        .subcommand(
            clap::command!("example").arg(
                clap::arg!(--"manifest-path" <PATH>)
                    .value_parser(clap::value_parser!(std::path::PathBuf)),),
        );

    // 从 os:env::args() 获得输入
    let matches = cmd.get_matches();

    // 匹配子命令
    let matches = match matches.subcommand() {
        Some(("example", matches)) => matches,
        _ => unreachable!("clap should ensure we don't get here"),
    };

    // 获得子命令的选项参数
    let manifest_path = matches.get_one::<std::path::PathBuf>("manifest-path");
    println!("{manifest_path:?}");
}

derive 宏模式:

use clap::Parser;

#[derive(Parser)]
#[command(name = "cargo", bin_name = "cargo", subcommand_required = true)]
enum CargoCli {
    ExampleDerive(ExampleDeriveArgs),
}

#[derive(clap::Args)]
#[command(version, about, long_about = None)]
struct ExampleDeriveArgs {
    #[arg(long)]
    manifest_path: Option<std::path::PathBuf>,
}

fn main() {
    let CargoCli::ExampleDerive(args) = CargoCli::parse();
    println!("{:?}", args.manifest_path);
}

ValueParser
#

ValueParserArg::value_parser() 的参数类型,用于解析和校验参数:

impl Arg
    pub fn value_parser(self, parser: impl IntoResettable<ValueParser>) -> Arg

两种创建方式:

  1. value_parser!() :参数是类型名称,该类型需要实现 ValueParserFactory trait
  2. ValueParser::new() : 参数是实现 TypedValueParser trait 的类型值;

value_parser!()
#

value_parser!(T) 的参数 T 是如下类型:

  • 实现 ValueParserFactory 的类型,包括: <— 为自定义类型 T 支持 value_parser!(T) 的方式。
    • 原生类型:bool、String、OsString、PathBuf
    • 有范围限制的数值类型:u8、i8、u16、i16、u32、i32、u64、i64
  • ValueEnum 类型
  • 实现 From 和 From<&OsStr> 的类型
  • 实现 From 和 From<&str> 的类型
  • 实现 FromStr 的类型,例如 usize、isize
// Register a type with value_parser!
pub trait ValueParserFactory {
    // 关联类型, 一般是实现 TypedValueParser 的类型。
    type Parser;

    // Required method
    fn value_parser() -> Self::Parser;
}

// clap 注册的 Rust 类型, 这些类型可以直接在 derive 宏修饰的 struct/enum 中使用.
impl ValueParserFactory for bool
impl ValueParserFactory for i8
impl ValueParserFactory for i16
impl ValueParserFactory for i32
impl ValueParserFactory for i64
impl ValueParserFactory for u8
impl ValueParserFactory for u16
impl ValueParserFactory for u32
impl ValueParserFactory for u64
impl ValueParserFactory for Box<str>
impl ValueParserFactory for Box<OsStr>
impl ValueParserFactory for Box<Path>
impl ValueParserFactory for String
impl ValueParserFactory for OsString
impl ValueParserFactory for PathBuf

// Parser 需要实现 TypedValueParser trait
impl<T> ValueParserFactory for Box<T>
    where T: ValueParserFactory + Send + Sync + Clone, <T as ValueParserFactory>::Parser: TypedValueParser<Value = T>
impl<T> ValueParserFactory for Arc<T>
    where T: ValueParserFactory + Send + Sync + Clone, <T as ValueParserFactory>::Parser: TypedValueParser<Value = T>
impl<T> ValueParserFactory for Wrapping<T>
    where T: ValueParserFactory + Send + Sync + Clone, <T as ValueParserFactory>::Parser: TypedValueParser<Value = T>,

使用 derive 宏来创建 Arg 时, 如果没有为 field 或 variant 类型 T 指定 #[arg(value_parser)] 属性, 则默认使用 value_parser!(T) 来解析该字段。

  • value_parser!(T) 的 T 类型需要实现 ValueParserFactory trait;
  • clap 为 Rust 内置类型,如 i8、u8、bool、String、PathBuf 等,以及它们的 Option<T>/Vec<T> 实现了 ValueParserFactory trait

示例:

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
    /// Name of the person to greet
    #[arg(short, long)]
    name: String, // String 和 u8 实现了 ValueParserFactory trait,故可以直接使用。

    /// Number of times to greet
    #[arg(short, long, default_value_t = 1)]
    count: u8,
    
    #[arg(long)]
    manifest_path: Option<std::path::PathBuf>, // Option<T>/Vec<T> 也 OK
    
    // 调用 value_parser() 方法,传入实现 TypedValuePrarser trait 的类型,
    // 这里使用闭包实现 TypedValuePrarser triat。
    #[arg(value_parser = validate_package_name)] 
    package_name: String,
    
    /// Network port to use
    // clap::value_parser!(u16) 返回 clap::builder::RangedI64ValueParser 类型,它有 range 方法
    #[arg(value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
}

// Built-in types
let parser = clap::value_parser!(String);
assert_eq!(format!("{parser:?}"), "ValueParser::string");

let parser = clap::value_parser!(std::ffi::OsString);
assert_eq!(format!("{parser:?}"), "ValueParser::os_string");

let parser = clap::value_parser!(std::path::PathBuf);
assert_eq!(format!("{parser:?}"), "ValueParser::path_buf");
        
// clap::value_parser!(u16) 返回 clap::builder::RangedI64ValueParser 类型,它有 range 方法        
clap::value_parser!(u16).range(3000..);
clap::value_parser!(u64).range(3000..);

let parser = clap::value_parser!(usize);
assert_eq!(format!("{parser:?}"), "_AnonymousValueParser(ValueParser::other(usize))");

// ValueEnum 类型
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum ColorChoice {
    Auto,
    Always,
    Never,
}
clap::value_parser!(ColorChoice);

TypedValuePrarser
#

TypedValueParser trait 用于为类型实现自定义解析,它实现了 Into<ValueParser> ,故可以作为 Arg::value_parser() 的参数:

pub trait TypedValueParser: Clone + Send + Sync + 'static {
    type Value: Send + Sync + Clone;

    // 从传入的 arg/value 来解析出 self::Value 类型值;
    fn parse_ref( &self, cmd: &Command, arg: Option<&Arg>, value: &OsStr ) -> Result<Self::Value, Error>;
    //...
}

// clap::builder module 提供了如下实现 TypedValueParser 的类型:
impl TypedValueParser for BoolValueParser // 返回 bool
impl TypedValueParser for BoolishValueParser // 将类似于 bool 值解析为 bool
impl TypedValueParser for FalseyValueParser
impl TypedValueParser for NonEmptyStringValueParser // 返回非空 String
impl TypedValueParser for OsStringValueParser // 返回 OsString
impl TypedValueParser for PathBufValueParser // 返回 PathBuf
impl TypedValueParser for PossibleValuesParser // 可选值列表
impl TypedValueParser for StringValueParser // 返回 String
impl TypedValueParser for UnknownArgumentValueParser
impl<E> TypedValueParser for EnumValueParser<E> where E: ValueEnum + Clone + Send + Sync + 'static // 枚举值

// 可以通过闭包来实现 TypedValueParser, 闭包的参数是 &str: Fn(&str) -> Result<T, E>, T 为解析后的值类型;
impl<F, T, E> TypedValueParser for F
where
    F: Fn(&str) -> Result<T, E> + Clone + Send + Sync + 'static,
    E: Into<Box<dyn Error + Sync + Send>>,
    T: Send + Sync + Clone

impl<P, F, T> TypedValueParser for MapValueParser<P, F>
where
    P: TypedValueParser,
    <P as TypedValueParser>::Value: Send + Sync + Clone,
    F: Fn(<P as TypedValueParser>::Value) -> T + Clone + Send + Sync + 'static,
    T: Send + Sync + Clone

impl<P, F, T, E> TypedValueParser for TryMapValueParser<P, F>
where
    P: TypedValueParser,
    <P as TypedValueParser>::Value: Send + Sync + Clone,
    F: Fn(<P as TypedValueParser>::Value) -> Result<T, E> + Clone + Send + Sync + 'static,
    T: Send + Sync + Clone,
    E: Into<Box<dyn Error + Sync + Send>>

// RangedI64ValueParser 和 RangedU64ValueParser 用于定义一个 range 范围。
impl<T> TypedValueParser for RangedI64ValueParser<T>
where
    T: TryFrom<i64> + Clone + Send + Sync + 'static,
    <T as TryFrom<i64>>::Error: Send + Sync + 'static + Error + ToString

impl<T> TypedValueParser for RangedU64ValueParser<T>
where
    T: TryFrom<u64> + Clone + Send + Sync + 'static,
    <T as TryFrom<u64>>::Error: Send + Sync + 'static + Error + ToString,
  • BoolishValueParser : 可以将任意值解析为 bool 值 true、false:
  • FalseyValueParser: False-like 值为 false,其他值为 true;
let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("append")
            .value_parser(clap::builder::BoolishValueParser::new())
            .required(true)
    );

let m = cmd.try_get_matches_from_mut(["cmd", "true"]).unwrap();
let port: bool = *m.get_one("append")
    .expect("required");
assert_eq!(port, true);

let value_parser = clap::builder::BoolishValueParser::new();
assert!(value_parser.parse_ref(&cmd, arg, OsStr::new("random")).is_err());
assert!(value_parser.parse_ref(&cmd, arg, OsStr::new("")).is_err());
assert!(value_parser.parse_ref(&cmd, arg, OsStr::new("100")).is_err());
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("true")).unwrap(), true);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("Yes")).unwrap(), true);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("oN")).unwrap(), true);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("1")).unwrap(), true);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("false")).unwrap(), false);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("No")).unwrap(), false);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("oFF")).unwrap(), false);
assert_eq!(value_parser.parse_ref(&cmd, arg, OsStr::new("0")).unwrap(), false);

例子:

// 使用宏来创建 TypedValueParser
let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("port")
            .long("port")            
            .value_parser(clap::value_parser!(u16).range(3000..))
            .action(clap::ArgAction::Set)
            .required(true)
    );
let m = cmd.try_get_matches_from_mut(["cmd", "--port", "3001"]).unwrap();
let port: u16 = *m.get_one("port").expect("required");
assert_eq!(port, 3001);

// 使用 clap::builder module 提供的实现 TypedValueParser trait 的类型值
let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("append")
            // 使用实现 TypedValueParser trait 的类型值作为参数
            .value_parser(clap::builder::NonEmptyStringValueParser::new())
            .required(true)
    );
let m = cmd.try_get_matches_from_mut(["cmd", "true"]).unwrap();
let port: &String = m.get_one("append").expect("required");
assert_eq!(port, "true");


// 使用 Fn 闭包实现 TypedValueParser,闭包的参数是 &str,
// 返回的 Ok 值类型需要和 arg 字段类型值 T/Option<T>/Vec<T>/Option<Vec<T>> 中的 T 一致。
fn bytes_from_str(src: &str) -> Result<Bytes, Infallible> {
    Ok(Bytes::from(src.to_string()))
}

#[derive(Subcommand, Debug)]
enum Command {
    Ping {
        // value_parser 的值为实现 TypedValueParser 的类型。
        #[arg(value_parser = bytes_from_str)]
        msg: Option<Bytes>,
    }
}

可以为自定义类型实现 clap::builder::TypedValueParserclap::builder::ValueParserFactory ,然后该类型就可以被 clap::value_parser!() 使用:

#[derive(Copy, Clone, Debug)]
pub struct Custom(u32);

// Custom 实现 ValueParserFactory 后才能作为 value_parser!() 宏的参数
impl clap::builder::ValueParserFactory for Custom {
    type Parser = CustomValueParser; // Parser 需要实现 TypedValueParser

    fn value_parser() -> Self::Parser {
        CustomValueParser
    }
}

#[derive(Clone, Debug)]
pub struct CustomValueParser;

impl clap::builder::TypedValueParser for CustomValueParser {
    type Value = Custom;

    fn parse_ref(&self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result<Self::Value, clap::Error> {
        // 使用 clap::value_parser!() 宏创建的 CustomValueParser 来解析参数。
        let inner = clap::value_parser!(u32);
        let val = inner.parse_ref(cmd, arg, value)?;
        Ok(Custom(val))
    }
}

// 使用自定义 Custom 类型
let parser: CustomValueParser = clap::value_parser!(Custom);

Arg::value_parser()
#

Arg::value_parser() 用于为 Arg 指定解析参数值的方式,如果未指定,默认解析后的类型是 String

该方法的参数类型为 impl IntoResettable<ValueParser>,如 Into<ValueParser>、Option<ValueParser>

pub fn value_parser(self, parser: impl IntoResettable<ValueParser>) -> Arg

// 任何能 `Into<ValueParser>` 的类型值都可以作为 `Arg::value_parser()` 的参数:
impl<I> IntoResettable<ValueParser> for I where I: Into<ValueParser>

impl IntoResettable<ValueParser> for Option<ValueParser>

impl<I> IntoResettable<ValueRange> for I where I: Into<ValueRange>

其中 ValueParser 是 clap 自定义的类型:

pub struct ValueParser(/* private fields */);

// 从 TypedValueParser 创建 ValueParser
pub fn new<P>(other: P) -> ValueParser where  P: TypedValueParser,

clap 为如下类型实现了到 ValueParser 的转换,更复杂的类型需要使用 value_parser!() 宏来指定:

// 从 [P; C] 和 Vec<P> 转换为 ValueParser, 其中 P 需要实现 Into<PossibleValue>,一般为 String/&str 等;
impl<P, const C: usize> From<[P; C]> for ValueParser where P: Into<PossibleValue>
impl<P> From<Vec<P>> for ValueParser where P: Into<PossibleValue>

// 从 TypedValueParser 转换为 ValueParser, 如预定义的 `BoolValueParser、BoolishValueParser、FalseyValueParser`` 等,
// 以及闭包 `Fn(&str) -> Result<T, E>`
impl<P> From<P> for ValueParser where P: TypedValueParser + Send + Sync + 'static

// 从 RangeXX 转换为 ValueParser
impl From<Range<i64>> for ValueParser
impl From<RangeFrom<i64>> for ValueParser
impl From<RangeFull> for ValueParser
impl From<RangeInclusive<i64>> for ValueParser
impl From<RangeTo<i64>> for ValueParser
impl From<RangeToInclusive<i64>> for ValueParser

// 从预定义的关联函数创建 ValueParser
pub const fn bool() -> ValueParser
pub const fn string() -> ValueParser
pub const fn os_string() -> ValueParser
pub const fn path_buf() -> ValueParser

上面的 PossibleValue/PossibleValuesParser 类型如下:

  1. 任何可以 Into<Str> 的类型都可以转成 PossibleValue
  2. 任何可以迭代生成 PossibleValue 的类型都可以转成 PossibleValuesParser, 而它实现了 TypedValueParser trait
pub struct PossibleValue { /* private fields */ }

impl PossibleValue
    pub fn new(name: impl Into<Str>) -> PossibleValue
    
// 为可生成 str 的任意对象转换为 PossibleValue
impl<S> From<S> for PossibleValue where S: Into<Str>

impl PossibleValuesParser
    pub fn new(values: impl Into<PossibleValuesParser>) -> PossibleValuesParser

// 为可迭代对象,如 &[&str], Vec<&str>, [&str; N] 等转换为 PossibleValuesParser
impl<I, T> From<I> for PossibleValuesParser where I: IntoIterator<Item = T>, T: Into<PossibleValue>

impl TypedValueParser for PossibleValuesParser

// 示例
let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("color")
            // 必须都是字符串列表
            // 1. Vec<&str>
            .value_parser(vec!["always", "auto", "never"]))
            // 2. [&str; 3]
            .value_parser(["always", "auto", "never"])
            .value_parser(clap::builder::PossibleValuesParser::new(["always", "auto", "never"]))
            .required(true)
        );

let m = cmd.try_get_matches_from_mut(["cmd", "always"]).unwrap();
let port: &String = m.get_one("color").expect("required");
assert_eq!(port, "always");

builder
#

builder 模式使用如下 struct 类型:

  1. Command
  2. Arg
  3. ArgGroup
  4. ValueParser

可以使用 command!(), arg!(), value_parser!() 来快捷创建这些类型对象:

  • command!() : 自动从 Cargo.toml 中提取和设置 Command 的 name、about、author、version 等信息(编译时);
  • arg!() : 从字符串快捷创建 Arg 对象;
  • value_parser!(typeName): 根据指定的类型名(实现了 TypeValueParserFactory)设置 Arg 的 ValueParser 的实现方式;

以下 carte_XX!() 宏也是从 Cargo.toml 中提取相关信息:

  • crate_authors!(): 编译时从 Cargo.toml 中提取作者信息,格式为:author1 lastname <[email protected]>:author2 lastname <[email protected]>
  • crate_description!(): 编译时从 Cargo.toml 中提取描述信息。
  • crate_name!(): 编译时从 Cargo.toml 中提取名称。
  • crate_version!() : 编译时从 Cargo.toml 中提取版本,形式为 MAJOR.MINOR.PATCH_PKGVERSION_PRE。
use std::path::PathBuf;
use clap::{arg, command, value_parser, ArgAction, Command};

// 设置 Command 参数
let m = Command::new("My Program")
    .author("Me, [email protected]")
    .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();

// 使用 crate_xx!() 宏来设置 Command 信息
let m = Command::new(crate_name!())
    .author(crate_authors!("\n"))
    .version(crate_version!())
    .about(crate_description!())
    .get_matches();

// command!() 从 Cargo.toml 中读取和设置 Command 的 name/version/authors/description 信息。
let matches = command!()
    // 使用 arg!() 宏来快捷创建 Arg,指定的 name 和 config 为 arg id,后续用来匹配查找参数值。
    .arg(arg!([name] "Optional name to operate on"))
    .arg(
        arg!(-c --config <FILE> "Sets a custom config file" )
            .required(false) // 可选参数(默认是必选的)
            .value_parser(value_parser!(PathBuf)) // 设置参数解析类型(默认为 String)
    ) 
    .arg(arg!( -d --debug ... "Turn debugging information on" ))
    .subcommand(
        // new 的参数为 command id,后续用来匹配命令
        Command::new("test")
            .about("does testing things")
            .arg(arg!(-l --list "lists test values").action(ArgAction::SetTrue))
    )
    .get_matches();

// name 为前面通过 Arg::new("name") 或 arg!([name]) 设置的 arg id
if let Some(name) = matches.get_one::<String>("name") {
    println!("Value for name: {name}");
}

if let Some(config_path) = matches.get_one::<PathBuf>("config") {
    println!("Value for config: {}", config_path.display());
}

if let Some(matches) = matches.subcommand_matches("test") {
    // "$ myapp test" was run
    if matches.get_flag("list") {
        // "$ myapp test -l" was run
        println!("Printing testing lists...");
    } else {
        println!("Not printing testing lists...");
    }
}

Command
#

  • get_matches(self) -> ArgMatches: 从 env::args_os 解析 flags,解析出错时退出;

  • ignore_errors(true): 当解析 flags 出错时不退出;

  • get_matches_from() 从指定的字符串列表解析命令行参数,列表中第一个元素为命令名称。

  • .propagate_version(true): 将当前命令的 verison 传递到所有子命令中;

  • .next_line_help(true) : 在新的一行显示 flag 的 help 信息(默认在 flag 的当前行)

  • .bin_name("my_binary"): 重新设置 binary 名称;

  • .help_template("{name} ({version}) - {usage}"): 设置 help template;

fn main() {
    let cli = Command::new("Built CLI");
    // Augment with derived subcommands
    let cli = Subcommands::augment_subcommands(cli);

    let matches = cli.get_matches();
    let derived_subcommands = Subcommands::from_arg_matches(&matches)
        .map_err(|err| err.exit())
        .unwrap();
    println!("Derived subcommands: {derived_subcommands:#?}");
}

let m = cmd.clone().get_matches_from(vec!["prog", "-F", "in-file", "out-file"]);

ArgMatches
#

Argmatches 是 Command::get_matches() 方法返回的解析后的参数和子命令:

  • get_one(&self, id: &str) -> Option<&T>: 获得 flag id 对应的参数值;
  • get_count(“flag”): 获得 flag id 对应的数量;
  • get_flag(“flag”):获得 flag id 对应的 bool 值;
  • get_many(&self, id: &str) -> Option<ValuesRef<’_, T»: 获得 flag id 对应的多个参数值,如 Vec ;
  • subcommand(&self) -> Option<(&str, &ArgMatches)>:获得当前用户指定的子命令(&str)和参数集合(&ArgMatches)
  • subcommand_matches(&self, name: &str) -> Option<&ArgMatches>:获得指定子命令的参数集合;
let matches = Command::new("MyApp")
    .arg(Arg::new("out")
        .long("output")
        .required(true)
        .action(ArgAction::Set)
        .default_value("-"))
    .arg(Arg::new("cfg")
        .short('c')
        .action(ArgAction::Set))
    .get_matches(); // builds the instance of ArgMatches

if let Some(c) = matches.get_one::<String>("cfg") {
    println!("Value for -c: {c}");
}
println!("Value for --output: {}", matches.get_one::<String>("out").unwrap());

// You can check the presence of an argument's values
if matches.contains_id("out") {
    // However, if you want to know where the value came from
    if matches.value_source("out").expect("checked contains_id") == ValueSource::CommandLine {
        println!("`out` set by user");
    } else {
        println!("`out` is defaulted");
    }
}


 let app_m = Command::new("git")
     .subcommand(Command::new("clone"))
     .subcommand(Command::new("push"))
     .subcommand(Command::new("commit"))
     .get_matches();
 
match app_m.subcommand() {
    Some(("clone",  sub_m)) => {}, // clone was used
    Some(("push",   sub_m)) => {}, // push was used
    Some(("commit", sub_m)) => {}, // commit was used
    _                       => {}, // Either no subcommand or one not tested for...
}

Arg
#

Arg 默认特性:

  • 默认为位置参数,调用 short/long() 方法后成为 flag 类型参数;
  • 未调用 value_parser() 指定 ValueParser 时,默认为 StringValueParser,所以默认解析值为 String 类型;
  • .value_parser([“fast”, “slow”]): 设置可选值列表;
  • 未调用 action() 指定值设置方式时,默认为 ArgAction::Set,对于 Vec 需要指定为 action(ArgAction::Append));
  • 参数默认是必须的,调用 .required(false) 方法来设置为可选;
    • Vec/Option/bool 值类型是可选的,具体参考:https://docs.rs/clap/latest/clap/_derive/index.html#arg-types
  • .alias(“alias”)、 short_alias(’t’) 为 Arg 设置别名;
  • .global(true)) 设置为各 sub command 继承的全局参数;
  • .num_args(2)、 .num_args(2..) 设置 Arg 的参数值个数;
  • .value_name(“FILE”) 、.value_names([“value1”, “value2”]) 设置 help 时显示的 Arg Value 名称;
  • value_delimiter(’,’) 设置将 Arg Value 解析为 Vec 的分隔符;
  • .default_value(“myval”)) 设置 Arg 的缺省值字符串;
  • default_value_if() 满足特定条件(ArgPredicate 支持的两种类型:IsPresent, Equals(OsStr),)的缺省值
  • required_unless_present()/ required_if() 设置条件要求;
  • .env(“MY_FLAG”): 当未指定 flag 时从指定的环境变量获取;
  • .help(“Some help text”))、.next_line_help(true): 默认在 Arg flag 当前行显示 help,当调用 next_line_help(true) 后,在新的一行显示 help,非常适合显示较长的 help 信息。
  • .hide(true) 隐藏 Arg;
  • .group(“mode”) 设置 Arg 归属的 ArgGroup;
  • .conflicts_with(“debug”) 与其他 Arg 冲突;
// 使用 arg!() 来创建 Arg:

// [name]: 位置参数 name,name 作为 arg id,外围的 [] 表示是可选的。
let input = arg!([name] "Optional name to operate on").required(false)
    
// -i:short flag
// --input: long flag,同时 input 作为 arg id
// <FILE>: value_name,在显示参数的 help 信息时使用
// "Provides an input file to the program": 帮助信息
let input = arg!(-i --input <FILE> "Provides an input file to the program");

let matches = command!()
    // 位置参数,id 为 name
    .arg(arg!([name] "Optional name to operate on").required(false))
    // flag 参数,id 为 list
    .arg(arg!(-l --list "lists test values").action(ArgAction::SetTrue))

// 对 args!() 创建的 Arg 对象,进一步调用 value_parser!() 等方法
let matches = command!()
    .arg(
        arg!([PORT])
            .value_parser(value_parser!(u16)) // Rust 内置类型
            .default_value("2020"),
    )
    .get_matches();


// 使用 Arg::new("arg_id") 函数来创建 Arg
let cmd = Command::new().arg(
    Arg::new("in_file") // 未调用 short 或 long 方法,所以为位置参数
)

let cfg = Arg::new("config") // 未调用 short 或 long 方法,所以为位置参数
    .action(ArgAction::Set)
    .value_name("FILE")
    .value_parser(2..5); // RangeFull 实现了 ValueParser

let cfg = Arg::new("config")
    .short('c') // flag 类型参数
    .long("config")
    .action(ArgAction::Set) // 设置参数值的动作, 默认为 ArgAction::Set
    .value_name("FILE") // 在显示参数的帮助信息时使用
    .help("Provides a config file to myprog");

// 更复杂的例子
use clap::{arg, command, ArgAction};

fn main() {
    let matches = command!().next_line_help(true)
        // 未指定 ValueParser 时默认为 StringValueParser
        .arg(arg!(--two <VALUE>).required(true).action(ArgAction::Set))
        .arg(arg!(--one <VALUE>).required(true).action(ArgAction::Set))
        .get_matches();

    println!( "two: {:?}", matches.get_one::<String>("two").expect("required") );
    println!( "one: {:?}", matches.get_one::<String>("one").expect("required") );
}

let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("output")
            .value_parser(clap::value_parser!(PathBuf))
            .required(true)
    );

可选值(枚举值)类型 Arg
#

let cfg = Arg::new("config") 
        .action(ArgAction::Set)
        .value_name("FILE")
        // RangeFull 实现了 ValueParser
        .value_parser(2..5); 

let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("color")
            .long("color")
            // [&str; 3] 实现了 ValueParser
            .value_parser(["always", "auto", "never"])
            .default_value("auto")
    );

let cfg = Arg::new("config")
    .action(ArgAction::Set)
    .value_name("FILE")
    // [PossibleValue; 3] 实现了 ValueParser
    .value_parser([
        PossibleValue::new("fast"),
        PossibleValue::new("slow").help("slower than fast"),
        PossibleValue::new("secret speed").hide(true)
    ]);

derive 宏:

  • 使用 #[arg(value_enum)] 来修饰枚举类型的 field;
  • 对枚举类型使用 #[derive(ValueEnum)] 进行修饰;
  • 使用 #[value(POSSIBLE VALUE ATTRIBUTE)] 来修饰 variant field;
/// Doc comment
#[derive(Parser)]
#[command(CMD ATTRIBUTE)]
#[group(GROUP ATTRIBUTE)]
struct Cli {
    /// Doc comment
    #[arg(ARG ATTRIBUTE)]
    field: UserType,

    #[arg(value_enum, ARG ATTRIBUTE...)]
    field: EnumValues,
}

/// Doc comment
#[derive(ValueEnum)]
#[value(VALUE ENUM ATTRIBUTE)]
enum EnumValues {
    /// Doc comment
    #[value(POSSIBLE VALUE ATTRIBUTE)]
    Variant1,
}

Vec 类型 Arg
#

builder 风格:

  • 使用 .action(clap::ArgAction::Append) 来表明将多个 --flag v1 --flag v2 (flag 参数) 或 v1 v2(位置参数)的 v1/v2 值追加到 Vec 中。
  • 使用 .num_args(2) 为单个 flag 指定参数值的数量(默认为 1),如 --flag v1 v2,也可以指定 Range,如 2..,表示至少 2 个位置参数;
// builder 风格
let cmd = Command::new("mycmd")
    .arg(
        Arg::new("flag")
            .long("flag") // 调用 short/long 方法后则设置为 flag 参数
            .action(clap::ArgAction::Append) // 默认 action 为 ArgAction::Set,Vec 需要设置为 ArgAction::Append
    );

let matches = cmd.try_get_matches_from(["mycmd", "--flag", "value", "--flag" "value2"]).unwrap();
assert!(matches.contains_id("flag"));
assert_eq!(
    // 需要使用 get_many() 方法来获得 Vec
    matches.get_many::<String>("flag").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),  vec!["value", "value2"]
);

// num_args 指定参数值的个数,这里表示 -F 有两个参数值,如 -F in-file out-file
let cmd = Command::new("prog")
    .arg(Arg::new("file")
        .action(ArgAction::Set) // 默认为 Set 类型
        .num_args(2) // 2 个参数值,也可以指定 Range,如 2.. 表示至少 2 个位置参数
        .short('F'));

let m = cmd.clone().get_matches_from(vec!["prog", "-F", "in-file", "out-file"]);
assert_eq!(
    // 需要使用 get_many() 方法来获得 Vec
    m.get_many::<String>("file").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),  vec!["in-file", "out-file"]
);
// 只指定一个参数值时报错
let res = cmd.clone().try_get_matches_from(vec!["prog", "-F", "file1"]);
assert_eq!(res.unwrap_err().kind(), ErrorKind::WrongNumberOfValues);

derive 风格: 使用 Vec<T> 类型:

  • clap 隐式调用 .action(ArgAction::Append).required(false),即认为该参数值是可选的;
  • 默认为 T 自动添加 .value_parser(value_parser!(T))。 如果不符合预期,则需要手动指定 value_parser 参数。
// derive 风格:
#[derive(Parser, Debug)]
struct RemoveArgs {
    // flag 参数
    #[arg(short, long)]
    force: bool,

    // 未加 #[arg],所以为位置参数。
    // name 是可选的位置参数,未指定时为空 Vec,多个值会收集到 Vec 中。
    name: Vec<String>,
    
    #[clap(value_parser = bytes_from_str)]
    name2: String 
}

Option 类型 Arg
#

  • builder 风格: 为 Arg 设置 .required(false);
  • derive 风格:
    • Vec<T> 或 Option<Vec<T>>:clap 自动调用 .action(ArgAction::Append).required(false)
    • Option<T>: clap 自动调用 .required(false)
    • 默认为 T 自动添加 .value_parser(value_parser!(T)), 如果不符合预期, 则需要手动指定 value_parser 参数。
let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("append")
            .value_parser(clap::builder::FalseyValueParser::new())
            .required(false)
    );

// 使用 Fn 闭包来实现 TypedValueParser:
// Result 的 Ok 值类型需要和 arg 字段类型值 T/Option<T>/Vec<T>/Option<Vec<T>> 中的 T 一致。
fn bytes_from_str(src: &str) -> Result<Bytes, Infallible> {
    Ok(Bytes::from(src.to_string()))
}

#[derive(Subcommand, Debug)]
enum Command {
    Ping {
        // 手动指定实现 TypedValueParser 的类型。
        #[clap(value_parser = bytes_from_str)]
        msg: Option<Bytes>,
    }
}

Global Arg
#

可以为 Arg 设置 global 为 true,这样该参数将在所有 subcommand 中可用:

let m = Command::new("prog")
    .arg(Arg::new("verb")
        .long("verbose")
        .short('v')
        .action(ArgAction::SetTrue)
        .global(true))
    .subcommand(Command::new("test"))
    .subcommand(Command::new("do-stuff"))
    .get_matches_from(vec![
        "prog", "do-stuff", "--verbose"
    ]);

assert_eq!(m.subcommand_name(), Some("do-stuff"));
let sub_m = m.subcommand_matches("do-stuff").unwrap();
assert_eq!(sub_m.get_flag("verb"), true);

Args Group
#

ArgGroup 类型用于将 Arg 进行分组,这样便于定义必须、互斥等规则:

  • 将整个 ArgGroup 设置为必需的,意味着在运行时该组中必须存在一个(且仅一个)参数。
  • 将 ArgGroup 命名为与另一个参数冲突。这意味着该组中的任何参数如果与冲突参数同时存在,将导致失败。
  • 确保参数之间的互斥性。
  • 从组中提取值,而不是确定具体使用了哪个参数。

在显示 help 时按照 Group 显示 Args:

use std::collections::BTreeMap;

use clap::{command, value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command};

fn main() {
    let matches = cli().get_matches();
    let values = Value::from_matches(&matches);
    println!("{values:#?}");
}

fn cli() -> Command {
    command!()
        .group(ArgGroup::new("tests").multiple(true))
        .next_help_heading("TESTS")
        .args([
            position_sensitive_flag(Arg::new("empty"))
                .long("empty")
                .action(ArgAction::Append)
                .help("File is empty and is either a regular file or a directory")
                .group("tests"),
            
            Arg::new("name")
                .long("name")
                .action(ArgAction::Append)
                .help("Base of file name (the path with the leading directories removed) matches shell pattern pattern")
                .group("tests")
        ])
        
        .group(ArgGroup::new("operators").multiple(true))
        .next_help_heading("OPERATORS")
        .args([
            position_sensitive_flag(Arg::new("or"))
                .short('o')
                .long("or")
                .action(ArgAction::Append)
                .help("expr2 is not evaluate if exp1 is true")
                .group("operators"),
            
            position_sensitive_flag(Arg::new("and"))
                .short('a')
                .long("and")
                .action(ArgAction::Append)
                .help("Same as `expr1 expr1`")
                .group("operators"),
        ])
}

fn position_sensitive_flag(arg: Arg) -> Arg {
    // Flags don't track the position of each occurrence, so we need to emulate flags with value-less options to get the same result
    arg.num_args(0)
        .value_parser(value_parser!(bool))
        .default_missing_value("true")
        .default_value("false")
}

#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum Value {
    Bool(bool),
    String(String),
}

impl Value {
    pub fn from_matches(matches: &ArgMatches) -> Vec<(clap::Id, Self)> {
        
        let mut values = BTreeMap::new();
        
        for id in matches.ids() {
            if matches.try_get_many::<clap::Id>(id.as_str()).is_ok() {
                // ignore groups
                continue;
            }
            let value_source = matches .value_source(id.as_str()) .expect("id came from matches");
            if value_source != clap::parser::ValueSource::CommandLine {
                // Any other source just gets tacked on at the end (like default values)
                continue;
            }
            if Self::extract::<String>(matches, id, &mut values) {
                continue;
            }
            if Self::extract::<bool>(matches, id, &mut values) {
                continue;
            }
            unimplemented!("unknown type for {id}: {matches:?}");
        }
        
        values.into_values().collect::<Vec<_>>()
    }

    fn extract<T: Clone + Into<Value> + Send + Sync + 'static>(
        matches: &ArgMatches,
        id: &clap::Id,
        output: &mut BTreeMap<usize, (clap::Id, Self)>,
    ) -> bool {
        
        match matches.try_get_many::<T>(id.as_str()) {
            Ok(Some(values)) => {
                for (value, index) in values.zip(matches.indices_of(id.as_str()).expect("id came from matches"),) {
                    output.insert(index, (id.clone(), value.clone().into()));
                }
                true
            }
            Ok(None) => {unreachable!("`ids` only reports what is present")}
            Err(clap::parser::MatchesError::UnknownArgument { .. }) => {unreachable!("id came from matches")}
            Err(clap::parser::MatchesError::Downcast { .. }) => false,
            Err(_) => {unreachable!("id came from matches")}
        }
    }
}

impl From<String> for Value {
    fn from(other: String) -> Self {
        Self::String(other)
    }
}

impl From<bool> for Value {
    fn from(other: bool) -> Self {
        Self::Bool(other)
    }
}

// $ find --help
// A simple to use, efficient, and full-featured Command Line Argument Parser

// Usage: find[EXE] [OPTIONS]

// Options:
//   -h, --help     Print help
//   -V, --version  Print version

// TESTS:
//       --empty        File is empty and is either a regular file or a directory
//       --name <name>  Base of file name (the path with the leading directories removed) matches shell
//                      pattern pattern

// OPERATORS:
//   -o, --or   expr2 is not evaluate if exp1 is true
//   -a, --and  Same as `expr1 expr1`

derive 宏
#

通过 derive 和属性宏来声明式定义命令和参数,需要启用 derive 和 cargo feature。

  • #[derive(Parser)] : 将命令行参数解析到类型自身(Self)。
  • #[derive(Args()] : 将一组参数解析到用户定义的容器中。
  • #[derive(Subcommand)] : 将子命令解析为用户定义的枚举。必须与 enum 结合使用。
  • #[derive(ValueEnum)] : 将参数解析为枚举类型。

和它们配合使用还有 #[command()]#[arg()] 属性宏。

示例:

use std::path::PathBuf;

use clap::{Args, Parser, Subcommand, ValueEnum};

#[derive(Debug, Parser)]
#[command(name = "gomod-local", about = "Local development helper for go.mod replacements")]
pub struct Cli {
    #[arg(long, default_value = "gomod-local.json", value_name = "FILE")]
    pub config: PathBuf,
    
    #[arg(long, default_value = ".gomod-local.lock", value_name = "FILE")]
    pub lock: PathBuf,
    
    #[arg(long)]
    pub dry_run: bool,
    
    #[command(subcommand)]
    pub command: Commands,
}

#[derive(Debug, Subcommand)]
pub enum Commands {
    Enable(EnableArgs),
    Status(StatusArgs),
    Verify(VerifyArgs),
}

#[derive(Debug, Args, Default)]
pub struct EnableArgs {
    #[arg(long)]
    pub no_tidy: bool,
    
    #[arg(long)]
    pub skip_hooks: bool,
}

#[derive(Debug, Args, Default)]
pub struct StatusArgs {
    #[arg(long, value_enum, default_value = "human")]
    pub output: StatusOutput,
}

#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq)]
pub enum StatusOutput {
    Human,
    Json,
}

impl Default for StatusOutput {
    fn default() -> Self {
        StatusOutput::Human
    }
}

#[derive(Debug, Args)]
pub struct GuardArgs {
    #[arg(required = true)]
    pub tool: String,
    #[arg()]
    pub args: Vec<String>,
}

#[derive(Debug, Args)]
pub struct HookArgs {
    #[command(subcommand)]
    pub command: HookCommand,
}

#[derive(Debug, Subcommand)]
pub enum HookCommand {
    Install(HookInstallArgs),
    Uninstall,
    Run(HookRunArgs),
}

#[derive(Debug, Args, Default)]
pub struct HookInstallArgs {
    #[arg(long)]
    pub overwrite: bool,
    #[arg(long)]
    pub attributes: bool,
}

#[derive(Debug, Args)]
pub struct HookRunArgs {
    #[arg(value_enum)]
    pub event: HookEvent,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum HookEvent {
    #[value(name = "pre-commit")]
    PreCommit,
    #[value(name = "post-commit")]
    PostCommit,
    #[value(name = "pre-push")]
    PrePush,
    #[value(name = "post-checkout")]
    PostCheckout,
    #[value(name = "post-merge")]
    PostMerge,
}

#[derive(Debug, Args)]
pub struct FilterArgs {
    #[command(subcommand)]
    pub command: FilterCommand,
}

#[derive(Debug, Subcommand)]
pub enum FilterCommand {
    Clean(FilterCleanArgs),
}

#[derive(Debug, Args, Default)]
pub struct FilterCleanArgs {
    #[arg(long)]
    pub strict: bool,
}

#[derive(Parser)]
#

命令行解构的入口类型, 可以联合使用 #[command]#[arg] 属性宏:

  • 一般用于 struct 类型,因为它的 filed 可以作为命令行参数;
  • 使用 #[command] 设置 struct 整体,即 Command 的各种参数, 如 next_line_help="xx"
  • 使用 #[command(subcommand)] /#[command(flattern)] / #[arg()] 来修饰 field;
  • 使用 /// 注释为 Parser、Command、Arg 快捷添加显示的 about、help 字符串;
use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
// 添加 version、about 但是没有赋值时,自动从 Cargo.toml 中获取相关信息。
#[command(version, about, long_about = None)]
// 等效于
#[command(version, about, long_about = None, about="Simple program to greet a person")]
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() {
    // 通过 #[derive(Parser)] 为 Args 实现了 Parser trait,故可以调用它的 parse() 方法
    let args = Args::parse();

    for _ in 0..args.count {
        println!("Hello {}!", args.name);
    }
}

执行效果:

$ demo --help
A simple to use, efficient, and full-featured Command Line Argument Parser

Usage: demo[EXE] [OPTIONS] --name <NAME>

Options:
  -n, --name <NAME>    Name of the person to greet
  -c, --count <COUNT>  Number of times to greet [default: 1]
  -h, --help           Print help
  -V, --version        Print version

$ demo --name Me
Hello Me!

Parser 的方法:

  • parse():从 std::env::args_os 解析参数
  • parse_from():从传入的字符串来解析(需要包含 arg0 也即完整的命令和参数);
pub trait Parser: FromArgMatches + CommandFactory + Sized {
    // Provided methods
    fn parse() -> Self
    fn try_parse() -> Result<Self, Error>

    fn parse_from<I, T>(itr: I) -> Self where I: IntoIterator<Item = T>, T: Into<OsString> + Clone
    fn try_parse_from<I, T>(itr: I) -> Result<Self, Error> where I: IntoIterator<Item = T>, T: Into<OsString> + Clone

    fn update_from<I, T>(&mut self, itr: I) where I: IntoIterator<Item = T>, T: Into<OsString> + Clone
    fn try_update_from<I, T>(&mut self, itr: I) -> Result<(), Error> where I: IntoIterator<Item = T>, T: Into<OsString> + Clone
}

#[arg(xx)]
#

指定 field 为 Arg flag 参数类型(默认为位置参数), 可以使用各种 Args builder 方法,如 long():

  • Arg.action() 的参数 ArgAction 默认为 Set/SetTrue, 对于 Vec 等类型需要明确设置为 Append
  • Arg field 的类型要求:
  • 参数默认是必选的,例外情况:
    • 使用 Option 类型;
    • Vec 也是可选的,未指定时为空列表;
    • 指定缺省值,如 #[arg(default_value_t = 2020)],参数也是可选的;
  • default_value_t 设置缺省值表达式 expr;
    • 如果未指定 expr,则类型需要实现 Default trait。
    • 指定该 attr 时,该 flag 是可选的(默认如果不是 Option 类型,则是必选的 flag)。
  • Vec : 可以为位置参数和 flag 参数指定,对于 flag 参数,指定多次时, 各参数值被 Append 到 Vec 中,
    • #[arg(value_delimiter = ':')] 表示使用 : 分割多个字符串(默认空格)
  • 指定 num_args(2) 时,为单个 flag 指定参数值的数量(默认为 1),如 --flag v1 v2,也可以指定 Range,如 2..,表示至少 2 个位置参数;

clap 根据各 field 的类型 XX, 来自动调用 value_parser!(XX), 所以 XX 必须是实现 ValueParserFactory 的类型。

对于未实现 ValueParserFactory 的类型,可以调用 Arg 的 value_parser() 方法,传入实现 impl IntoResettable<ValueParser> 的类型对象。

clap 为 Into<ValueParser>、Option<ValueParser> 实现了 IntoResettable<ValueParser>:

而实现了 Into<ValueParser> 的类型包括:

  • [P; C]Vec<P> 转换为 ValueParser, 其中 P 需要实现 Into<PossibleValue>,一般为 String/&str 等;
  • 从实现 TypedValueParser trait 转换为 ValueParser
    • 各种预定义的实现 TypedValueParser 的类型,如 BoolValueParser、BoolishValueParser、FalseyValueParser 等;
    • 闭包,如 #[arg(value_parser = validate_package_name)]
  • 各种 RangeXX 类型,如 Range/RangeFrom/RangeFull 等;
use clap::Parser;

#[derive(Parser)]
// auth/version/about 等均为 Command builder 方法
// 1. 如果未赋值,则编译时从 Cargo.toml 获取缺省值。
#[command(author, version, about, long_about = None)]
// 2. 或者指定缺省值
#[command(name = "MyApp")]
// 可以调用 Command builder 的各种方法
#[command(next_line_help = true)]
struct Cli {
    // 位置参数:Option<T> 表示是可选的(类似的还有 Vec 和 bool 类型)
    name: Option<String>,

    // 指定缺省值后,参数也是可选的。
    #[arg(default_value_t = 2020)]
    port: u16,
    
    // 对于 flag 参数必须指定 #[arg], 否则为位置参数。
    #[arg(long)]
    two: String,

    // flag 参数:参数值用 : 分割,结果都合并到一个 Vec 中(默认空格分隔)
    #[arg(short, long, value_delimiter = ':')]
    name2: Vec<String>,

    // 可以使用 clap::builder::Arg 的各种方法来设置 arg 的参数
    #[arg(short = 'n')]
    #[arg(long = "name")]
    #[arg(short, long)] // 根据 field name 自动推断
    name: Option<String>, // field 类型是 value_parser!() 支持的类型,它们实现了 ValueParserFactory trait   
    
    // 调用 value_parser() 方法,传入实现 TypedValuePrarser trait 的类型,
    // 这里使用闭包实现 TypedValuePrarser triat。
    #[arg(value_parser = validate_package_name)] 
    package_name: String,
    
    /// Network port to use
    // clap::value_parser!(u16) 返回 clap::builder::RangedI64ValueParser 类型,它有 range 方法
    #[arg(value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
        
    // #[arg] 可以指定 Arg builder 的所有方法,如 default_value_t 来指定缺省值表达式,
    // 对于 short、long 参数,未设置值时自动推导。
    #[arg(short, long, default_value_t = 1)]
    one: String,

    #[arg(default_value_t = usize::MAX, short, long)]
    max_depth: usize,

    #[arg(short, long, default_value_t = String::from("default"))]
    namespace: String,

    // 自定义 action 类型,对指定的次数进行计数
    #[arg(short, long, action = clap::ArgAction::Count)] 
    verbosity: u8,
}

// 实现 TypedValuePrarser 的闭包,可以作为 value_parser() 的参数,用来解析命令行参数
fn validate_package_name(name: &str) -> Result<(), String> {
    if name.trim().len() != name.len() {
        Err(String::from( "package name cannot have leading and trailing space", ))
    } else {
        Ok(())
    }
}

fn main() {
    let cli = Cli::parse();
    println!("two: {:?}", cli.two);
    println!("one: {:?}", cli.one);
    println!("name: {:?}", cli.name.as_deref());
}

#[arg(value_enum)]/#[derive(ValueEnum)]
#

修饰枚举类型的 field,同时对应 field type 也需要使用 #[derive(ValueEnum) 来修饰:

#[derive(ValueEnum)
pub enum ColorChoice {
    Auto,
    Always,
    Never,
}

let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("color")
            .value_parser(clap::builder::EnumValueParser::<ColorChoice>::new())
            .required(true)
    );
let m = cmd.try_get_matches_from_mut(["cmd", "always"]).unwrap();
let port: ColorChoice = *m.get_one("color").expect("required");
assert_eq!(port, ColorChoice::Always);

// 例子
use clap::{Parser, ValueEnum};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(value_enum)] // 枚举类型 field
    mode: Mode,
}

// 枚举类型 field 需要使用 ValueEnum 来修饰
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
    Fast,
    Slow,
}

fn main() {
    let cli = Cli::parse();
    match cli.mode {
        Mode::Fast => {
            println!("Hare");
        }
        Mode::Slow => {
            println!("Tortoise");
        }
    }
}

#[command(xx)]
#

#[command(xx)] 属性宏用于设置 root command 或 sub command 的属性(实际是 Command builder 的方法调用):

#[derive(Parser)]
// 设置 root command 的属性
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(value_enum)] // 枚举类型 field
    mode: Mode,
    
    #[command(subcommand)]
    sub_cmds: Command 
}

/// Doc comment
#[derive(Subcommand)]
#[command(PARENT CMD ATTRIBUTE)]
enum Command {
    /// Doc comment
    // 设置 sub command 的属性
    #[command(CMD ATTRIBUTE)]
    Variant1(Struct),

    /// Doc comment
    #[command(CMD ATTRIBUTE)]
    Variant2 {
        /// Doc comment
        #[arg(ARG ATTRIBUTE)]
        field: UserType,
    }
}

#[command(subcommand)]/#[derive(Subcommand)]
#

在 Parser 的 struct 中,#[command(subcommand)] 来修饰 enum 类型,用来指定子命令。

  • 该 enum 类型整体必须用 #[derive(Subcommand)] 来修饰;
  • 该 enum 类型:
    • 每一个 variant 都是一个 subcommand;
    • 每一个 variant 的类型可以是 struct 或 onetype 类型 struct
    • struct 的 field 作为该 subcommand 的 args。该 struct 类型必须使用 #[derive(Args)] 修饰;
    • onetype struct field 分两种情况:
      1. 代表该命令行参数 args,则对应 struct 类型必须用 #[derive(Args)] 来修饰;
      2. 代表子命令,则对应 onetype struct field 用 #[command(flatten)] 来修饰;
use clap::{Args, Parser, Subcommand};

#[derive(Parser)]
#[command(version, about, long_about = None)]
// 每个 command 和 sub command 都可以有自己的 version
// 这里使用 root command 的 version 作为所有 sub command 的 version。
#[command(propagate_version = true)] 
struct Cli {
    #[command(subcommand)]
    command: Commands, // 这里的 sub command 是必须的,未指定时报错
    
    // 指定 sub command 是可选的
    //command: Option<Commands>
}

#[derive(Subcommand)]
enum Commands {
    /// Adds files to myapp
    Add(AddArgs),
}

#[derive(Args)]
struct AddArgs {
    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.name);
        }
    }
}

#[command(flatten)]
#

#[command(flatten)] 来修饰 struct 类型,将它的 field 作为到当前 Command 的参数,该 struct 类型必须使用 #[derive(Args)] 来修饰;

#[command(flatten)] : 两种使用场景:

  1. #[derive(Parser)]#[derive(Args)] 中:field type 必须是使用 #[derive(Args)] 修饰的 struct 类型,用于将指定的 field 作为 Args 打平到当前命令中;
  2. #[derive(Subcommand)] 中: field type 必须是实现 #[derive(Subcommand)] 的 enum 类型,用于指定下一个层级的子命令;
use clap::{Args, Parser};

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct Cli {
    #[command(flatten)]
    vers: Vers,

    /// 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>,
}

#[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,
}

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()
    } 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() {
        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(Args)]
#

只支持 non-tuple struct 类型,即只能通过 struct field 来定义 Args;

pub fn test_clap() {
    use clap::{Args, Parser, Subcommand};
    use std::fmt::Debug;

    /// Cli command
    #[derive(Parser, Debug)]
    struct Cli {
        #[command(flatten)]
        // Parser/Args 场景下,flatten 的 field type 必须是 struct 类型,该类型必须实现 Args,它的 field 作为命令行 Arg
        delegate: Struct,

        // 必须修饰 enum 类型
        #[command(subcommand)]
        command: Command,
    }

    // Args 必须修饰 struct,不需要对每个 field 添加 #[arg]
    #[derive(Args, Debug)]
    struct Struct {
        /// field arg
        field: u8,
    }

    /// sub command
    #[derive(Subcommand, Debug)]
    // Subcommand 必须修饰 enum 类型, 各 variant 是 struct 或 newtype 类型,struct 的各 field 为命令的参数 Args,
    // newtype 的 type 必须是 struct 类型。
    enum Command {
        // 每一个 Variant 都是一个 subcommand

        /// cmd1, newtype 类型, Struct 必须实现 Args。
        Cmd1(Struct),

        /// cmd2, 支持 struct variant 类型, struct field 为命令行参数(不需要添加 #[arg])
        Cmd2 {
            // Variant struct 的 field 对应 subcommand 的 args field
            field: u8,
        },

        #[command(flatten)]
        // SubCommand 中也可以使用 flatten,但它必须是实现 Subcommand 的 enum 类型.
        Variant3(SubCmd),
    }

    /// sub command
    #[derive(Subcommand, Debug)]
    enum SubCmd {
        /// Doc comment
        Sub1 { field: u8 },
    }

    let cli = Cli::parse();
    println!("{:?}", cli);
}


// cargo run:

// Usage: foo <FIELD> <COMMAND>

// Commands:
//   cmd1  cmd1, 支持 tuple 类型, 但则只支持一个类型的 newtype, 不支持 2 个及以上的 tuple
//   cmd2  cmd2, 支持 struct 类型, struct field 为命令行参数(不需要添加 #[arg])
//   sub1  Doc comment
//   help  Print this message or the help of the given subcommand(s)

// Arguments:
//   <FIELD>  field arg

// Options:
//   -h, --help  Print help

// cargo run cmd1 -h

// cmd1, 支持 tuple 类型, 但则只支持一个类型的 newtype, 不支持 2 个及以上的 tuple

// Usage: foo <FIELD> cmd1 <FIELD>

// Arguments:
//   <FIELD>  field arg

// Options:
//   -h, --help  Print help

// cargo run cmd2 -h

// cmd2, 支持 struct 类型, struct field 为命令行参数(不需要添加 #[arg])

// Usage: foo <FIELD> cmd2 <FIELD>

// Arguments:
//   <FIELD>  field

// Options:
//   -h, --help  Print help

help template
#

use std::io::Write;
use clap::Command;

fn main() -> Result<(), String> {
    loop {
        let line = readline()?;
        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        match respond(line) {
            Ok(quit) => {
                if quit {
                    break;
                }
            }
            Err(err) => {
                write!(std::io::stdout(), "{err}").map_err(|e| e.to_string())?;
                std::io::stdout().flush().map_err(|e| e.to_string())?;
            }
        }
    }

    Ok(())
}

fn respond(line: &str) -> Result<bool, String> {
    let args = shlex::split(line).ok_or("error: Invalid quoting")?;

    let matches = cli()
        .try_get_matches_from(args) // 从自定义输入中解析命令行选项和参数
        .map_err(|e| e.to_string())?;

    match matches.subcommand() {
        Some(("ping", _matches)) => {
            write!(std::io::stdout(), "Pong").map_err(|e| e.to_string())?;
            std::io::stdout().flush().map_err(|e| e.to_string())?;
        }
        Some(("quit", _matches)) => {
            write!(std::io::stdout(), "Exiting ...").map_err(|e| e.to_string())?;
            std::io::stdout().flush().map_err(|e| e.to_string())?;
            return Ok(true);
        }
        Some((name, _matches)) => unimplemented!("{name}"),
        None => unreachable!("subcommand required"),
    }
    Ok(false)
}

fn cli() -> Command {
    // help template
    const PARSER_TEMPLATE: &str = "\
        {all-args}
    ";

    // 另一个 help template
    const APPLET_TEMPLATE: &str = "\
        {about-with-newline}\n\
        {usage-heading}\n    {usage}\n\
        \n\
        {all-args}{after-help}\
    ";

    Command::new("repl")
        .multicall(true)
        .arg_required_else_help(true)
        .subcommand_required(true)
        .subcommand_value_name("APPLET")
        .subcommand_help_heading("APPLETS")
        .help_template(PARSER_TEMPLATE)
        .subcommand(
            Command::new("ping")
                .about("Get a response")
                .help_template(APPLET_TEMPLATE),
        )
        .subcommand(
            Command::new("quit")
                .alias("exit")
                .about("Quit the REPL")
                .help_template(APPLET_TEMPLATE),
        )
}

fn readline() -> Result<String, String> {
    write!(std::io::stdout(), "$ ").map_err(|e| e.to_string())?;
    std::io::stdout().flush().map_err(|e| e.to_string())?;
    let mut buffer = String::new();
    std::io::stdin()
        .read_line(&mut buffer)
        .map_err(|e| e.to_string())?;
    Ok(buffer)
}

自定义帮助提示风格
#

use clap::Parser;

#[derive(Parser)] // requires `derive` feature
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
#[command(styles = CLAP_STYLING)]
enum CargoCli {
    ExampleDerive(ExampleDeriveArgs),
}

// See also `clap_cargo::style::CLAP_STYLING`
pub const CLAP_STYLING: clap::builder::styling::Styles = clap::builder::styling::Styles::styled()
    .header(clap_cargo::style::HEADER)
    .usage(clap_cargo::style::USAGE)
    .literal(clap_cargo::style::LITERAL)
    .placeholder(clap_cargo::style::PLACEHOLDER)
    .error(clap_cargo::style::ERROR)
    .valid(clap_cargo::style::VALID)
    .invalid(clap_cargo::style::INVALID);

#[derive(clap::Args)]
#[command(version, about, long_about = None)]
struct ExampleDeriveArgs {
    #[arg(long)]
    manifest_path: Option<std::path::PathBuf>,
}

fn main() {
    let CargoCli::ExampleDerive(args) = CargoCli::parse();
    println!("{:?}", args.manifest_path);
}

indicatif 终端进度条指示
#

终端进度条指示。

https://docs.rs/indicatif/latest/indicatif/

eyre 用户友好的彩色报错信息显示
#

https://docs.rs/eyre/latest/eyre/

assert_cmd
#

提供 cmd 的测试功能。

https://docs.rs/assert_cmd/latest/assert_cmd/

参考
#

  1. 各种命令行程序参考: https://github.com/uutils/coreutilshttps://github.com/rustcoreutils/posixutils-rs
  2. https://www.shuttle.rs/blog/2023/12/08/clap-rust
  3. https://blog.logrocket.com/using-clap-rust-command-line-argument-parsing/
  4. Command Line Applications in Rust
rust crate - 这篇文章属于一个选集。
§ 12: 本文

相关文章

axum
·
axum 是基于 hyper 实现的高性能异步 HTTP 1/2 Server 库。
tokio
·
Tokio 是 Rust 主流的异步运行时库。它提供了异步编程所需要的所有内容:单线程或多线程的异步任务运行时、工作窃取、异步网络/文件/进程/同步等 APIs。
config
·
config 提供从文件或环境变量解析配置参数的功能。
diesel
·
diesel 是高性能的 ORM 和 Query Builder,crates.io 使用它来操作数据库。