跳过正文

clap

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

两种使用方式:

  1. derive macro
  2. builder

buidler:

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 macro 示例:

use clap::Parser;

#[derive(Parser)]
#[command(name = "cargo")]
#[command(bin_name = "cargo")]
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);
}

1 ValueParser
#

ValueParser 是 Arg::value_parser() 的参数类型,用于解析和校验参数。两种创建方式:

  1. value_parser!() :自动根据类型选择对应的实现, 参数是 实现 ValueParserFactory traint 的类型名称 ,如 std::path::PathBuf 或其他自定义类型等;
  2. ValueParser::new() :使用自定义的 TypedValueParser 类型来创建 ValueParser
    • pub fn new<P>(other: P) -> ValueParser where P: TypedValueParser
    • clap::builder 提供了一些实现 TypedValueParser trait 的类型, 如 BoolValueParser 等;

1.1 value_parser!()
#

value_parser!() 宏的参数是 Rust 类型,但需要通过 clap::builder::ValueParserFactory 来注册:

  1. clap 为各种 Rust 内置类型实现了 =ValueParserFactory=:
    • Native types: bool, String, OsString, PathBuf
    • Ranged numeric types: u8, i8, u16, i16, u32, i32, u64, i64
  2. ValueEnum types
  3. From<OsString> types 和 From<&OsStr> types
  4. From<String> types 和 From<&str> types
  5. FromStr types, including usize, isize
  6. 自定义类型;
// Register a type with value_parser!
pub trait ValueParserFactory {
    //  关联类型 Parser 一般也是实现 TypedValueParser 的自定义类型;
    type Parser;

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

// clap 内置注册的类型
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

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

示例:

// 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).range(3000..);
clap::value_parser!(u64).range(3000..);

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

// ValueEnum types
pub enum ColorChoice {
    Auto,
    Always,
    Never,
}
clap::value_parser!(ColorChoice);

1.2 TypedValuePrarser
#

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

  • 闭包 Fn(&str) -> Result<T,E> 也实现了 TypedValueParser:是最快捷实现自定义解析逻辑;
pub trait TypedValueParser: Clone + Send + Sync + 'static {
    type Value: Send + Sync + Clone;

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

// clap::builder 提供了如下实现类型:
impl TypedValueParser for BoolValueParser // 返回 bool
impl TypedValueParser for BoolishValueParser // 类似于 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 // 枚举值

//  Fn(&str) -> Result<T,E> 闭包也实现了 TypedValueParser
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,

例子:

// 使用预定义的 ValueParser
let mut cmd = clap::Command::new("raw")
    .arg(
        clap::Arg::new("append")
            .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");

// 使用宏来创建 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);

// 使用 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 {
        #[clap(value_parser = bytes_from_str)] // value_parser 的值为实现 TypedValueParser 的类型。
        msg: Option<Bytes>,
    }
}

通过实现 clap::builder::ValueParserFactoryclap::builder::TypedValueParser 可以为自定义类型实现参数解析:

  • 内部一般使用 clap::value_parser!() 宏创建的 CustomValueParser 来解析参数。
#[derive(Copy, Clone, Debug)]
pub struct Custom(u32);

impl clap::builder::ValueParserFactory for Custom {
    type Parser = CustomValueParser;

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

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

1.3 Arg::value_parser()
#

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

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

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

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

impl<I> IntoResettable<ValueParser> for I where I: Into<ValueParser>
impl IntoResettable<ValueParser> for Option<ValueParser>

任何能 Into<ValueParser> 的类型值都可以作为 Arg::value_parser() 的参数:

  • value_parser!(T) :根据 T 的类型自动选择合适的实现。
  • 各种 RangeXX,如 0..=1 代表 RangedI64ValueParser;
  • [P; C] 和 Vec<P> ,其中 P 需要实现 Into<PossibleValue>,一般为 String/&str 等;
  • [&str] 和 PossibleValuesParser :静态枚举类型;
  • BoolishValueParser 和 FalseyValueParser :布尔值语义
  • NonEmptyStringValueParser :字符串基本校验
  • 任何实现了 TypedValueParser 的类型;
    • Fn(&str) -> Result<T, E> :该闭包实现了 TypedValueParser trait.
  • Option<ValueParser>;
// 从 [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
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
// 更灵活的是为实现 TypedValueParser 的自定义类型创建 ValueParser
pub fn new<P>(other: P) -> ValueParser where P: TypedValueParser

PossibleValue 和 PossibleValuesParser:

  1. 任何可以 Into<Str> 的类型都可以转成 PossibleValue;
  2. 任何可以迭代生成 PossibleValue 的类型都可以转成 PossibleValuesParser;
  3. PossibleValuesParser 实现了 TypedValueParser,可以作为 Arg::value_parser() 的参数;
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")
            // 必须都是字符串列表
            //.value_parser(vec!["always", "auto", "never"]))
            //.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");

2 builder
#

builder 模式使用如下核心类型:

  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): 根据指定的 typeName(实现了 TypeValueParserFactory)设置 Arg 的 ValueParser 实现;

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

  • crate_authors : Allows you to pull the authors for the command from your Cargo.toml at compile time in the form:“author1 lastname <[email protected]>:author2 lastname <[email protected]>”
  • crate_description : Allows you to pull the description from your Cargo.toml at compile time.
  • crate_name : Allows you to pull the name from your Cargo.toml at compile time.
  • crate_version : Allows you to pull the version from your Cargo.toml at compile time as 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();

// 从 Cargo.toml 中读取和设置 Command 的 name/version/authors/description 信息。
// 使用 arg!(aString) 宏来快捷创建 Arg。
let matches = command!()
    .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(
        Command::new("test")
            .about("does testing things")
            .arg(arg!(-l --list "lists test values").action(ArgAction::SetTrue)))
    .get_matches();

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...");
    }
}

2.1 Command
#

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

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

2.2 Arg
#

Arg 默认特性:

  • 未调用 value_parser() 指定 ValueParser 时,默认为 StringValueParser,所以默认解析为 String;
  • 未调用 action() 时默认为 ArgAction::Set,对于 Vec 需要指定为 action(ArgAction::Append));
  • 参数是必须的,需要调用 .required(false) 方法来设置为可选;
let cmd = Command::new().arg(
    Arg::new("in_file")
)

let matches = command!()
    .arg(arg!([name] "Optional name to operate on").required(false))
    .arg(arg!(-l --list "lists test values").action(ArgAction::SetTrue))

// 为 Command 使用 args!() 来创建 Arg 对象,内部使用 value_parser!() 来指定解析后的值类型;
let matches = command!()
    .arg(
        arg!([PORT])
            .value_parser(value_parser!(u16)) // Rust 内置类型
            .default_value("2020"),
    )
    .get_matches();

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)
        ]);

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

let cfg = Arg::new("config")
          .short('c')
          .long("config")
          .action(ArgAction::Set)
          .value_name("FILE")
          .help("Provides a config file to myprog");

// 使用 arg!() 来创建 Arg
let input = arg!(-i --input <FILE> "Provides an input file to the program");

    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("color")
            .long("color")
            // [&str; 3] 实现了 ValueParser
            .value_parser(["always", "auto", "never"])
            .default_value("auto")
    );

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

Vec 类型 Arg 的实现:

  • builder 风格:
    • 方式一:使用 .action(clap::ArgAction::Append) 来指定 flag Arg 出现多次时追加到 Vec。
    • 方式二:使用 .num_args(2) 为单个 flag Arg 指定参数值的数量(默认为 1),也可以指定 Range,如 2.. 表示至少 2 个位置参数;
  • derive 风格: 使用 Vec<T> 类型:
    • clap 隐式调用 .action(ArgAction::Append).required(false);
    • 默认为 T 自动添加 .value_parser(value_parser!(T)), 如果不符合预期则需要为自定义类型实现 ValueParserFactory;
// builder 风格
let cmd = Command::new("mycmd")
    .arg(
        Arg::new("flag")
            .long("flag")
            .action(clap::ArgAction::Append)
    );

let matches = cmd.try_get_matches_from(["mycmd", "--flag", "value", "--flag" "value2"]).unwrap();
assert!(matches.contains_id("flag"));
assert_eq!(
    matches.get_many::<String>("flag").unwrap_or_default().map(|v| v.as_str()).collect::<Vec<_>>(),
    vec!["value", "value2"]
);

// derive 风格
#[derive(Parser, Debug)]
struct AddArgs {
    name: Vec<String>, // name 是可选的(未指定时为空 Vec),为位置参数。
}

#[derive(Parser, Debug)]
struct RemoveArgs {
    #[arg(short, long)]
    force: bool,

    name: Vec<String>,
}

// num_args 指定参数值的个数
let cmd = Command::new("prog")
    .arg(Arg::new("file")
        .action(ArgAction::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!(
    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);

Option 类型 Arg 的实现:

  • builder 风格: 为 Arg 设置 .required(false);
  • derive 风格:
    • Option<Vec<T>>:clap 自动调用 .action(ArgAction::Append).required(false)
    • Option<T>: clap 自动调用 .required(false)
    • 默认为 T 自动添加 .value_parser(value_parser!(T)), 如果不符合预期, 如 T 不是 Rust 原始类型, 则需要为自定义类型实现 ValueParserFactory;
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>,
    }
}

Args Group:在显示 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`

3 derive
#

通过 derive&attr macro 来声明式定义命令和参数,需要启用 derive 和 cargo feature。

derive macro:

  • Parser : Parse command-line arguments into Self.
  • Args : Parse a set of arguments into a user-defined container.
  • Subcommand : Parse a sub-command into a user-defined enum. 必须和 enum 结合使用。
  • ValueEnum : Parse arguments into enums.

Parser: 命令行解构的入口类型, 可以联合使用 command 和 arg attribue macro,前者修饰整个 struct,后者修饰 field:

  • 一般修饰的是 struct 类型:
  • #[command] 指定命令参数, 可以使用任何 Command builder 的方法, 如 Command::next_line_help。
  • field 默认为参数选项(Arg)或位置参数;
  • 使用 #[command(subcommand)] 来修饰的 enum field 为子命令;
  • parse() 默认从 std::env::args_os 解析参数, 使用 parse_from() 来传入其他命令行字符串来源;
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 { ... }
}

使用 / 注释为 Parser、Command、Arg 快捷添加 about 字符串,这样就不需要明确为 about 赋值:

#[command(author = "Author Name", version, about)]
/// A Very simple Package Hunter
struct Arguments {...}

// 等效于

#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about="A Very simple Package Hunter")]
struct Arguments{...}

#[arg] :指定 field 为 Arg 选项(默认为位置参数), 可以使用任何 Args builder 方法,如 long:

  • Arg.action() 的参数 ArgAction 默认为 Set/SetTrue, 对于 Vec 等类型需要明确设置为 Append
  • Arg field 的类型要求:
    • 参数默认是必选的,使用 Option<T> 来表明可选;
    • default_value_t 设置缺省值表达式 expr;
      • 如果未指定 expr,则类型需要实现 Default trait。
      • 指定该 attr 时,该 flag 是可选的(默认如果不是 Option 类型,则是必选的 flag)。
    • Vec<XX> : 可以指定多次 flag, 各参数值被 Append 到 Vec 中,
      • #[arg(value_delimiter = ‘:’)] 表示使用:分割多个字符串(默认空格)

clap 根据各 field 的类型 XX, 来设置 value_parser!(XX), 所以 XX 必须是实现 ValueParser 的类型。

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 {
    // 对于 flag 参数必须指定 #[arg], 否则为位置参数。
    #[arg(long)]
    two: String,

    // 使用 default_value_t 来指定缺省值表达式
    // short、long:为 Arg builder 方法,设置单未赋值时,使用自动推导。
    #[arg(short, long, default_value_t = 1)]
    one: String,

    #[arg(default_value_t = usize::MAX, short, long)]
    /// maximum depth to which sub-directories should be explored
    max_depth: usize,

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

    // 位置参数:没有指定任何 clip 相关的 attr 的 field 为位置参数;
    // Option<T> 表示是可选的。
    name: Option<String>,

    // 命令行选项,可以多次 flag,结果都合并到一个 Vec 中(默认空格分隔)
    #[arg(short, long, value_delimiter = ':')] // 使用 : 分割
    name2: Vec<String>,

    #[arg(value_parser = validate_package_name)] // 使用自定义解析函数
    /// Name of the package to search
    package_name: String,

    #[arg(short, long, action = clap::ArgAction::Count)] // 自定义 action 类型
    verbosity: u8,

    // 可以使用 clap::builder::Arg 的各种方法来设置 arg 的参数
    #[arg(short = 'n')]
    #[arg(long = "name")]
    #[arg(short, long)] // 根据 field name 自动推断
    name: Option<String>, // field 类型可以是任何 clap 支持的类型
}

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

#[derive(subcommand)] : 只支持 enum 类型:

  • 每一个 variant 都是一个 subcommand;
  • variant 类型可以是 struct 或 onetype struct:
    • struct 或 onetype 的 struct 用于指定该 subcommand 的 args;
    • onetype struct field 分两种情况:
      1. 代表该命令参数 args,则对应 struct 类型必须用 #[derive(Args)] 来修饰;
      2. 代表子命令,这对应 onetype struct field 用 #[command(flatten)] 来修饰,表示它定义了一组子命令而非 args;

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

  1. 在 Parser/Args 中:field type 必须是实现 Args 的 struct 类型;
  2. 在 Subcommand 中: field type 必须是实现 Subcommand 的 enum 类型;

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

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

    /// struct args
    #[derive(Args, Debug)] // Args 必须修饰 struct,不需要对每个 field 添加 #[arg]
    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, Struct 必须实现 Args。
        /// cmd1, 支持 tuple 类型, 但则只支持 1 个类型的 newtype
        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), /* SubCmd 必须实现 Subcommand,而 Subcommand 必须是 enum 类型 */
    }

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

~ #[arg(value_enum)]~ : 修饰枚举类型的 field,同时对应 field type 也需要使用 #[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");
        }
    }
}

参考:

  1. https://github.com/mrjackwills/havn/blob/main/src/parse_arg.rs
  2. https://github.com/franticxx/dn/blob/main/src/cli/cli.rs

4 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 {
    // strip out usage
    const PARSER_TEMPLATE: &str = "\
        {all-args}
    ";

    // strip out name/version
    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);
}

5 示例
#

use clap::{Parser, Subcommand};
use logger::DummyLogger;
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
mod logger;

fn validate_package_name(name: &str) -> Result<String, String> {
    if name.trim().len() != name.len() {
        Err(String::from(
            "package name cannot have leading and trailing space",
        ))
    } else {
        Ok(name.to_string())
    }
}

#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about)]
/// A Very simple Package Hunter
struct Arguments {
    #[arg(default_value_t = usize::MAX, short, long)]
    /// maximum depth to which sub-directories should be explored
    max_depth: usize,
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbosity: u8,
    #[command(subcommand)]
    cmd: SubCommand,
}

#[derive(Subcommand, Debug)]
enum SubCommand {
    /// Count how many times the package is used
    Count {
        #[arg(value_parser = validate_package_name)]
        /// Name of the package to search
        package_name: String,
    },
    /// list all the projects
    Projects {
        #[arg(short, long, default_value_t = String::from("."), value_parser = validate_package_name)]
        /// directory to start exploring from
        start_path: String,
        #[arg(short, long, value_delimiter = ':')]
        /// paths to exclude when searching
        exclude: Vec<String>,
    },
}

/// Not the dracula
fn count(name: &str, max_depth: usize, logger: &logger::DummyLogger) -> std::io::Result<usize> {
    let mut count = 0;
    logger.debug("Initializing queue");
    // queue to store next dirs to explore
    let mut queue = VecDeque::new();
    logger.debug("Adding current dir to queue");
    // start with current dir
    queue.push_back((PathBuf::from("."), 0));
    logger.extra("starting");
    loop {
        if queue.is_empty() {
            logger.extra("queue empty");
            break;
        }
        let (path, crr_depth) = queue.pop_back().unwrap();
        logger.debug(format!("path :{:?}, depth :{}", path, crr_depth));
        if crr_depth > max_depth {
            continue;
        }
        logger.extra(format!("exploring {:?}", path));
        for dir in fs::read_dir(path)? {
            let dir = dir?;
            // we are concerned only if it is a directory
            if dir.file_type()?.is_dir() {
                if dir.file_name() == name {
                    logger.log(format!("match found at {:?}", dir.path()));
                    // we have a match, so stop exploring further
                    count += 1;
                } else {
                    logger.debug(format!("adding {:?} to queue", dir.path()));
                    // not a match so check its sub-dirs
                    queue.push_back((dir.path(), crr_depth + 1));
                }
            }
        }
    }
    logger.extra("search completed");
    return Ok(count);
}

fn projects(
    start: &str,
    max_depth: usize,
    exclude: &[String],
    logger: &DummyLogger,
) -> std::io::Result<()> {
    logger.debug("Initializing queue");
    // queue to store next dirs to explore
    let mut queue = VecDeque::new();
    logger.debug("Adding start dir to queue");
    // start with current dir
    queue.push_back((PathBuf::from(start), 0));
    logger.extra("starting");
    loop {
        if queue.is_empty() {
            logger.extra("queue empty");
            break;
        }
        let (path, crr_depth) = queue.pop_back().unwrap();
        logger.debug(format!("path :{:?}, depth :{}", path, crr_depth));
        if crr_depth > max_depth {
            continue;
        }
        logger.extra(format!("exploring {:?}", path));
        // we label the loop so we can continue it from inner loop
        'outer: for dir in fs::read_dir(path)? {
            let dir = dir?;
            let _path = dir.path();
            let temp_path = _path.to_string_lossy();
            for p in exclude {
                if temp_path.contains(p) {
                    // this specifies that it should continue the 'outer loop
                    // not the for p in exclude loop
                    // I originally had bug where I just used continue, and was wondering why
                    // the projects weren't getting filtered!
                    continue 'outer;
                }
            }
            // we are concerned only if it is a directory
            if dir.file_type()?.is_dir() {
                if dir.file_name() == ".git" {
                    logger.log(format!("project found at {:?}", dir.path()));
                    // we have a match, so stop exploring further
                    println!("{:?}", dir.path());
                } else {
                    logger.debug(format!("adding {:?} to queue", dir.path()));
                    // not a match so check its sub-dirs
                    queue.push_back((dir.path(), crr_depth + 1));
                }
            }
        }
    }
    logger.extra("search completed");
    return Ok(());
}

fn main() {
    let args = Arguments::parse();
    let logger = logger::DummyLogger::new(args.verbosity as usize);
    match args.cmd {
        SubCommand::Count { package_name } => match count(&package_name, args.max_depth, &logger) {
            Ok(c) => println!("{} uses found", c),
            Err(e) => eprintln!("error in processing : {}", e),
        },
        SubCommand::Projects {
            start_path,
            exclude,
        } => match projects(&start_path, args.max_depth, &exclude, &logger) {
            Ok(_) => {}
            Err(e) => eprintln!("error in processing : {}", e),
        },
    }
}
rust crate - 这篇文章属于一个选集。
§ 12: 本文

相关文章

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