16 个稳定版本

2.1.0 2023年4月22日
2.0.1 2023年4月22日
1.3.1 2023年4月21日
1.2.5 2022年4月14日
1.0.1 2021年11月28日

命令行界面 中排名 #355


被用于 fix-name-case

MPL-2.0 许可证

19KB
63 行代码(不含注释)



将命令行界面作为函数实现。

fncmd

#[fncmd::fncmd] pub fn main() { println!("Hello, World!"); }

crates.io docs.rs License



fncmd 是一个具有意见的命令行解析器前端,它包装了 clap。其功能与 clap 大致相同,但提供了更自动化和集成的体验。

动机

想象一下你想要创建的命令行程序。本质上,它可以抽象为一个简单函数,该函数以命令行选项作为参数。那么,不应该有什么东西阻止你将其真正地写成函数,而不使用像今天的 Rustaceans 使用的结构体或构建器。

这个概念受到了 argopt 的极大启发,我非常感激这项工作。然而,它仍然需要一些繁琐的代码,特别是处理子命令时。 fncmd 已经从头开始重写,以消除所有复杂性。请查看 子命令 部分,了解我们如何处理它。

安装

这个 crate 仅限于 nightly 版本。在使用之前,请确保已将您的工具链设置为 nightly(例如,拥有 rust-toolchain 文件)。您可能对 为什么是 nightly 感兴趣。

要安装,请在您的项目目录中运行

cargo add fncmd

对于喜欢案例研究的人:请参阅 examples

基础知识

此 crate 仅公开了一个属性宏,fncmd,该宏只能附加到 main 函数上

// main.rs

/// Description of the command line tool
#[fncmd::fncmd]
pub fn main(
  /// Argument foo
  #[opt(short, long)]
  foo: String,
  /// Argument bar
  #[opt(short, long)]
  bar: Option<String>,
) {
  println!("{:?} {:?}", foo, bar);
}

就这么多,现在您已经得到了一个由 clap 处理选项的命令行程序。使用上面的代码,帮助信息如下

$ crate-name --help
Description of the command line tool

Usage: crate-name[EXE] [OPTIONS] --foo <FOO>

Options:
  -f, --foo <FOO>  Argument foo
  -b, --bar <BAR>  Argument bar
  -h, --help       Print help
  -V, --version    Print version

您的命令名称和版本将自动从 Cargo 元数据中推断。

opt 属性的使用几乎与底层 arg 属性 完全相同。它们直接传递,唯一的区别是如果没有提供配置,则隐含了 (long),即 #[opt] 表示 #[opt(long)]。如果您想不带 --foo 就取 foo 参数,只需省略 #[opt]

子命令

如您所知,在 Cargo 项目中 您可以将其他二进制程序的入口点放入 src/bin。如果 1) 它们的名称以 crate-name 前缀开头,2) 它们的 main 函数装饰了 #[fncmd] 属性,并且 3) 被公开为 pub,那么这些将自动包装为默认二进制目标 crate-name 的子命令。假设您有以下目录结构

src
├── main.rs
└── bin
    ├── crate-name-subcommand1.rs
    └── crate-name-subcommand2.rs

您将得到以下子命令结构

crate-name
├── crate-name subcommand1
└── crate-name subcommand2

多个命令和嵌套子命令

实际上,fncmd 并没有区分“默认”二进制和“附加”二进制。它仅基于前缀结构确定子命令结构。因此,在您的 Cargo.toml 中配置二进制目标应该按预期工作,例如

[[bin]]
name = "crate-name"
path = "src/clis/crate-name.rs"

[[bin]]
name = "another"
path = "src/clis/another.rs"

[[bin]]
name = "another-sub" # `pub`
path = "src/clis/another-sub.rs"

[[bin]]
name = "another-sub-subsub" # `pub`
path = "src/clis/another-sub-subsub.rs"

[[bin]]
name = "another-orphan" # non-`pub`
path = "src/clis/another-orphan.rs"

[[bin]]
name = "another-orphan-sub" # `pub`
path = "src/clis/another-orphan-sub.rs"

此配置生成以下命令

crate-name

another
└── another sub
    └── another sub subsub

another-orphan
└── another-orphan sub

请注意,another-orphan 不包含在 another 中,因为它没有被公开为 pub。如上图所示,使 main 不是 pub 仅当您希望它与其他程序有共同的 prefixes 但不希望被其他程序包含时才有意义,因此在大多数情况下,您可以设置 pub 而无需考虑。

当然,也可以在不手动编辑 Cargo.toml 的情况下达到相同的结构,只需将文件放入默认位置

src
├── main.rs
└── bin
    ├── another.rs
    ├── another-sub.rs
    ├── another-sub-subsub.rs
    ├── another-orphan.rs
    └── another-orphan-sub.rs

与异构属性宏一起使用

有时您可能想使用其他属性宏,如 #[tokio::main]#[async_std::main] 来转换 main 函数。在这种情况下,您必须将 #[fncmd] 放在最高级别

/// Description of the command line tool
#[fncmd]
#[tokio::main]
pub async fn main(hello: String) -> anyhow::Result<()> {
  ...
}

但是不要这样做

/// Description of the command line tool
#[tokio::main]
#[fncmd]
pub async fn main(hello: String) -> anyhow::Result<()> {
  ...
}

这是因为像 #[tokio::main] 这样的宏会在其内部进行一些断言,因此我们需要向它们提供一个良好的 main 函数版本,例如删除参数。

文档注释的位置并不重要。

限制

fncmd 设计上不会支持以下功能。这就是为什么 fncmd 被称为“有偏见的”。

无法在帮助信息中显示作者

在帮助信息中显示作者在普通用户看来只会增加噪音。

无法更改命令的名称和版本为不同的值

将元数据(如 nameversion)更改为与 Cargo.toml 中定义的不同值,很容易破坏其可维护性和一致性。

无法将 #[fncmd] 附接到 main 之外的函数

#[fncmd] 附接到任意函数会导致单个文件代码库膨胀,这在一般情况下应该避免。

为什么是 nightly

自动确定哪些目标是子命令或不是子命令的方式需要 #[fncmd] 宏本身知道附加的目标名称,以及调用它的文件的路径。这可以通过 Span::source_file 实现,这背后是一个不稳定的特性标志 proc_macro_span

依赖

~4MB
~79K SLoC