rust从0开始写项目-06-如何接受命令行参数clap-01

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

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

Part1一、clap_v3

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

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

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

11. 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

21.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...

31.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

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

41.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);
}

51.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 |--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()
        );
    }

}

61.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 可以打印命令的版本号

71.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多平台发布

相关推荐
zopple4 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001115 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本6 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34166 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan7 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer8 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3568 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3568 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer9 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP9 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪