Rust命令行库Clap快速入门教程
几乎所有编程语言都是支持命令行库, Rust自然也不例外, 不过Rust标准库不支持,而是第三方库支持,比较常用和主流的是Clap
这个库, 通过它可以很简单的组建自己的命令行工具,这样就不用花太多时间放在参数处理上了。
命令行工具的接口一般比较简单, 参数无非两个部分, 可选参数
, 位置参数
, 以下面的命令为例。
每门编程语言有自己的想法, 库作者也一样, 不同的语言对于我所说的两个参数有不同的名字, 比如Option, Argument, Flags等, 这里仅以中文名作为统一说法。
ls -r /var
ls --reverse /var
上面两条命令的结果是一样的, 这条命令包括三个部分, 命令行工具本体, 可选参数
,位置参数
。
其中本体自然是ls
, 可选参数是-r
或--reverse
, 位置参数是/var
。
本文依赖如下
[dependencies]
clap = { version = "4.4.13", features = ["derive"] }
快速入门
Clap
支持两种方式构建命令行工具, 一种是使用衍生宏, 一种是使用Builder
模式(即不断的调用方法设置参数), 本文只演示第一种, 作者也推荐这一种。
use clap::Parser;
#[derive(Parser)]
#[command(name = "youerning")]
#[command(author = "youerning.top")]
#[command(version = "1.0")]
#[command(about = "a tutorial of crate clap", long_about = None)]
struct Cli {
// 注意下面的注释是三个斜杠!!!
/// use which method
#[arg(short, long)]
method: Option<String>,
/// Optional name to call
name: Option<String>,
}
fn main() {
let cli = Cli::parse();
let method = match cli.method {
Some(method) => method,
None => "hello".to_owned(),
};
let name = match cli.name {
Some(name) => name,
None => "world".to_owned(),
};
println!("{method} {name}");
}
上面的代码没有参数时输出如下:
hello world
使用参数时如下
quickstart -m wtf Tom
wtf Tom
使用--help
时输出如下
a tutorial of crate clap
Usage: quickstart.exe [OPTIONS] [NAME]
Arguments:
[NAME] Optional name to call
Options:
-m, --method <METHOD> use which method
-h, --help Print help
-V, --version Print version
可以发现, Clap
为我们添加了必要的参数和说明, 这符合预期, 没有什么奇怪的。
元数据
一般来说命令行会带有一些元数据, 比如使用的说明文档,命令行的版本等,下面是Clap
支持的一些参数。
#[command(name = "youerning")]
#[command(author = "youerning.top")]
#[command(version = "1.0")]
#[command(about = "a tutorial of crate clap", long_about = None)]
struct Cli {
// 省略。。。。
}
上面分别是名字, 作者, 版本, 命令行说明等参数。
可选参数
可选参数
一般有缩写和全写两种形式, 前者使用单横杠-
, 比如-r
, 后者使用双横杠--
, 比如--reverse
, 两者作用是一样的, 只是调用命令时的方式不一样而已。
值得注意的是,这里所说的约定是linux平台的约定, 而约定总有例外,比如golang标准库的单横杠就支持全写, 比如-reverse
, 实在是异类,所以我喜欢用golang的第三方库cobra
。
为啥可选参数有简短好用的缩写还要全写呢? 因为缩写一般只有一个字符,而因为只有26个字符呀,只用缩写不够用,而且可能会冲突。
默认情况下结构体的字段都是位置参数,想要将其转换成可选参数需要在前面设置一个宏属性,比如#[arg(long, short)]
值得注意的是: 可以只选long或者short
use clap::Parser;
#[derive(Parser)]
struct Cli {
// 注意下面的注释是三个斜杠!!!
/// options1
#[arg(long, short='x')]
option1: String,
/// options2
#[arg(long, short)]
option2: Option<String>,
/// options3
#[arg(long="option", short='y', default_value="option3")]
option3: String,
}
fn main() {
let cli = Cli::parse();
let option1 = cli.option1;
println!("options {option1}");
let option2 = match cli.option2 {
Some(option) => option,
None => "option2".to_owned(),
};
let option3 = cli.option3;
println!("option1: '{option1}'");
println!("option2: '{option2}'");
println!("option3: '{option3}'");
}
上面的代码帮助文档如下
Usage: option.exe [OPTIONS] --option1 <OPTION1>
Options:
-x, --option1 <OPTION1> options1
-o, --option2 <OPTION2> options2
-y, --option <OPTION3> options3 [default: option3]
-h, --help Print help
其中第一个参数是强制的,因为它没有默认值也没有用Option枚举类型包裹, 第二个参数是可选的, 因为它被Option枚举类型包裹, 第三个参数是可选的,因为它有默认参数。
除此之外,因为三个参数的第一个字母都是o
(缩写默认使用第一个字母), 所以会冲突,那么就需要额外指定一个不冲突的名字,比如这里的x
和y
, 注意要使用单引号''
包裹, 而全写可选参数需要用双引号""
包裹。
位置参数
位置参数能做的不多,也不需要额外的配置,Clap
会根据参数类型来设置参数。
use clap::Parser;
#[derive(Parser)]
struct Cli {
// 注意下面的注释是三个斜杠!!!
/// argument of name
name: String,
/// argument of names
names: Vec<String>,
}
fn main() {
let cli = Cli::parse();
let name = cli.name;
let names = cli.names;
println!("name: {name}");
println!("names: {names:?}");
}
--help
的帮助信息如下
Usage: argument.exe <NAME> [NAMES]...
Arguments:
<NAME> argument of name
[NAMES]... argument of names
Options:
-h, --help Print help
可以使用以下命令调用
argument.exe youerning name1 name2 name3
# 输出如下
name: youerning
names: ["name1", "name2", "name3"]
参数的位置根据从上到下的顺序指定,值得注意的是,不能在位置参数中设置两个接受列表的值,比如下面这样。
struct Cli {
// 注意下面的注释是三个斜杠!!!
/// argument of name
name: String,
/// argument of names
names: Vec<String>,
names2: Vec<String>,
}
因为两个列表的话会产生歧义,前面的列表应该包含哪部分,后面的列表应该包含哪部分?
参数验证
命令行解析有许多常见的验证模式,自己写来肯定是很讨厌的,所以Clap
提供了一系列的参数验证设置,完整列表可以参考:https://docs.rs/clap/latest/clap/_derive/_tutorial/chapter_3/index.html,它大致支持以下四种类型
- 枚举值验证
- 参数值验证
- 参数关联
- 自定义验证
枚举值验证
use clap::{Parser, ValueEnum};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
/// What mode to run the program in
#[arg(value_enum)]
mode: Mode,
/// options of mode
#[arg(value_enum, short, long)]
mode2: Option<Mode>,
}
// 注意要配置以下衍生宏
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
Fast,
Slow,
}
fn main() {
let cli = Cli::parse();
match cli.mode {
Mode::Fast => {
println!("you are fast");
}
Mode::Slow => {
println!("you are slow");
}
}
match cli.mode2 {
Some(mode) => {
match mode {
Mode::Fast => {
println!("you are fast");
}
Mode::Slow => {
println!("you are slow");
}
}
}
None => {
println!("no mode2")
}
}
}
--help
的帮助信息如下
Usage: validate1.exe --mode2 <MODE2> <MODE>
Arguments:
<MODE> What mode to run the program in [possible values: fast, slow]
Options:
-m, --mode2 <MODE2> options of mode [possible values: fast, slow]
-h, --help Print help
-V, --version Print version
枚举验证和一般参数或者位置参数的主要区别是要配置#[arg(value_enum)]
以说明使用的是枚举模式, 当然也可以添加long
, short
等将其转换成可选参数
。
参数值验证
常见的就是数字类型的范围了。
use clap::Parser;
#[derive(Parser)]
struct Cli {
/// Network port to use
#[arg(value_parser = clap::value_parser!(u16).range(1..))]
port: u16,
}
fn main() {
let cli = Cli::parse();
println!("PORT = {}", cli.port);
}
上面只指定了从1开始,为啥不指定结束范围呢? 因为u16最大只支持65535, 所以更大的值会报错,也就无需额外的指定了。
参数关联
这部分主要是配置关联的参数,比如参数1和参数2互斥,或者参数1依赖参数2,这个一般用的不多,这里就不展示了,可以直接看官方文档。
自定义验证
这里也略过了。。。。
子命令
下面的示例一共有两个子命令,add
和remove
use clap::{Parser, Subcommand};
#[derive(Parser)]
struct Cli {
#[arg(short, long, default_value="0")]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Adds files to myapp
Add { name: Option<String> },
/// Remove files from myapp
Remove {
names: Vec<String>,
#[arg(short, long, default_value="false")]
force: bool
},
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Commands::Add { name } => {
if name.is_none() {
println!("'请选择你要自家的文件名")
} else {
println!("'你要增加的文件名是: {name:?}")
}
},
Commands::Remove {
names ,
force,
} => {
println!("你要删除的文件名有: {names:?}, 强制执行么? {force}")
},
}
}
没有指定子命令的--help
帮助信息如下
Usage: subcommand.exe [OPTIONS] <COMMAND>
Commands:
add Adds files to myapp
remove Remove files from myapp
help Print this message or the help of the given subcommand(s)
Options:
-v, --verbose <VERBOSE> [default: 0]
-h, --help Print help
remove
子命令的帮助信息如下
Remove files from myapp
Usage: subcommand.exe remove [OPTIONS] [NAMES]...
Arguments:
[NAMES]...
Options:
-f, --force
-h, --help Print help
测试
官方的这部分写的有点奇怪,大家可以参考以下代码。
use clap::Parser;
#[derive(Parser)]
#[command(about = "扫描目标的指定端口", long_about = None)]
struct Cli {
/// the target need to be connected
target: String,
/// timeout in millisecond
#[arg(long, short, default_value="80")]
port: u16,
}
fn main() {
let cli = Cli::parse();
let target = cli.target;
let port = cli.port;
println!("尝试扫描目标[{target}]的端口[{port}]")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn should_panic() {
// 因为没有指定target
let _ = Cli::try_parse_from(["test"]).unwrap();
}
#[test]
#[should_panic]
fn should_panic2() {
// 因为端口号大于65535
let _ = Cli::try_parse_from(["test", "-p", "655361"]).unwrap();
}
#[test]
fn test_right_args() {
let cli = Cli::try_parse_from(["test", "baidu.com"]).unwrap();
assert_eq!(cli.target, "baidu.com");
assert_eq!(cli.port, 80);
let cli = Cli::try_parse_from(["test", "baidu.com", "--port", "443"]).unwrap();
assert_eq!(cli.target, "baidu.com");
assert_eq!(cli.port, 443);
}
}
测试一般有测两个方向,正确的要测,错误的也要测,不然的话可能出现未知错误,比如错误的参数也正确执行了。
总结
每门编程语言有自己的习惯和语法,所以配置方式会有所不同,但是大致方向是差不多的,比如可选参数,位置参数两个类型的参数,然后按照这个框架去学习一般能很快的掌握的。
之前还写过Python的Typer和Golang的Cobra库,现在钟情于Rust, 所以总算写了对应的文章, 之前关于Python和Golang对应的命令行库有兴趣的可以看看。