两种使用方式:
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);
}
ValueParser #
ValueParser 是 Arg::value_parser()
的参数类型,用于解析和校验参数。
两种创建方式:
value_parser!()
:自动根据类型选择对应的实现, 参数是实现ValueParserFactory trait
的类型名称;ValueParser::new()
:使用实现TypedValueParser trait
的类型来创建ValueParser
对象:
- pub fn new
(other: P) -> ValueParser where P: TypedValueParser
- clap::builder module 提供了一些实现 TypedValueParser trait 的类型, 如 BoolValueParser 等;
value_parser!() #
value_parser!()
宏的参数是实现 clap::builder::ValueParserFactory trait
的类型值:
- 各种 Rust 内置类型实现了
ValueParserFactory
:- Native types: bool, String, OsString, PathBuf
- Ranged numeric types: 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 {
// 关联类型 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
// 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>,
示例:
// 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);
TypedValuePrarser #
TypedValueParser trait
用于为类型实现自定义解析,它实现了 Into<ValueParser>
,可以作为 Arg::value_parser()
的参数:
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 module 提供了如下实现类型:
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")
// 使用实现 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");
// 使用宏来创建 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,返回的 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::ValueParserFactory
和 clap::builder::TypedValueParser
可以为自定义类型实现参数解析:
#[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))
}
}
// 使用自定义 ValueParser 类型
let parser: CustomValueParser = clap::value_parser!(Custom);
Arg::value_parser() #
Arg::value_parser()
方法用于为 Arg 指定解析参数值的方式。如果未指定,默认解析后的类型是 String
。
clap 为 Into<ValueParser>
Option<ValueParser>
实现了 IntoResettable<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 trait 类型值包括:
value_parser!(T)
:根据 T 的类型自动选择合适的实现。RangeXX
,如 0..=1 代表 RangedI64ValueParser;[P; C]
和Vec<P>
,其中 P 需要实现 Into,一般为 String/&str 等; [&str] 和 PossibleValuesParser
:静态枚举类型;BoolishValueParser 和 FalseyValueParser
:布尔值语义NonEmptyStringValueParser
:字符串基本校验- 任何实现了
TypedValueParser
的类型, 如上面的 BoolValueParser 等,以及Fn(&str) -> Result<T, E>
闭包; - Option
;
// 从实现 TypedValueParser 的自定义类型值创建 ValueParser
pub fn new<P>(other: P) -> ValueParser where P: TypedValueParser
// 从 [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
上面的 PossibleValue
和 PossibleValuesParser
类型如下:
- 任何可以
Into<Str>
的类型都可以转成PossibleValue
; - 任何可以迭代生成
PossibleValue
的类型都可以转成PossibleValuesParser
; 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");
builder #
builder 模式使用如下 struct 类型:
- Command
- Arg
- ArgGroup
- 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
: 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();
// 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
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...");
}
}
Command #
get_matches_from()
从指定的字符串列表解析命令行参数,列表中第一个元素为命令名称。
let m = cmd.clone().get_matches_from(vec!["prog", "-F", "in-file", "out-file"]);
Arg #
Arg 默认特性:
- 默认为位置参数,设置 short/long 后成为 flag 参数;
- 未调用 value_parser() 指定 ValueParser 时,默认为 StringValueParser,所以默认解析值为 String;
- 未调用 action() 时默认为 ArgAction::Set,对于 Vec 需要指定为 action(ArgAction::Append));
- 参数默认是必须的,需要调用 .required(false) 方法来设置为可选;
// 使用 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 cmd = Command::new().arg(
Arg::new("in_file")
)
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))
// 为 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") // 未调用 short 或 long 方法,所以为位置参数
.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");
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)
);
可选值类型 Arg:
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)
]);
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;
// derive 风格:
#[derive(Parser, Debug)]
struct RemoveArgs {
// flag 参数
#[arg(short, long)]
force: bool,
// 未加 #[arg],所以为位置参数。
// name 是可选的位置参数,未指定时为空 Vec
name: Vec<String>,
}
// builder 风格
let cmd = Command::new("mycmd")
.arg(
Arg::new("flag")
.long("flag") // 调用 short/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"]
);
// num_args 指定参数值的个数
// 这里表示 -F 有两个参数值,如 -F in-file out-file
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))
, 如果不符合预期, 则需要为自定义类型实现 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`
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
。- 使用
#[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
来表明可选; - default_value_t 设置缺省值表达式 expr;
- 如果未指定 expr,则类型需要实现 Default trait。
- 指定该 attr 时,该 flag 是可选的(默认如果不是 Option 类型,则是必选的 flag)。
- Vec
: 可以指定多次 flag, 各参数值被 Append 到 Vec 中, #[arg(value_delimiter = ':')]
表示使用:分割多个字符串(默认空格)
- 参数默认是必选的,使用 Option
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,
// #[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,
// 位置参数:Option<T> 表示是可选的。
name: Option<String>,
// flag 参数:参数值用 : 分割,结果都合并到一个 Vec 中(默认空格分隔)
#[arg(short, long, value_delimiter = ':')]
name2: Vec<String>,
#[arg(value_parser = validate_package_name)] // 使用自定义解析函数
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());
}
在 Parser 的 struct 中,可以:
- 使用
#[command(subcommand)]
来修饰 enum 类型,用来指定子命令。然后该 enum 类型也必须用#[derive(Subcommand)]
来修饰; - 使用
#[command(flatten)]
来修饰 struct 类型,用来将它的 field 打平到当前 Command 的参数中,该 struct 类型必须使用#[derive(Args)]
来修饰;
#[derive(Subcommand)]
修饰的 enum 类型:
- 每一个 variant 都是一个 subcommand;
- variant 类型可以是 struct 或 onetype 类型 struct,它们的 field 作为该 subcommand 的 args:
- onetype struct field 分两种情况:
- 代表该命令参数 args,则对应 struct 类型必须用
#[derive(Args)]
来修饰; - 代表子命令,则对应 onetype struct field 用
#[command(flatten)]
来修饰,表示它定义了一组子命令而非 args;
- 代表该命令参数 args,则对应 struct 类型必须用
- onetype struct field 分两种情况:
#[command(flatten)]
: 两种使用场景:
- 在 Parser/Args 中:field type 必须是实现 Args 的 struct 类型,用于将指定的 field 作为 Args 打平到当前命令中;
- 在 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,
// 必须修饰 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
#[arg(value_enum)]
: 修饰枚举类型的 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");
}
}
}
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);
}
示例 #
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),
},
}
}