Yashodhan Joshi 2024-07-12
CLI 应用程序是一种用户通过终端中的文本命令与其交互的程序。例如 cargo run 就是一个通过 Cargo 引导的 Rust 应用程序。
在本文中,我们将看到如何在 Rust 应用程序中手动解析命令行参数,为什么对于大型应用来说手动解析可能不是一个好选择,以及 Clap 库如何帮助解决这些问题。
注意:你应该能够舒适地阅读和编写基本的 Rust 代码,例如变量声明、if...else 块、循环和结构体。
设置一个示例 Rust 应用程序
假设我们有一个包含许多基于 Node 的项目的文件夹,我们想找出我们使用了哪些包(包括依赖项),以及它们被使用了多少次。毕竟,那合计超过 1GB 的 node_modules 不可能全是唯一的依赖项吧?😰
如果我们制作一个小程序来统计我们在项目中使用某个包的次数会怎么样?
为此,让我们用 cargo new package-hunter 在 Rust 中创建一个项目。src/main.rs 文件是默认的主函数,初始内容如下:
fn main() {
println!("Hello, world!");
}
我们将用我们的代码替换默认文件内容。首先,定义一个函数来获取用户传入的参数,并在 main 函数中调用它,如下所示:
fn get_arguments() {
let args: Vec<_> = std::env::args().collect(); // 获取传递给应用程序的所有参数
println!("{:?}", args);
}
fn main() {
get_arguments();
}
当我们运行上述代码时,会得到一个漂亮的输出,显示所有参数组成的数组,没有错误或 panic:
# '--' 之后的内容会传递给你的应用程序,而不是传递给 cargo
> cargo run -- svelte
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/package-hunter svelte`
["target/debug/package-hunter", "svelte"] //<- 参数
第一个参数是可执行文件的路径,第二个参数是传递给可执行文件的参数。
编写计数函数
现在我们已经为 CLI 设置了基础,可以愉快地编写用于统计依赖项的函数了。它将接收一个名称,并统计子目录中匹配该名称的目录数量。我们将使用标准库中的 VecDeque、fs 和 PathBuf 来处理目录结构:
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
/// Not the dracula
fn count(name: &str) -> std::io::Result<usize> {
let mut count = 0;
// 用于存储待探索目录的队列
let mut queue = VecDeque::new();
// 从当前目录开始
queue.push_back(PathBuf::from("."));
loop {
if queue.is_empty() {
break;
}
let path = queue.pop_back().unwrap();
for dir in fs::read_dir(path)? {
// for 循环变量 'dir' 实际上是一个 Result,因此我们在这里使用 ? 转换为实际的 dir 结构
let dir = dir?;
// 仅当它是目录时才考虑
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
// 找到匹配项,不再继续探索
count += 1;
} else {
// 不匹配,检查其子目录
queue.push_back(dir.path());
}
}
}
}
return Ok(count);
}
这是一个相当长的代码块。让我们分解一下。
在上面的代码中,我们导入了 VecDeque 用于高效的队列操作,fs 用于文件系统操作,PathBuf 用于处理文件系统路径。以下是相关代码:
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
对于当前路径中的每个目录条目,我们:
- 使用
?将目录条目结果转换为实际的目录条目 - 检查该条目是否为目录
- 如果目录名称与给定名称匹配,则增加计数
- 如果不匹配,则将该目录的路径添加到队列中,以便稍后探索其子目录
相关代码如下:
for dir in fs::read_dir(path)? {
let dir = dir?;
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
count += 1;
} else {
queue.push_back(dir.path());
}
}
}
我们将更新 get_arguments 函数以返回命令后的第一个参数:
fn get_arguments() -> String {
let args: Vec<_> = std::env::args().collect();
args[1].clone()
}
并在 main 中使用该参数调用 count:
fn main() {
let args = get_arguments();
match count(&args) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
当我们在其中一个项目文件夹内运行此程序时,它意外地完美工作并返回计数为 1,因为单个项目只会包含一次依赖项。
创建深度限制
现在,当我们向上移动一个目录并尝试运行它时,我们注意到一个问题:它需要更多时间,因为有更多的目录需要遍历。
理想情况下,我们希望从项目目录的根目录运行它,这样我们可以找到所有包含该依赖项的项目,但这将需要更多时间。
因此,我们决定妥协,只探索到某个特定深度的目录。如果目录的深度超过给定深度,它将被忽略。更新以下代码部分以支持深度:
/// Not the dracula
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
...
queue.push_back((PathBuf::from("."), 0));
...
let (path, crr_depth) = queue.pop_back().unwrap();
if crr_depth > max_depth {
continue;
}
...
// not a match so check its sub-dirs
queue.push_back((dir.path(), crr_depth + 1));
...
}
现在应用程序接受两个参数:首先是包名,然后是要探索的最大深度。
然而,我们希望深度是一个可选参数,如果没有给出,它将探索所有子目录,否则将在给定深度停止。
为此,我们可以更新 get_arguments 函数,使第二个参数可选:
fn get_arguments() {
let args: Vec<_> = std::env::args().collect();
let mdepth = if args.len() > 2 {
args[2].parse().unwrap()
} else {
usize::MAX
};
println!("{:?}", count(&args[1], mdepth));
}
这样,我们可以以两种方式运行它,而且它能正常工作:
> cargo run -- svelte
> cargo run -- svelte 5
不幸的是,这并不十分灵活。当我们以相反的顺序给出参数时,比如 cargo run 5 package-name,应用程序会崩溃,因为它试图将 package-name 解析为数字。
添加标志
现在,我们可能希望参数带有标志,比如 -f 和 -d,这样我们可以以任意顺序给出它们。(另外,使用 Unix 风格的标志还能获得额外加分!)
我们再次更新 get_arguments 函数,这次为参数添加一个合适的结构体,以便更容易返回解析后的参数:
#[derive(Default)]
struct Arguments {
package_name: String,
max_depth: usize,
}
fn get_arguments() -> Arguments {
let args: Vec<_> = std::env::args().collect();
// 至少应该有 3 个参数:命令名、-f 标志和实际的文件名
if args.len() < 3 {
eprintln!("filename is a required argument");
std::process::exit(1);
}
let mut ret = Arguments::default();
ret.max_depth = usize::MAX;
if args[1] == "-f" {
// 是文件
ret.package_name = args[2].clone();
} else {
// 是最大深度
ret.max_depth = args[2].parse().unwrap();
}
// 现在一个参数已解析,处理第二个
if args.len() > 4 {
if args[3] == "-f" {
ret.package_name = args[4].clone();
} else {
ret.max_depth = args[4].parse().unwrap();
}
}
return ret;
}
fn count(name: &str, max_depth: usize) -> std::io::Result<usize> {
...
}
fn main() {
let args = get_arguments();
match count(&args.package_name, args.max_depth) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
现在,我们可以使用花哨的 - 标志运行它,比如 cargo run -- -f svelte 或 cargo run -- -d 5 -f svelte。
参数和标志的问题
然而,这有一些相当严重的 bug:我们可以两次给出相同的参数,从而完全跳过文件参数 cargo run -- -d 5 -d 7,或者我们可以给出无效的标志,而程序会毫无错误地运行 😭
我们可以通过检查第 27 行上方的 file_name 是否为空来修复这个问题,并在给出错误值时打印预期内容。但是,当我们向 -d 传递非数字时,由于我们直接在 parse 上调用 unwrap,程序也会崩溃。
此外,对于新用户来说,这个应用程序可能会很棘手,因为它不提供任何帮助信息。用户可能不知道要传递什么参数以及以什么顺序,而且应用程序没有像传统 Unix 程序那样的 -h 标志来显示这些信息。
尽管对于这个特定应用程序来说,这些只是小麻烦,但随着选项数量的增加和复杂性的提高,手动维护所有这些变得越来越困难。
这就是 Clap 发挥作用的地方。
什么是 Clap?
Clap 是一个提供生成参数解析逻辑功能的库,为应用程序提供整洁的 CLI,包括参数说明和 -h 帮助命令。
使用 Clap 非常简单,只需要对我们当前的设置进行少量修改。
Clap 在许多 Rust 项目中有三个常用版本:v2、v3 和 v4。v2 主要提供基于构建器的实现来构建命令行参数解析器。
Clap v4 是最新发布的主版本(截至本文撰写时),它在 v3 的派生过程宏和构建器实现的基础上进行了构建。因此,我们可以注解我们的结构体,宏将为我们派生必要的函数。
这两种方法各有优势,关于它们的详细差异和功能列表,我们可以查看它们的文档和帮助页面,其中提供了示例并建议在哪些情况下使用派生和构建器。
在本文中,我们将看到如何使用带有过程宏的 Clap v4。
将 Clap 添加到项目中
要将 Clap 集成到我们的项目中,在 Cargo.toml 中添加以下内容:
[dependencies]
clap = { version = "4.0", features = ["derive"] }
这将以派生功能添加 Clap 作为依赖项。
现在,让我们从 main 中删除 get_arguments 函数及其调用:
use clap::{Parser, Subcommand};
use std::collections::VecDeque;
use std::fs;
use std::path::PathBuf;
#[derive(Default)]
struct Arguments {
package_name: String,
max_depth: usize,
}
/// 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");
// 用于存储待探索目录的队列
let mut queue = VecDeque::new();
logger.debug("Adding current dir to queue");
// 从当前目录开始
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?;
// 我们只关心目录
if dir.file_type()?.is_dir() {
if dir.file_name() == name {
logger.log(format!("match found at {:?}", dir.path()));
// 找到匹配项,不再继续探索
count += 1;
} else {
logger.debug(format!("adding {:?} to queue", dir.path()));
// 不匹配,检查其子目录
queue.push_back((dir.path(), crr_depth + 1));
}
}
}
}
logger.extra("search completed");
return Ok(count);
}
fn main() {}
接下来,在 Arguments 结构体的派生中添加 Parser 和 Debug:
use clap::Parser;
#[derive(Parser, Default, Debug)]
struct Arguments {...}
最后,在 main 中调用 parse 方法:
let args = Arguments::parse();
println!("{:?}", args);
如果我们运行没有参数的应用程序 cargo run,会得到一个错误消息:
error: The following required arguments were not provided:
<PACKAGE_NAME>
<MAX_DEPTH>
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
For more information try --help
这已经比我们的手动版本提供了更好的错误报告!
作为额外奖励,它自动提供了 -h 标志用于帮助,可以打印参数及其顺序:
package-hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME>
<MAX_DEPTH>
OPTIONS:
-h, --help Print help information
现在,如果我们为 MAX_DEPTH 提供非数字内容,我们会得到一个错误,说明提供的字符串不是数字:
> cargo run -- 5 test
error: Invalid value "test" for '<MAX_DEPTH>': invalid digit found in string
For more information try --help
如果我们以正确的顺序提供参数,我们会得到 println 的输出:
> cargo run -- test 5
Arguments { package_name: "test", max_depth: 5 }
所有这些只需要两行新代码,无需编写任何解析代码或进行任何错误处理!🎉
更新帮助消息
目前,我们的帮助消息有点单调,因为它只显示参数的名称和顺序。如果用户能看到特定参数的用途,甚至应用程序版本(以防他们想要报告错误),那就更有帮助了。
Clap 也为此提供了选项:
#[derive(Parser, Debug)]
#[command(author = "Author Name", version, about="A Very simple Package Hunter")]
struct Arguments{...}
现在,-h 输出显示所有详细信息,还提供了 -V 标志来打印版本号:
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME>
<MAX_DEPTH>
OPTIONS:
-h, --help Print help information
-V, --version Print version information
在宏本身中编写多行信息可能会有点繁琐。因此,我们可以为结构体添加文档注释 ///,宏将使用它作为 about 信息。如果两者都存在,宏中的信息优先于文档注释:
#[command(author = "Author Name", version, about)]
/// A Very simple Package Hunter
struct Arguments {...}
这提供了与之前相同的帮助。
要为参数添加信息,我们可以为参数本身添加类似的注释:
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter <PACKAGE_NAME> <MAX_DEPTH>
ARGS:
<PACKAGE_NAME> Name of the package to search
<MAX_DEPTH> maximum depth to which sub-directories should be explored
OPTIONS:
-h, --help Print help information
-V, --version Print version information
这更有帮助!
现在,让我们恢复我们之前拥有的其他功能,比如参数标志(-f 和 -d)和将深度参数设为可选。
在 Clap 中添加标志
Clap 使标志参数变得极其简单:我们只需为结构体成员添加另一个 Clap 宏注解 #[arg(short, long)]。
这里,short 指的是标志的简写版本,比如 -f,long 指的是完整版本,比如 --file。我们可以选择其中一种或两种。添加后,我们现在有:
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter --package-name <PACKAGE_NAME> --max-depth <MAX_DEPTH>
OPTIONS:
-h, --help Print help information
-m, --max-depth <MAX_DEPTH> maximum depth to which sub-directories should be explored
-p, --package-name <PACKAGE_NAME> Name of the package to search
-V, --version Print version information
由于两个参数都有标志,现在没有位置参数了;这意味着我们不能运行 cargo run -- test 5,因为 Clap 会查找标志并给出错误,说参数未提供。
相反,我们可以运行 cargo run -- -p test -m 5 或 cargo run -- -m 5 -p test,它会正确解析两者,给出以下输出:
Arguments { package_name: "test", max_depth: 5 }
因为我们总是需要包名,我们可以将其设为位置参数,这样就不需要每次都输入 -p 标志。
为此,从它上面移除 #[arg(short,long)];现在第一个没有标志的参数将被视为包名:
> cargo run -- test -m 5
Arguments { package_name: "test", max_depth: 5 }
> cargo run -- -m 5 test
Arguments { package_name: "test", max_depth: 5 }
需要注意的是,在简写参数中,如果两个参数以相同字母开头——比如 package-name 和 path——并且两者都启用了简写标志,应用程序会在调试构建中崩溃,在发布构建中给出一些令人困惑的错误消息。
因此,请确保:
- 所有参数以不同的字母开头
- 或者只有以相同字母开头的参数中的一个具有简写标志
下一步是使 max_depth 可选。
使参数可选
要将任何参数标记为可选,只需将该参数的类型设为 Option<T>,其中 T 是原始类型参数。因此在我们的情况下,我们有:
#[arg(short, long)]
/// maximum depth to which sub-directories should be explored
max_depth: Option<usize>,
这应该可以解决问题。这种更改也反映在帮助中,它不再将最大深度列为必需参数:
package-hunter 0.1.0
Author Name
A Very simple Package Hunter
USAGE:
package-hunter [OPTIONS] <PACKAGE_NAME>
ARGS:
<PACKAGE_NAME> Name of the package to search
OPTIONS:
-h, --help Print help information
-m, --max-depth <MAX_DEPTH> maximum depth to which sub-directories should be explored
-V, --version Print version information
而且,我们可以不提供 -m 标志运行它:
> cargo run -- test
Arguments { package_name: "test", max_depth: None }
但是,这仍然有点麻烦;现在我们必须对 max_depth 运行 match,如果它是 None,我们就将其设为 usize::MAX,就像之前一样。
然而,Clap 在这里也有解决方案!与其使其成为 Option<T>,我们可以设置参数的默认值(如果不提供的话)。
因此,修改如下:
#[arg(default_value_t=usize::MAX, short, long)]
/// maximum depth to which sub-directories should be explored
max_depth: usize,
现在,我们可以选择是否提供 max_depth 的值(usize 的最大值取决于你的系统配置):
> cargo run -- test
Arguments { package_name: "test", max_depth: 18446744073709551615 }
> cargo run -- test -m 5
Arguments { package_name: "test", max_depth: 5 }
现在,让我们像以前一样将其连接到 main 中的 count 函数:
fn main() {
let args = Arguments::parse();
match count(&args.package_name, args.max_depth) {
Ok(c) => println!("{} uses found", c),
Err(e) => eprintln!("error in processing : {}", e),
}
}
通过这种方式,我们恢复了原始功能,但代码更少,还增加了一些额外功能!
修复空字符串 bug
package-hunter 正如预期那样运行,但唉,从手动解析阶段就存在一个微妙的 bug,并延续到了基于 Clap 的版本。你能猜出它是什么吗?
尽管对于我们的小应用程序来说这不是一个非常危险的 bug,但对于其他应用程序来说可能是漏洞。在我们的案例中,当应该给出错误时,它会给出错误的结果。
尝试运行以下命令:
> cargo run -- ""
0 uses found
这里,package_name 被作为空字符串传递,而空包名不应该被允许。这是由于我们运行命令的 shell 向应用程序传递参数的方式造成的。
通常,shell 使用空格来分割传递给程序的参数列表,所以 abc def hij 会被作为三个单独的参数给出:abc、def 和 hij。
如果我们想在参数中包含空格,必须在其周围加上引号,比如 "abc efg hij"。这样 shell 就知道这是一个单一参数,并按此传递。
另一方面,这也允许我们向应用程序传递空字符串或仅包含空格的字符串。再次,Clap 来救援!它提供了一种方法来拒绝参数的空值:
#[arg(value_parser = validate_package_name)]
/// Name of the package to search
package_name: String,
有了这个,如果我们尝试给出空字符串作为参数,我们会得到一个错误:
> cargo run -- ""
error: The argument '<PACKAGE_NAME>' requires a value but none was supplied
但是,这仍然将空格作为包名提供,意味着 "" 是一个有效参数。要修复这个问题,我们必须提供一个自定义验证器来检查名称是否有前导或尾随空格,并在有空格时拒绝它。
我们将验证函数定义如下:
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(())
}
}
然后,为其设置 package_name 如下:
#[arg(value_parser = validate_package_name)]
/// Name of the package to search
package_name: String,
现在,如果我们尝试传递空字符串或包含空格的字符串,它会给出错误,正如应该的那样:
> cargo run -- ""
error: The argument '<PACKAGE_NAME>' requires a value but none was supplied
> cargo run -- " "
error: Invalid value " " for '<PACKAGE_NAME>': package name cannot have leading and trailing space
通过这种方式,我们可以使用自定义逻辑验证参数,而无需编写所有解析代码。
使用 Clap 进行日志记录
应用程序现在运行良好,但我们无法看到在失败情况下发生了什么。为此,我们应该记录应用程序正在做什么,以便在崩溃时可以看到发生了什么。
就像其他命令行应用程序一样,我们应该有一种简单的方法让用户设置日志级别。默认情况下,它应该只记录主要细节和错误,这样日志不会杂乱,但在应用程序崩溃的情况下,应该有一种模式记录所有可能的详细信息。
像其他应用程序一样,让我们让应用程序使用 -v 标志来设置详细级别;没有标志是最小日志记录,-v 是中级日志记录,-vv 是最大日志记录。
为此,Clap 提供了一种方法,使参数的值设置为它出现的次数,这正是我们需要的!我们可以添加另一个参数,并将其设置如下:
#[arg(short, long, parse(from_occurrences))]
verbosity: usize,
现在,如果我们运行时不给出 -v 标志,它的值将为零,否则会计算 -v 标志出现的次数:
> cargo run -- test
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 0 }
> cargo run -- test -v
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 1 }
> cargo run -- test -vv
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 2 }
> cargo run -- -vv test -v
Arguments { package_name: "test", max_depth: 18446744073709551615, verbosity: 3 }
使用这个值,我们可以轻松初始化记录器并使其记录适当数量的详细信息。
我没有在这里添加虚拟记录器代码,因为本文重点是参数解析,但你可以在文末的仓库中找到它。
统计和查找项目
现在我们的应用程序运行良好,我们想添加另一个功能:列出我们拥有的项目。这样,当我们想要一个漂亮的项目列表时,可以快速获得。
Clap 具有强大的子命令功能,可以为应用程序提供多个子命令。要使用它,定义另一个带有自己参数的结构体,即子命令。主参数结构体包含所有子命令共用的参数,然后是子命令。
我们将 CLI 结构化如下:
- 日志详细级别和
max_depth参数在主结构体中 count命令接收要查找的文件名并输出计数projects命令接收可选的起始路径来开始搜索projects命令接收可选的排除路径列表,跳过给定的目录
因此,我们添加 count 和 project 枚举如下:
use clap::{Parser, 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>,
},
}
这里,我们将 package_name 移动到 Count 变体中,并在 Projects 变体中添加 start_path 和 exclude 选项。
现在,如果我们检查帮助,它会列出这两个子命令,每个子命令都有自己的帮助。
然后我们可以更新 main 函数以适应它们:
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),
},
}
}
我们也可以像以前一样使用 count 命令来统计使用次数:
> cargo run -- -m 5 count test
由于 max_depth 在主 Arguments 结构体中定义,它必须在子命令之前给出。
然后我们可以根据需要为 projects 命令的排除目录给出多个值:
> cargo run -- projects -e ./dir1 ./dir2
["./dir1", "./dir2"] # exclude vector 的值
我们还可以设置自定义分隔符,如果我们不想用空格分隔值,而是用自定义字符:
#[arg(short, long, multiple_values = true, value_delimiter = ':')]
/// paths to exclude when searching
exclude: Vec<String>,
现在我们可以使用 : 来分隔值:
> cargo run -- projects -e ./dir1:./dir2
["./dir1", "./dir2"]
这完成了应用程序的 CLI。项目列出函数在这里没有显示,但你可以自己尝试编写,或者在 GitHub 仓库中查看其代码。
与其他 CLI 库的比较
还有一些其他库允许你构建命令行解析应用程序。它们包括 Argh、Pico-args 和 Gumdrop。以下是基于它们的流行度、文档、维护等方面的比较摘要:
| Argh | Clap | Pico-args | Gumdrop | |
|---|---|---|---|---|
| 社区 | 小型,正在增长(GitHub 上 1.6k 星) | 大型,活跃(GitHub 上 13.7k 星) | 小型,专注(555 星) | 非常小(223 GitHub 星) |
| 文档 | GitHub 上的基本示例 | 带示例仓库的 Rust 文档 | Rust 风格文档 | Rust 风格文档 |
| 易用性 | 易于使用 | 易于使用 | 易于使用 | 中等设置,直接 |
| 自定义性 | 有限且直接 | 高度可定制 | 最小自定义选项 | 有限且直接 |
| 子命令支持 | 是 | 是 | 是 | 是 |
| 依赖项 | 2 个依赖项和 2 个开发依赖项 | 2 个依赖项和 7 个开发依赖项 | 0 个依赖项 | 1 个依赖项和 1 个开发依赖项 |
| 维护 | 最后更新是 3 个月前 | 最后更新是几天前 | 最后更新是 9 个月前 | 最近更新是 2 年前 |
结论
现在你了解了 Clap,可以为你的项目制作干净优雅的 CLI。它还有许多其他功能,如果你的项目需要特定的命令行功能,Clap 很可能已经有了。