写web项目或者app等,必不可少的要接受参数和校验参数的准确性,基本也是项目开始的第一步,那么我们今天来看下rust提供了哪些优秀的crates

关注 vx golang技术实验室,获取更多golang、rust好文

一、clap_v3

本来是想用structOpt,但是看文档是这样描述的

由于 clap v3 现已发布,并且 structopt 功能已集成(几乎按原样),因此 structopt 现在处于维护模式:不会添加新功能。

错误将被修复,文档改进将被接受。

1. 1 添加依赖

[dependencies]
clap = { version = "4.2.7", features = ["derive","cargo"] }
features = "0.10.0"
cargo = "0.70.1"


或者
cargo add clap -- features     cargo
需要注意:如果不启用 cargo feature ,则会报如下错误。
requires `cargo` feature

如果使用command!arg! 必须在features 中添加cargo

1.2 快速启动

use std::env::Args;
/////////////////////////////////////// clap_v3 原来的structOpt //////////////////////////////////
use std::path::PathBuf;

use clap::{arg, command, value_parser, ArgAction, Command};

fn test() {
    let matches = command!() // requires `cargo` feature
        .arg(arg!([name] "Optional name to operate on"))
        .arg(
            arg!(
                -c --config <FILE> "Sets a custom config file"
            )
                // We don't have syntax yet for optional options, so manually calling `required`
                .required(false)
                .value_parser(value_parser!(PathBuf)),
        )
        .arg(arg!(
            -d --debug ... "Turn debugging information on"
        ))
        .subcommand(
            Command::new("test")
                .about("does testing things")
                .arg(arg!(-l --list "lists test values").action(ArgAction::SetTrue)),
        )
        .get_matches();

    // You can check the value provided by positional arguments, or option arguments
    if let Some(name) = matches.get_one::<String>("name") {
        println!("Value for name: {name}");
    }

    if let Some(config_path) = matches.get_one::<PathBuf>("config") {
        println!("Value for config: {}", config_path.display());
    }

    // You can see how many times a particular flag or argument occurred
    // Note, only flags can have multiple occurrences
    match matches
        .get_one::<u8>("debug")
        .expect("Count's are defaulted")
    {
        0 => println!("Debug mode is off"),
        1 => println!("Debug mode is kind of on"),
        2 => println!("Debug mode is on"),
        _ => println!("Don't be crazy"),
    }

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    if let Some(matches) = matches.subcommand_matches("test") {
        // "$ myapp test" was run
        if matches.get_flag("list") {
            // "$ myapp test -l" was run
            println!("Printing testing lists...");
        } else {
            println!("Not printing testing lists...");
        }
    }

    // Continued program logic goes here...
}

pub fn claps(){
    test()
}

1、默认执行情况

cargo run
Debug mode is off

2、参看帮助文档

cargo run --help
Run a binary or example of the local package

Usage: cargo run [OPTIONS] [args]...

Arguments:
  [args]...  Arguments for the binary or example to run

Options:
  -q, --quiet                   Do not print cargo log messages
      --bin [<NAME>]            Name of the bin target to run
      --example [<NAME>]        Name of the example target to run
  -p, --package [<SPEC>]        Package with the target to run
  -j, --jobs <N>                Number of parallel jobs, defaults to # of CPUs.
      --keep-going              Do not abort the build as soon as there is an error (unstable)
  -r, --release                 Build artifacts in release mode, with optimizations
      --profile <PROFILE-NAME>  Build artifacts with the specified profile
  -F, --features <FEATURES>     Space or comma separated list of features to activate
      --all-features            Activate all available features
      --no-default-features     Do not activate the `default` feature
      --target <TRIPLE>         Build for the target triple
      --target-dir <DIRECTORY>  Directory for all generated artifacts
      --manifest-path <PATH>    Path to Cargo.toml
      --message-format <FMT>    Error format
      --unit-graph              Output build graph in JSON (unstable)
      --ignore-rust-version     Ignore `rust-version` specification in packages
      --timings[=<FMTS>]        Timing output formats (unstable) (comma separated): html, json
  -h, --help                    Print help
  -v, --verbose...              Use verbose output (-vv very verbose/build.rs output)
      --color <WHEN>            Coloring: auto, always, never
      --frozen                  Require Cargo.lock and cache are up to date
      --locked                  Require Cargo.lock is up to date
      --offline                 Run without accessing the network
      --config <KEY=VALUE>      Override a configuration value
  -Z <FLAG>                     Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details

Run `cargo help run` for more detailed information.

3、使用 -dd 参数

cargo run -- -dd test 
Debug mode is on
Not printing testing lists...

1.3 command 解析器

1.3.1 基本使用

fn command(){
    let matches = Command::new("MyApp")
        .version("1.0")
        .author("ZHangQL Z <ZQL@gmail.com>")
        .about("this is the test project")
        .args(&[//次数是args,如果单个的的arg
            arg!(--config <FILE> "a required file for the configuration and no short").
                required(true)//必须包含
                .require_equals(true)//要求使用等号赋值
                // .default_value() //设置默认值
            ,
            arg!(-d --debug ... "turns on debugging information and allows multiples"),
            arg!([input] "an optional input file to use")
        ])
        .arg(arg!(--two <VALUE>).required(true))//单个的
        .get_matches();

    println!(
        "config: {:?}",
        matches.get_one::<String>("config").expect("required")
    );
    println!(
        "debug: {:?}",
        matches.get_one::<String>("debug")
    );
    println!(
        "input: {:?}",
        matches.get_one::<String>("input")
    );

}

查看help

RUST_BACKTRACE=1 cargo run  -- --help

this is the test project

Usage: my_test [OPTIONS] --config=<FILE> --two <VALUE> [input]

Arguments:
  [input]  an optional input file to use

Options:
      --config=<FILE>  a required file for the configuration and no short
  -d, --debug...       turns on debugging information and allows multiples
      --two <VALUE>    
  -h, --help           Print help
  -V, --version        Print version

运行

RUST_BACKTRACE=1 cargo run  -- --config=./config.yaml --two rrr lllll

config: "./config.yaml"
two: Some("rrr")
input: Some("lllll")

1.3.2 使用command!构建解析器

你也可以使用 command! 宏 构建解析器,不过要想使用 command! 宏,你需要开启 cargo feature。

use clap::{arg, command};

fn main() {
    // requires `cargo` feature, reading name, version, author, and description from `Cargo.toml`
    let matches = command!()
        .arg(arg!(--two <VALUE>).required(true))
        .arg(arg!(--one <VALUE>).required(true))
        .get_matches();

    println!(
        "two: {:?}",
        matches.get_one::<String>("two").expect("required")
    );
    println!(
        "one: {:?}",
        matches.get_one::<String>("one").expect("required")
    );
}

1.3.3 Command::next_line_help

使用 Command::next_line_help 方法 可以修改参数打印行为

use clap::{arg, command, ArgAction};

fn main() {
    let matches = command!() // requires `cargo` feature
        .next_line_help(true)
        .arg(arg!(--two <VALUE>).required(true).action(ArgAction::Set))
        .arg(arg!(--one <VALUE>).required(true).action(ArgAction::Set))
        .get_matches();

    println!(
        "two: {:?}",
        matches.get_one::<String>("two").expect("required")
    );
    println!(
        "one: {:?}",
        matches.get_one::<String>("one").expect("required")
    );
}
Usage: my_test [OPTIONS] --config=<FILE> --two <VALUE> [input]

Arguments:
  [input]
          an optional input file to use

Options:
      --config=<FILE>
          a required file for the configuration and no short
  -d, --debug...
          turns on debugging information and allows multiples
      --two <VALUE>
          
  -h, --help
          Print help
  -V, --version
          Print version

效果就是:参数的描述和参数是分行的,描述信息在参数下一行。

1.4 添加命令行参数(Adding Arguments)

我们可以使用 Command::arg 方法来添加 Arg 对象来添加命令行参数

fn adding_arg(){
    let matches = command!()
        .arg(Arg::new("name"))
        .get_matches();
    println!("name: {:?}", matches.get_one::<String>("name"));
}

查看help

RUST_BACKTRACE=1 cargo run  -- --help
Usage: my_test [name]

Arguments:
  [name]  

Options:
  -h, --help     Print help
  -V, --version  Print version

2、使用 name 参数:默认

cargo run
name: None

3、使用 name 参数:blob

注意定义的时候没有是直接使用的 不需要key的

cargo run bob
name: Some("bob")

1.4.2 设置参数行为

需要注意:参数默认值是一个 Set 类型

我们可以使用 Command::action 方法来设置 参数行为。如果可以添加多个只,我们可以使用 ArgAction::Append

use clap::{command, Arg, ArgAction};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(Arg::new("name").action(ArgAction::Append))
        .get_matches();

    let args = matches
        .get_many::<String>("name")
        .unwrap_or_default()
        .map(|v| v.as_str())
        .collect::<Vec<_>>();

    println!("names: {:?}", &args);
}

1.5 参数选项

一个参数行为的标志:

  • 顺序无关
  • 可选参数
  • 意图清晰
fn arg_switch(){
    let matches = command!()
        .arg(Arg::new("name")
            .short('n')
            .long("name")
        ).get_matches();
    println!("name: {:?}", matches.get_one::<String>("name"));
}

上述代码:我们定义了一个name参数,缩写是n,全拼是name,也就是如下形式

-n, --name <name>

我们使用方式就有如下几种

cargo run -- --name blo
cargo run -- --name=blob
cargo run -- -n blob
cargo run -- -n=blob
cargo run -- -nblob

1.5.1 开启/关闭标志

我们可以是 ArgAction::SetTrue 开启参数

use clap::{command, Arg, ArgAction};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(ArgAction::SetTrue),
        )
        .get_matches();

    println!("verbose: {:?}", matches.get_flag("verbose"));

}

1.5.2参数调用计数

我们可以使用 ArgAction::Count

use clap::{command, Arg, ArgAction};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .action(ArgAction::Count),
        )
        .get_matches();

    println!("verbose: {:?}", matches.get_count("verbose"));
}

默认值是0,多次使用参数就会计数

cargo run --  --verbose --verbose

1.5.3 默认值

fn default_value(){
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!([PORT])
                .value_parser(value_parser!(u16))
                .default_value("2023"),
        )
        .get_matches();

    println!(
        "port: {:?}",
        matches
            .get_one::<u16>("PORT")
            .expect("default ensures there is always a value")
    );

}
cargo run
port: 2023

cargo run 897
port: 897

1.5.4 参数校验

默认情况下,参数被认为是 String,并且使用 UTF-8 校验。

枚举值

fn enum_check(){
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<MODE>)
                .help("What mode to run the program in")
                .value_parser(["fast", "slow"]),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    match matches
        .get_one::<String>("MODE")
        .expect("'MODE' is required and parsing will fail if its missing")
        .as_str()
    {
        "fast" => {
            println!("Hare");
        }
        "slow" => {
            println!("Tortoise");
        }
        _ => unreachable!(),
    }
}
cargo run rrr 
error: invalid value 'rrr' for '<MODE>'
  [possible values: fast, slow]

cargo run fast
Hare

如果我们开启了 derive feature, 则我们也可以实现 ValueEnum 特征实现相同的功能

use clap::{arg, builder::PossibleValue, command, value_parser, ValueEnum};

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Mode {
    Fast,
    Slow,
}

// Can also be derived with feature flag `derive`
impl ValueEnum for Mode {
    fn value_variants<'a>() -> &'a [Self] {
        &[Mode::Fast, Mode::Slow]
    }

    fn to_possible_value<'a>(&self) -> Option<PossibleValue> {
        Some(match self {
            Mode::Fast => PossibleValue::new("fast").help("Run swiftly"),
            Mode::Slow => PossibleValue::new("slow").help("Crawl slowly but steadily"),
        })
    }

}

impl std::fmt::Display for Mode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.to_possible_value()
            .expect("no values are skipped")
            .get_name()
            .fmt(f)
    }
}

impl std::str::FromStr for Mode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        for variant in Self::value_variants() {
            if variant.to_possible_value().unwrap().matches(s, false) {
                return Ok(*variant);
            }
        }
        Err(format!("invalid variant: {s}"))
    }

}

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<MODE>)
                .help("What mode to run the program in")
                .value_parser(value_parser!(Mode)),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    match matches
        .get_one::<Mode>("MODE")
        .expect("'MODE' is required and parsing will fail if its missing")
    {
        Mode::Fast => {
            println!("Hare");
        }
        Mode::Slow => {
            println!("Tortoise");
        }
    }

}

1.5.5 校验值

我们可以使用 Arg::value_parser 验证并解析成我们需要的任何类型。

fn validated(){
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<PORT>)
                .help("Network port to use")
                .value_parser(value_parser!(u16).range(1..)),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    let port: u16 = *matches
        .get_one::<u16>("PORT")
        .expect("'PORT' is required and parsing will fail if its missing");
    println!("PORT = {port}");

}

cargo run 0
error: invalid value '0' for '<PORT>': 0 is not in 1..=65535

cargo run 1
PORT = 10

1.5.6 自定义解析器

我们也可以使用自定义解析器用于改进错误信息提示和额外的验证。

use std::ops::RangeInclusive;

use clap::{arg, command};

fn main() {
    let matches = command!() // requires `cargo` feature
        .arg(
            arg!(<PORT>)
                .help("Network port to use")
                .value_parser(port_in_range),
        )
        .get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    let port: u16 = *matches
        .get_one::<u16>("PORT")
        .expect("'PORT' is required and parsing will fail if its missing");
    println!("PORT = {port}");
}

const PORT_RANGE: RangeInclusive<usize> = 1..=65535;

fn port_in_range(s: &str) -> Result<u16, String> {
    let port: usize = s
        .parse()
        .map_err(|_| format!("`{s}` isn't a port number"))?;
    if PORT_RANGE.contains(&port) {
        Ok(port as u16)
    } else {
        Err(format!(
            "port not in range {}-{}",
            PORT_RANGE.start(),
            PORT_RANGE.end()
        ))
    }
}

1.5。7 参数关系(Argument Relations)

我们可以声明 Arg 和 ArgGroup。ArgGroup 用于声明参数关系。

use std::path::PathBuf;

use clap::{arg, command, value_parser, ArgAction, ArgGroup};

fn main() {
    // Create application like normal
    let matches = command!() // requires `cargo` feature
        // Add the version arguments
        .arg(arg!(--"set-ver" <VER> "set version manually"))
        .arg(arg!(--major         "auto inc major").action(ArgAction::SetTrue))
        .arg(arg!(--minor         "auto inc minor").action(ArgAction::SetTrue))
        .arg(arg!(--patch         "auto inc patch").action(ArgAction::SetTrue))
        // Create a group, make it required, and add the above arguments
        .group(
            ArgGroup::new("vers")
                .required(true)
                .args(["set-ver", "major", "minor", "patch"]),
        )
        // Arguments can also be added to a group individually, these two arguments
        // are part of the "input" group which is not required
        .arg(
            arg!([INPUT_FILE] "some regular input")
                .value_parser(value_parser!(PathBuf))
                .group("input"),
        )
        .arg(
            arg!(--"spec-in" <SPEC_IN> "some special input argument")
                .value_parser(value_parser!(PathBuf))
                .group("input"),
        )
        // Now let's assume we have a -c [config] argument which requires one of
        // (but **not** both) the "input" arguments
        .arg(
            arg!(config: -c <CONFIG>)
                .value_parser(value_parser!(PathBuf))
                .requires("input"),
        )
        .get_matches();

    // Let's assume the old version 1.2.3
    let mut major = 1;
    let mut minor = 2;
    let mut patch = 3;
    
    // See if --set-ver was used to set the version manually
    let version = if let Some(ver) = matches.get_one::<String>("set-ver") {
        ver.to_owned()
    } else {
        // Increment the one requested (in a real program, we'd reset the lower numbers)
        let (maj, min, pat) = (
            matches.get_flag("major"),
            matches.get_flag("minor"),
            matches.get_flag("patch"),
        );
        match (maj, min, pat) {
            (true, _, _) => major += 1,
            (_, true, _) => minor += 1,
            (_, _, true) => patch += 1,
            _ => unreachable!(),
        };
        format!("{major}.{minor}.{patch}")
    };
    
    println!("Version: {version}");
    
    // Check for usage of -c
    if matches.contains_id("config") {
        let input = matches
            .get_one::<PathBuf>("INPUT_FILE")
            .unwrap_or_else(|| matches.get_one::<PathBuf>("spec-in").unwrap())
            .display();
        println!(
            "Doing work using input {} and config {}",
            input,
            matches.get_one::<PathBuf>("config").unwrap().display()
        );
    }

}

此时 --set-ver <VER>|--major|--minor|--patch 是一个组的参数。

1.5.8 自定义校验(Custom Validation)

我们可以创建自定义校验错误 Command::error 方法可以返回指定错误 Error和自定义错误信息


use std::path::PathBuf;

use clap::error::ErrorKind;
use clap::{arg, command, value_parser, ArgAction};

fn main() {
    // Create application like normal
    let mut cmd = command!() // requires `cargo` feature
        // Add the version arguments
        .arg(arg!(--"set-ver" <VER> "set version manually"))
        .arg(arg!(--major         "auto inc major").action(ArgAction::SetTrue))
        .arg(arg!(--minor         "auto inc minor").action(ArgAction::SetTrue))
        .arg(arg!(--patch         "auto inc patch").action(ArgAction::SetTrue))
        // Arguments can also be added to a group individually, these two arguments
        // are part of the "input" group which is not required
        .arg(arg!([INPUT_FILE] "some regular input").value_parser(value_parser!(PathBuf)))
        .arg(
            arg!(--"spec-in" <SPEC_IN> "some special input argument")
                .value_parser(value_parser!(PathBuf)),
        )
        // Now let's assume we have a -c [config] argument which requires one of
        // (but **not** both) the "input" arguments
        .arg(arg!(config: -c <CONFIG>).value_parser(value_parser!(PathBuf)));
    let matches = cmd.get_matches_mut();

    // Let's assume the old version 1.2.3
    let mut major = 1;
    let mut minor = 2;
    let mut patch = 3;
    
    // See if --set-ver was used to set the version manually
    let version = if let Some(ver) = matches.get_one::<String>("set-ver") {
        if matches.get_flag("major") || matches.get_flag("minor") || matches.get_flag("patch") {
            cmd.error(
                ErrorKind::ArgumentConflict,
                "Can't do relative and absolute version change",
            )
            .exit();
        }
        ver.to_string()
    } else {
        // Increment the one requested (in a real program, we'd reset the lower numbers)
        let (maj, min, pat) = (
            matches.get_flag("major"),
            matches.get_flag("minor"),
            matches.get_flag("patch"),
        );
        match (maj, min, pat) {
            (true, false, false) => major += 1,
            (false, true, false) => minor += 1,
            (false, false, true) => patch += 1,
            _ => {
                cmd.error(
                    ErrorKind::ArgumentConflict,
                    "Can only modify one version field",
                )
                .exit();
            }
        };
        format!("{major}.{minor}.{patch}")
    };
    
    println!("Version: {version}");
    
    // Check for usage of -c
    if matches.contains_id("config") {
        let input = matches
            .get_one::<PathBuf>("INPUT_FILE")
            .or_else(|| matches.get_one::<PathBuf>("spec-in"))
            .unwrap_or_else(|| {
                cmd.error(
                    ErrorKind::MissingRequiredArgument,
                    "INPUT_FILE or --spec-in is required when using --config",
                )
                .exit()
            })
            .display();
        println!(
            "Doing work using input {} and config {}",
            input,
            matches.get_one::<PathBuf>("config").unwrap().display()
        );
    }

}

1.6、子命令(Subcommand)

我们可以使用 Command::subcommand 方法添加子命令。每一个子命令都自己的版本、作者、参数和它的子命令。

use clap::{arg, command, Command};

fn main() {
    let matches = command!() // requires `cargo` feature
        .propagate_version(true)
        .subcommand_required(true)
        .arg_required_else_help(true)
        .subcommand(
            Command::new("add")
                .about("Adds files to myapp")
                .arg(arg!([NAME])),
        )
        .get_matches();

    match matches.subcommand() {
        Some(("add", sub_matches)) => println!(
            "'myapp add' was used, name is: {:?}",
            sub_matches.get_one::<String>("NAME")
        ),
        _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"),
    }

}

我们使用 Command::arg_required_else_help 如果参数不存在,优雅的退出。
使用 Command::propagate_version 可以打印命令的版本号

1.7、测试

我们可以使用 debug_assert! 宏 或者 使用 Command::debug_assert 方法。

use clap::{arg, command, value_parser};

fn main() {
    let matches = cmd().get_matches();

    // Note, it's safe to call unwrap() because the arg is required
    let port: usize = *matches
        .get_one::<usize>("PORT")
        .expect("'PORT' is required and parsing will fail if its missing");
    println!("PORT = {port}");

}

fn cmd() -> clap::Command {
    command!() // requires `cargo` feature
        .arg(
            arg!(<PORT>)
                .help("Network port to use")
                .value_parser(value_parser!(usize)),
        )
}

#[test]
fn verify_cmd() {
    cmd().debug_assert();
}

本文由mdnice多平台发布


-咕咚
1 声望1 粉丝