2 个版本

0.0.3 2020年12月13日
0.0.2 2020年12月13日
0.0.1 2020年12月13日

#763命令行界面

MIT 许可协议

9KB
197

type-cli

type-cli 是一个方便、强类型的命令行界面解析器。

让我们先为 grep 创建一个界面。

基本用法

use type_cli::CLI;

#[derive(CLI)]
struct Grep(String, String);

fn main() {
    let Grep(pattern, file) = Grep::process();
    let pattern = regex::Regex::new(&pattern).unwrap();

    eprintln!("Searching for `{}` in {}", pattern, file);
}

现在,如果我们用参数运行二进制文件,它们将被正确解析。如果我们遗漏了一个参数,它会给出有用的错误。

$ grep foo* myFile
Searching for `foo*` in myFile

$ grep foo*
Expected an argument at position `2`

然而,这并不是一个忠实的 grep 界面:在 grep 中,文件是可选的。此外,那个 unwrap() 看起来有点糟糕。

use type_cli::CLI;

#[derive(CLI)]
struct Grep(regex::Regex, #[optional] Option<String>);

fn main() {
    match Grep::process() {
        Grep(pattern, Some(file)) => eprintln!("Serching for `{}` in {}", pattern, file),
        Grep(pattern, None) => eprintln!("Searching for `{}` in stdin", pattern),
    }
}

那是什么?我们正在直接接受一个 Regex 作为参数?在 type-cli 中,任何实现了 FromStr 的类型都可以作为参数。任何解析错误都会优雅地返回给用户,无需你担心。

$ grep foo(
Error parsing positional argument `1`:
regex parse error:
    foo(
       ^
error: unclosed group

在这里,你还可以看到必须使用 #[optional] 来注释可选参数。

$ grep foo* myFile
Serching for `foo*` in myFile

$ grep foo*
Searching for `foo*` in stdin

这个界面 仍然 并不完全忠实;grep 允许多个文件进行搜索。

use type_cli::CLI;

#[derive(CLI)]
struct Grep(regex::Regex, #[variadic] Vec<String>);

fn main(){
    let Grep(pattern, file_list) = Grep::process();
    if file_list.is_empty() {
        eprintln!("Searching for `{}` in stdin", pattern);
    } else {
        eprint!("Searching for `{}` in ", pattern);
        file_list.iter().for_each(|f| eprint!("{}, ", f));
    }
}

如果你在最后一个字段上注释 #[variadic],它将解析任意数量的参数。这对于任何实现了 FromIterator 的集合都有效。

$ grep foo*
Searching for `foo*` in stdin

$grep foo* myFile yourFile ourFile
Searching for `foo*` in myFile, yourFile, ourFile,

尽管如此,这仍然不是理想的解决方案。没有任何字段有名字,也没有标志或选项!显然,元组结构体限制了我们的能力。

命名参数和标志

use type_cli::CLI;

#[derive(CLI)]
struct Grep {
    pattern: regex::Regex,

    #[named]
    file: String,

    #[flag(short = "i")]
    ignore_case: bool,
}

fn main() {
    let Grep { pattern, file, ignore_case } = Grep::process();
    eprint!("Searching for `{}` in {}", pattern, file);
    if ignore_case {
        eprint!(", ignoring case");
    }
    eprintln!();
}

命名参数使用 #[named] 注释,并允许它们以任何顺序传递给命令。默认情况下,命名参数仍然是必需的,但也可以用 #[optional] 标记。

$ grep foo*
Expected an argument named `--file`

$ grep foo* --file myFile
Searching for `foo*` in myFile

标志使用 #[flag] 进行注释,并且是完全可选的布尔或整数标志。您可以使用 #[flag(short = "a")] (此形式也适用于命名参数)指定简短形式。

$ grep foo* --file myFile --ignore-case
Searching for `foo*` in myFile, ignoring case

$ grep foo* --file myFile -i
Searching for `foo*` in myFile, ignoring case

这似乎很好,但如果我想在我的应用程序中使用多个命令呢?

子命令

use type_cli::CLI;

#[derive(CLI)]
enum Cargo {
    New(String),
    Build {
        #[named] #[optional]
        target: Option<String>,
        #[flag]
        release: bool,
    },
    Clippy {
        #[flag]
        pedantic: bool,
    }
}

fn main() {
    match Cargo::process() {
        Cargo::New(name) => eprintln!("Creating new crate `{}`", name),
        Cargo::Build { target, release } => {
            let target = target.as_deref().unwrap_or("windows");
            if release {
                eprintln!("Building for {} in release", target);
            } else {
                eprintln!("Building for {}", target);
            }
        }
        Cargo::Clippy { pedantic: true } => eprintln!("Annoyingly checking your code."),
        Cargo::Clippy { pedantic: false } => eprintln!("Checking your code."),
    }
}

如果您从一个枚举派生 CLI,则每个变体将表示一个子命令。每个子命令都使用与之前相同的语法进行解析。

Rust 的 Pascal 风格命名将自动转换为壳的标准形式: SubCommand -> sub-command

$ cargo new myCrate
Creating new crate `myCrate`

$ cargo build
Building for windows

$ cargo build --target linux
Building for linux

$ cargo build --target linux --release
Building for linux in release

$ cargo clippy
Checking your code.

$ cargo clippy --pedantic
Annoyingly checking your code.

关于文档呢?

--help

use type_cli::CLI;

#[derive(CLI)]
#[help = "Build manager tool for rust"]
enum Cargo {
    New(String),

    #[help = "Build the current crate."]
    Build {
        #[named] #[optional]
        #[help = "the target platform"]
        target: Option<String>,

        #[flag]
        #[help = "build for release mode"]
        release: bool,
    },

    #[help = "Lint your code"]
    Clippy {
        #[flag]
        #[help = "include annoying and subjective lints"]
        pedantic: bool,
    }
}

type-cli 将自动为您生成的命令生成帮助屏幕。如果您对子命令或参数使用 #[help = ""] 注释,它将包括您的简短描述。显示时,它将被发送到标准错误,并且进程将以非零状态退出。

$ cargo
Help - cargo
Build manager tool for rust

SUBCOMMANDS:
    new
    build       Build the current crate.
    clippy      Lint your code

对于枚举,如果没有指定子命令调用命令,将显示此内容。

$ cargo build --help
Help - build
Build the current crate.

ARGUMENTS:
    --target    the target platform     [optional]

FLAGS:
    --release   build for release mode


$ cargo clippy -h
Help - clippy
Lint your code

FLAGS:
    --pedantic  include annoying and subjective lints

对于结构体或子命令,如果传递了标志 --help-h,将调用此内容。目前不支持为元组结构体提供帮助信息。

依赖关系

~3.5–5MB
~97K SLoC