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

40 阅读5分钟

上篇我们介绍了 如何在app内使用clap接受参数,本文介绍下如何使用struct 优雅的接收和校验数据

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

二、derive feature方式

个人更喜欢这种方式,代码看起来更简洁

2.1 添加依赖

cargo add clap --features derive

这里是不需要cargo的

2.2 快速开始

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Clis {
    /// Optional name to operate on
    name: Option<String>,

    /// Sets a custom config file
    #[arg(short, long, value_name = "FILE")]
    config: Option<PathBuf>,

    /// Turn debugging information on
    #[arg(short, long, action = clap::ArgAction::Count)]
    debug: u8,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// does testing things
    Test {
        /// lists test values
        #[arg(short, long)]
        list: bool,
    },
}
fn quick_test(){
    let cli = Clis::parse();

    if let Some(name) = cli.name.as_ref(){
        println!("value for name {name}")
    }

    if let Some(config_path) = cli.config.as_deref(){
        println!("value for config: {:?}",config_path)
    }

    match cli.debug {
        0 => println!("debug mode is off"),
        1=> println!("debug mode is kind of on"),
        2=>println!("debug mode is on"),
        _=> println!("dont be crazy")
    }


    match &cli.command {
        Some(Commands::Test {list})=>{
            if *list{
                println!("printing testing lists...")
            }else{
                println!("not printing testing lists...")
            }
        }
        None=>{

        }
    }
}

查看help

RUST_BACKTRACE=1 cargo run -- --help
   Compiling my_test v0.1.0 (/Users/zhangqiuli24/Desktop/rust/my_test)
    Finished dev [unoptimized + debuginfo] target(s) in 5.63s
     Running `target/debug/my_test --help`
Usage: my_test [OPTIONS] [NAME] [COMMAND]

Commands:
  test  does testing things
  help  Print this message or the help of the given subcommand(s)

Arguments:
  [NAME]  Optional name to operate on

Options:
  -c, --config <FILE>  Sets a custom config file
  -d, --debug...       Turn debugging information on
  -h, --help           Print help
  -V, --version        Print version

调用

RUST_BACKTRACE=1 cargo run -- name  -c ./config.toml -d    test --list  

value for name name
value for config: "./config.toml"
debug mode is kind of on
printing testing lists...

2.2 配置解析器

我们可以是 Parse 属性开启构建解析器

#[derive(Parser)]
#[command(name="MyApp")]
#[command(author="ZHangQL")]
#[command(version="1.0")]
#[command(about="这是测试的一些东西",long_about=None)]
struct Clic{
    #[arg(long)]
    two:String,
    #[arg(long)]
    one:String
}
fn parse(){
    let cli = Clic::parse();
    println!("value for two {:?}",cli.two);
    println!("value for one {:?}",cli.one)
}

help

RUST_BACKTRACE=1 cargo run -- --help                                         
   Compiling my_test v0.1.0 (/Users/zhangql/Desktop/rust/my_test)
    Finished dev [unoptimized + debuginfo] target(s) in 6.34s
     Running `target/debug/my_test --help`
这是测试的一些东西

Usage: my_test --two <TWO> --one <ONE>

Options:
      --two <TWO>  
      --one <ONE>  
  -h, --help       Print help
  -V, --version    Print version


cargo run -- -V
MyApp 1.0

我们也可使用使用 #[command(author, version, about)] 形式从 Cargo.toml 读取配置消息

读取的是cargo.toml的[package]信息

#[derive(Parser)]
#[command(author, version, about, long_about = None)] // Read from `Cargo.toml`
struct Cli {
    #[arg(long)]
    two: String,
    #[arg(long)]
    one: String,
}

fn main() {
    let cli = Cli::parse();

    println!("two: {:?}", cli.two);
    println!("one: {:?}", cli.one);
}

2.3 Command::next_line_help 换行

我们可以使用 #[command(next_line_help = true)] 方法替代 Command::next_line_help

RUST_BACKTRACE=1 cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.98s
     Running `target/debug/my_test --help`
Usage: my_test --two <TWO> --one <ONE>

Options:
      --two <TWO>
          
      --one <ONE>
          
  -h, --help
          Print help
  -V, --version
          Print version

cargo run -- -V
my_test 0.1.0

2.4 添加可选参数

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(next_line_help = true)]
struct CliW{
    name:Option<String>
}
fn switch(){
    let cli = CliW::parse();
    println!("value for name {:?}",cli.name);
}
cargo run -- --help
Usage: my_test [NAME]

Arguments:
  [NAME]
          

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


RUST_BACKTRACE=1 cargo run -- 
value for name None

RUST_BACKTRACE=1 cargo run -- kkk
value for name Some("kkk")

2.5 添加多值参数

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(next_line_help = true)]
struct CliW{
    name:Option<String>,
    more : Vec<String>
}
fn switch(){
    let cli = CliW::parse();
    println!("value for name {:?}",cli.name);
    println!("value for more {:?}",cli.more);
}
RUST_BACKTRACE=1 cargo run -- --help

Usage: my_test [NAME] [MORE]...

Arguments:
  [NAME]
          
  [MORE]...
          

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

RUST_BACKTRACE=1 cargo run -- kkk more1 more2 m3
value for name Some("kkk")
value for more ["more1", "more2", "m3"]

2.6 长短名

我们可以使用 #[arg(short = ‘n’)] 和 #[arg(long = “name”)] 属性设置参数的短名称和长名称

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    name: Option<String>,
}

fn main() {
    let cli = Cli::parse();

    println!("name: {:?}", cli.name.as_deref());
}

2.7 开启和关闭

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long)]
    verbose: bool,
}

fn main() {
    let cli = Cli::parse();

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

需要注意我们默认调用的是clap::ArgAction::SetTrue

2.8 参数计数

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
}

fn main() {
    let cli = Cli::parse();

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

2.9 参数默认值

我们使用 #[arg(default_value_t)] 属性来给参数设置默认值
use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[arg(default_value_t = 2020)]
    port: u16,
}

fn main() {
    let cli = Cli::parse();

    println!("port: {:?}", cli.port);
}

2.10 参数枚举

我们使用 #[arg(value_enum)] 设置参数枚举 结合枚举类

use clap::{Parser, ValueEnum};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// What mode to run the program in
    #[arg(value_enum)]
    mode: Mode,
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Mode {
    /// Run swiftly
    Fast,
    /// Crawl slowly but steadily
    ///
    /// This paragraph is ignored because there is no long help text for possible values.
    Slow,
}

fn main() {
    let cli = Cli::parse();

    match cli.mode {
        Mode::Fast => {
            println!("Hare");
        }
        Mode::Slow => {
            println!("Tortoise");
        }
    }
}

2.11 参数校验

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Network port to use
    #[arg(value_parser = clap::value_parser!(u16).range(1..))]
    port: u16,
}

fn main() {
    let cli = Cli::parse();

    println!("PORT = {}", cli.port);
}

2.12 自定义解析

use std::ops::RangeInclusive;

use clap::Parser;

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// Network port to use
    #[arg(value_parser = port_in_range)]
    port: u16,
}

fn main() {
    let cli = Cli::parse();

    println!("PORT = {}", cli.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()
        ))
    }
}

2.13 参数关系

use clap::{Args, Parser};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    #[command(flatten)]
    vers: Vers,

    /// some regular input
    #[arg(group = "input")]
    input_file: Option<String>,

    /// some special input argument
    #[arg(long, group = "input")]
    spec_in: Option<String>,

    #[arg(short, requires = "input")]
    config: Option<String>,
}

#[derive(Args)]
#[group(required = true, multiple = false)]
struct Vers {
    /// set version manually
    #[arg(long, value_name = "VER")]
    set_ver: Option<String>,

    /// auto inc major
    #[arg(long)]
    major: bool,

    /// auto inc minor
    #[arg(long)]
    minor: bool,

    /// auto inc patch
    #[arg(long)]
    patch: bool,
}

fn main() {
    let cli = Cli::parse();

    // 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 vers = &cli.vers;
    let version = if let Some(ver) = vers.set_ver.as_deref() {
        ver.to_string()
    } else {
        // Increment the one requested (in a real program, we'd reset the lower numbers)
        let (maj, min, pat) = (vers.major, vers.minor, vers.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 let Some(config) = cli.config.as_deref() {
        let input = cli
            .input_file
            .as_deref()
            .unwrap_or_else(|| cli.spec_in.as_deref().unwrap());
        println!("Doing work using input {input} and config {config}");
    }
}

2.14 自定义校验

use clap::error::ErrorKind;
use clap::{CommandFactory, Parser};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// set version manually
    #[arg(long, value_name = "VER")]
    set_ver: Option<String>,

    /// auto inc major
    #[arg(long)]
    major: bool,

    /// auto inc minor
    #[arg(long)]
    minor: bool,

    /// auto inc patch
    #[arg(long)]
    patch: bool,

    /// some regular input
    input_file: Option<String>,

    /// some special input argument
    #[arg(long)]
    spec_in: Option<String>,

    #[arg(short)]
    config: Option<String>,
}

fn main() {
    let cli = Cli::parse();

    // 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) = cli.set_ver.as_deref() {
        if cli.major || cli.minor || cli.patch {
            let mut cmd = Cli::command();
            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) = (cli.major, cli.minor, cli.patch);
        match (maj, min, pat) {
            (true, false, false) => major += 1,
            (false, true, false) => minor += 1,
            (false, false, true) => patch += 1,
            _ => {
                let mut cmd = Cli::command();
                cmd.error(
                    ErrorKind::ArgumentConflict,
                    "Can only modify one version field",
                )
                .exit();
            }
        };
        format!("{major}.{minor}.{patch}")
    };

    println!("Version: {version}");

    // Check for usage of -c
    if let Some(config) = cli.config.as_deref() {
        let input = cli
            .input_file
            .as_deref()
            // 'or' is preferred to 'or_else' here since `Option::as_deref` is 'const'
            .or(cli.spec_in.as_deref())
            .unwrap_or_else(|| {
                let mut cmd = Cli::command();
                cmd.error(
                    ErrorKind::MissingRequiredArgument,
                    "INPUT_FILE or --spec-in is required when using --config",
                )
                .exit()
            });
        println!("Doing work using input {input} and config {config}");
    }
}

2.15子命令

我们使用 #[command(subcommand)] 属性和#[derive(Subcommand)] 联合起来使用声明子命令。

use clap::{Parser, Subcommand};

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Adds files to myapp
    Add { name: Option<String> },
}

fn main() {
    let cli = Cli::parse();

    // You can check for the existence of subcommands, and if found use their
    // matches just as you would the top level cmd
    match &cli.command {
        Commands::Add { name } => {
            println!("'myapp add' was used, name is: {name:?}")
        }
    }
}

本文由mdnice多平台发布