赞
踩
本文将从 CLI(Command Line Interface)命令行工具的概述讲起,介绍一个优秀的命令行工具应该具备的功能和特性。然后介绍 Rust 中一个非常优秀的命令行解析工具 clap
经典使用方法,并利用 clap
实现一个类似于 curl
的工具 httpie
。文章最后还将 clap
于 Go 语言中同样优秀的命令行解析工具 cobra
进行一个简单对比,便于读者进一步体会 clap
的简洁和优秀。
本文将包含以下几个部分:
特此声明,本文包含 AI 辅助生成内容,如有错误遗漏之处,敬请指出。
CLI(Command Line Interface,命令行界面)是一种允许用户通过文本命令与计算机程序或操作系统进行交互的接口。与图形用户界面(GUI,Graphical User Interface)相比,CLI 不提供图形元素,如按钮或图标,而是依赖于文本输入。用户通过键盘输入特定的命令行指令,命令行界面解释这些指令并执行相应的操作。
一款优秀的 CLI 工具应该具备以下的功能和特性,以提升用户体验和效率:
一个优秀的命令行工具(CLI, Command Line Interface)应该具备以下功能和特性,以提升用户体验和效率:
--help
或-h
参数轻松访问帮助信息。这些特性不仅能够提高用户的工作效率,还能增强用户体验,使命令行工具更加强大和易用。
下面我们以 curl
为例,看看优秀的 CLI 工具大概长什么样子。
curl
是一种命令行工具和库,用于传输数据。它支持多种协议,包括 HTTP、HTTPS、FTP、FTPS、SCP、SFTP、TFTP、TELNET、DICT、LDAP、LDAPS、IMAP、POP3、SMTP 和 RTSP 等。curl
是一个非常强大和灵活的工具,广泛应用于自动化脚本、系统测试、数据收集和许多其他用途。
进入终端,我们可以用下面命令查看 curl
的说明文档:
➜ ~ curl --help Usage: curl [options...] <url> -d, --data <data> HTTP POST data -f, --fail Fail fast with no output on HTTP errors -h, --help <category> Get help for commands -i, --include Include protocol response headers in the output -o, --output <file> Write to file instead of stdout -O, --remote-name Write output to a file named as the remote file -s, --silent Silent mode -T, --upload-file <file> Transfer local FILE to destination -u, --user <user:password> Server user and password -A, --user-agent <name> Send User-Agent <name> to server -v, --verbose Make the operation more talkative -V, --version Show version number and quit This is not the full help, this menu is stripped into categories. Use "--help category" to get an overview of all categories. For all options use the manual or "--help all".
使用示例:
curl -O http://example.com/file.txt
curl -d "param1=value1¶m2=value2" http://example.com/resource
curl -k https://example.com
curl -u username:password http://example.com
curl
的这些特性使其成为开发者、系统管理员和自动化脚本中广泛使用的工具之一。
clap
,代表 Command Line Argument Parser,是一个旨在创建直观、易用且功能强大的命令行界面的 Rust 库。截至目前(2024.2),clap
已经发展到了 4.5.1 版本,它通过简化命令行参数的处理,让开发者能更专注于应用逻辑的构建。
clap
之所以在 Rust 社区如此流行,得益于以下几个优点:
1. 易于使用
clap
的设计理念是让命令行参数的解析变得简单而直观。即使是没有经验的开发者也能快速上手,通过几行代码就能实现复杂的命令行参数解析。
2. 功能丰富
clap
提供了广泛的功能来满足各种命令行解析需求,包括但不限于:
clap
能根据定义的参数自动生成帮助信息,包括参数的说明、类型、默认值等。clap
会提供清晰且有用的错误提示,帮助用户快速定位问题。clap
的派生宏,可以简化命令行解析器的定义,使代码更加清晰。3. 高度可定制
clap
允许开发者高度定制命令行解析的行为和外观,包括自定义帮助信息的格式、控制错误消息的显示方式等。这种灵活性意味着你可以根据应用程序的需求调整 clap
的行为。
4. 性能优异
尽管 clap
功能强大,但它仍然非常注重性能。clap
经过优化,以尽可能少的性能开销处理命令行参数。
5. 活跃的社区支持
clap
有一个非常活跃的社区,在 GitHub 上不断有新的贡献者加入。这意味着 clap
不断地得到改进和更新,同时也有大量的社区资源可供参考。
clap
提供了 2 种构建命令行的方式,分别为 Derive
和 Builder
。顾名思义,Derive
就是利用宏强大的功能来构建命令行,而 Builder
则采用构建者模式链式构建命令行工具。
在这里我们先给出示例来直观感受这 2 种构建方式的不同:
Derive:
#[derive(Parser)] #[command(version, author, about, long_about = None)] struct Cli { /// Specify your name name: String, /// Specify your age optionally #[arg(short, long)] age: Option<i8>, } fn main() { let cli = Cli::parse(); println!("name: {}", cli.name); println!("age: {:?}", cli.age); }
Builder:
fn main() {
let matches = Command::new("myapp")
.version("1.0.0")
.author("hedon")
.about("this is the short about")
.long_about("this is the long about")
.arg(arg!([NAME]).required(true).help("Specify your name"))
.arg(arg!(-a --age <AGE>)
.value_parser(clap::value_parser!(u8))
.help("Specify your age optionally"))
.get_matches();
println!("name: {:?}", matches.get_one::<String>("NAME"));
println!("age: {:?}", matches.get_one::<u8>("age"));
}
这 2 个程序都实现了相同的功能,使用 --help
,输出的内容大致都如下:
Usage: derive [OPTIONS] <NAME>
Arguments:
<NAME> Specify your name
Options:
-a, --age <AGE> Specify your age optionally
-h, --help Print help
通过观察,可以发现 Derive 模式下,宏中的每一个属性,如 version
、author
等,都对应到 Builder 模式下一个同名的函数。
下面我们将从**「应用配置」、「参数类型」和「参数校验」**三个方面,分别介绍 clap
中 Derive 和 Builder 两种模式构建 CLI 的常用方法。
特别说明:后续的例子均在 examples
目录下实现,故编译和执行命令都包含 example。
目录结构大概如下:
➜ learn-clap git:(master) ✗ tree
.
├── Cargo.lock
├── Cargo.toml
├── examples
│ ├── optional.rs
├── src
│ └── main.rs
└── target
└── release
└── examples
└── optional
要使用 clap
的 Derive 模式,需要:
cargo add clap --features derive
我们需要定义一个 strut
来表示我们的 application
,利用它来承载应用的参数:
/// The example of clap derive #[derive(Parser)] #[command(version, author, about, long_about = None)] struct Cli { /// Specify your name name: String, /// Specify your age optionally #[arg(short, long)] age: Option<i8>, } fn main() { let cli = Cli::parse(); println!("name: {}", cli.name); println!("age: {:?}", cli.age); }
#[derive(Parser)]
是一个过程宏(procedural macro),用于自动为结构体实现 clap::Parser
trait。这使得该结构体可以用来解析命令行参数。
#[derive(Parser)]
,你可以简化命令行解析的代码,因为 clap
会根据结构体的字段自动生成命令行解析的逻辑。#[command(version, about, long_about = None)]
属性用于为整个命令行程序提供元信息,它支持以下几个元素:
#[arg(short, long)]
属性用于配置命令参数的元信息,它支持以下几个属性:
属性 | 方法 | 默认值/行为 | 备注 |
---|---|---|---|
id | Arg::id | field’s name | 当属性不存在时,使用字段名 |
value_parser | Arg::value_parser | auto-select based on field type | 当属性不存在时,会基于字段类型自动选择实现 |
action | Arg::action | auto-select based on field type | 当属性不存在时,会基于字段类型自动选择动作 |
help | Arg::help | Doc comment summary | 当属性不存在时,使用文档注释摘要 |
long_help | Arg::long_help | Doc comment with blank line, else nothing | 当属性不存在时,使用文档注释,如果有空行 |
verbatim_doc_comment | Minimizes preprocessing | - | 将文档注释转换为 help/long_help 时最小化预处理 |
short | Arg::short | no short set | 当属性不存在时,没有短名称设置 |
long | Arg::long | no long set | 当属性不存在时,没有长名称设置 |
env | Arg::env | no env set | 当属性不存在时,没有环境变量设置 |
from_global | Read Arg::global | - | 无论在哪个子命令中,都读取 Arg::global 参数 |
value_enum | Parse with ValueEnum | - | 使用 ValueEnum 解析值 |
skip | Ignore this field | fills the field with Default::default() | 忽略此字段,用 <expr> 或 Default::default() 填充 |
default_value | Arg::default_value | Arg::required(false) | 设置默认值,并将 Arg 设置为非必须 |
default_value_t | Arg::default_value | Arg::required(false) | 要求 std::fmt::Display 与 Arg::value_parser 相匹配 |
default_values_t | Arg::default_values | Arg::required(false) | 要求字段类型为 Vec<T>,T 实现 std::fmt::Display |
default_value_os_t | Arg::default_value_os | Arg::required(false) | 要求 std::convert::Into<OsString> |
default_values_os_t | Arg::default_values_os | Arg::required(false) | 要求字段类型为 Vec<T>,T 实现std::convert::Into<OsString> |
从上面这个输出样例中:
the example of clap derive
Usage: derive [OPTIONS] <NAME>
Arguments:
<NAME> Specify your name
Options:
-a, --age <AGE> Specify your age optionally
-h, --help Print help
可以看到跟在命令后面有 2 中参数类型:
cmd hedon
,有严格的顺序要求。-{short}
或 --{long}
来指定是哪个参数,无严格的顺序要求。它们的定义区别就是是否使用了 #[arg]
:
#[derive(Parser)]
struct Cli {
/// 会被解析成 [NAME]
name: String,
/// 会被解析成 -a <AGE>
#[arg(short, long)]
age: u8,
}
可以使用 Option
来实现可选参数:
use clap::Parser; #[derive(Parser)] #[command(version, author, about, long_about = None)] struct Cli { name: Option<String>, #[arg(short, long)] age: Option<u8>, } fn main() { let cli = Cli::parse(); println!("name: {:?}", cli.name); println!("age: {:?}", cli.age); }
编译:
cargo build --example optional --release
执行:
/target/release/examples/optional --help
输出:
this is the about from Cargo.toml
Usage: optional [OPTIONS] [NAME]
Arguments:
[NAME]
Options:
-a, --age <AGE>
-h, --help Print help
-V, --version Print version
测试:
➜ learn-clap git:(master) ✗ ./target/release/examples/optional
name: None
age: None
➜ learn-clap git:(master) ✗ ./target/release/examples/optional -a 1
name: None
age: Some(1)
➜ learn-clap git:(master) ✗ ./target/release/examples/optional hedon
name: Some("hedon")
age: None
➜ learn-clap git:(master) ✗ ./target/release/examples/optional hedon -a 18
name: Some("hedon")
age: Some(18)
可以使用 enum
搭配 value_enum
来实现多选一参数,并限制可选参数的取值。
use clap::{Parser, ValueEnum}; #[derive(Parser)] #[command(version, author, about, long_about = None)] struct Cli { /// Choose the program mode run in #[arg(value_enum)] mode: Mode, } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] enum Mode { /// run in fast mode Fast, /// run in slow mode Slow, } fn main() { let cli = Cli::parse(); match cli.mode { Mode::Fast => println!("fast!!!!!"), Mode::Slow => println!("slow......"), } }
输出:
Usage: enum <MODE> Arguments: <MODE> Choose the program mode run in Possible values: - fast: run in fast mode - slow: run in slow mode Options: -h, --help Print help (see a summary with '-h') -V, --version Print version
累积参数允许用户通过重复指定同一个标志(例如 -d
)来累加值或效果,通常用于控制命令行应用的详细级别(verbosity level)或其他需要根据次数变化的行为。
在很多命令行工具中,累积参数常见于控制日志输出的详细程度。例如,一个 -v
(verbose)标志可能每被指定一次,就增加一层详细级别。所以,-vvv
(等价于 -v -v -v
) 会比单个 -v
提供更多的详细信息。
在 clap
中可以通过 clap::ArgAction::Count
来实现这种累积参数。
use clap::Parser;
#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
}
fn main() {
let cli = Cli::parse();
println!("verbose: {}", cli.verbose);
}
输出:
➜ learn-clap git:(master) ✗ ./target/release/examples/accurate --help
this is the about from Cargo.toml
Usage: accurate [OPTIONS]
Options:
-v, --verbose...
-h, --help Print help
-V, --version Print version
➜ learn-clap git:(master) ✗ ./target/release/examples/accurate -v
verbose: 1
➜ learn-clap git:(master) ✗ ./target/release/examples/accurate -vvvv
verbose: 4
有时候我们希望接收变长参数,比如说:
del file1 file2 file3
这个时候可以使用 Vec<>
来实现。
use clap::Parser;
#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
files: Vec<String>,
}
fn main() {
let cli = Cli::parse();
println!("files: {:?}", cli.files);
}
输出:
➜ learn-clap git:(master) ✗ ./target/release/examples/var_length --help this is the about from Cargo.toml Usage: var_length [FILES]... Arguments: [FILES]... Options: -h, --help Print help -V, --version Print version ➜ learn-clap git:(master) ✗ ./target/release/examples/var_length files: [] ➜ learn-clap git:(master) ✗ ./target/release/examples/var_length file1 files: ["file1"] ➜ learn-clap git:(master) ✗ ./target/release/examples/var_length file1 file2 files: ["file1", "file2"] ➜ learn-clap git:(master) ✗ ./target/release/examples/var_length file1 file2 file3 files: ["file1", "file2", "file3"]
对于标志参数,只要指定类型为 bool
,就可以自动实现了。
use clap::Parser;
#[derive(Parser)]
#[command(version, author, about, long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
}
fn main() {
let cli = Cli::parse();
println!("verbose: {}", cli.verbose);
}
输出:
➜ learn-clap git:(master) ✗ ./target/release/examples/flag --help
Usage: flag [OPTIONS]
Options:
-v, --verbose
-h, --help Print help
-V, --version Print version
➜ learn-clap git:(master) ✗ ./target/release/examples/flag
verbose: false
➜ learn-clap git:(master) ✗ ./target/release/examples/flag -v
verbose: true
在更复杂的命令行工具中,除了主命令,还有子命令,甚至子命令下面还有子命令,其实就是一颗命令树。
在 clap
中可以使用 #[command(subcommand)]
搭配 #[derive(Subcommand)]
实现子命令功能。
use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(version, author, about, long_about = None)] struct Cli { #[command(subcommand)] test: Option<Test>, } #[derive(Subcommand)] enum Test { /// Add a number Add { #[arg(short, long)] num: u16, }, /// Sub a number Sub { #[arg(short, long)] num: u16, } } fn main() { let cli = Cli::parse(); if let Some(test) = cli.test { match test { Test::Add {num} => println!("test add num: {:?}", num), Test::Sub {num} => println!("test sub num: {:?}", num), } } }
输出:
➜ learn-clap git:(master) ✗ ./target/release/examples/subcommand --help this is the about from Cargo.toml Usage: subcommand [COMMAND] Commands: add Add a number sub Sub a number help Print this message or the help of the given subcommand(s) Options: -h, --help Print help -V, --version Print version ➜ learn-clap git:(master) ✗ ./target/release/examples/subcommand add --help Add a number Usage: subcommand add --num <NUM> Options: -n, --num <NUM> -h, --help Print help ➜ learn-clap git:(master) ✗ ./target/release/examples/subcommand add -n 1 test add num: 1 ➜ learn-clap git:(master) ✗ ./target/release/examples/subcommand sub -n 2 test sub num: 2
可以发现,使用 Derive 模式的时候,我们在参数后面指定参数类型的时候,clap
就会对我们输入参数进行类型检查,不匹配的时候会输出丰富的报错信息和指导建议。
error: invalid value 'xxxx' for '--num <NUM>': invalid digit found in string
For more information, try '--help'.
默认支持:
bool
, String
, OsString
, PathBuf
、usize
、isize
u8
, i8
, u16
, i16
, u32
, i32
, u64
, i64
ValueEnum
的 enum 类型From<OsString>
、From<&OsStr>
、FromStr
的类型这是因为他们都实现了 TypedValueParser
trait,你自定义的类型也可以实现这个 triat,这样就可以自动进行类型校验了。
clap
还提供了一些更加严格的参数校验功能。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。