有没有办法让 clap 使用文件中的默认值?

pic*_*ame 10 rust clap

我正在使用 clap 编写 CLI解析我的参数。我想提供选项的默认值,但如果有配置文件,则配置文件应该胜过默认值。

将命令行参数的优先级设置为默认值很容易,但我想要的优先级顺序为:

  1. 命令行参数
  2. 配置文件
  3. 默认值

如果配置文件不是通过命令行选项设置的,设置它也很容易,只需在运行之前解析配置文件parse_args,并将解析的配置文件中的值提供到default_value. 问题是,如果您在命令行中指定配置文件,则在解析之后才能更改默认值。

我能想到的唯一方法是不设置 adefault_value然后手动""匹配value_of. 问题是在这种情况下, clap 将无法构建有用的--help.

有没有办法让 clap 读取配置文件本身?

DPD*_*PD- 8

对于 clap v3 或 clap v4 的用户来说,这将受益于派生宏,我解决了这个问题,制作了两个结构:一个是目标结构,另一个是相同的,但所有字段都是可选的。我使用配置文件中的 serde 和使用 clap 的命令行解析第二个结构,然后可以将结构合并到第一个结构中:配置/命令行参数中不存在 None 元素。

为了促进这一点,我创建了一个派生宏(ClapSerde),它会自动:

  • 创建具有可选字段的结构
  • 派生出 clap Parser 和 serde 对其进行反序列化
  • 提供从 clap 和带有可选字段的反序列化(使用 serde)结构合并到目标结构中的方法;这可以用于创建分层配置解析器,即针对请求的情况
// Priority:
// 1. command line arguments (clap)
// 2. config file (serde)
// 3. defaults
Args::from(serde_parsed)
    .merge_clap();
Run Code Online (Sandbox Code Playgroud)
  • 在目标函数上实现默认值(可能具有自定义值),当两个层都没有该字段的值时将使用该默认值。

例子:

use clap_serde_derive::{
    clap::{self, ArgAction},
    serde::Serialize,
    ClapSerde,
};

#[derive(ClapSerde, Serialize)]
#[derive(Debug)]
#[command(author, version, about)]
pub struct Args {
    /// Input files
    pub input: Vec<std::path::PathBuf>,

    /// String argument
    #[arg(short, long)]
    name: String,

    /// Skip serde deserialize
    #[default(13)]
    #[serde(skip_deserializing)]
    #[arg(long = "num")]
    pub clap_num: u32,

    /// Skip clap
    #[serde(rename = "number")]
    #[arg(skip)]
    pub serde_num: u32,

    /// Recursive fields
    #[clap_serde]
    #[command(flatten)]
    pub suboptions: SubConfig,
}

#[derive(ClapSerde, Serialize)]
#[derive(Debug)]
pub struct SubConfig {
    #[default(true)]
    #[arg(long = "no-flag", action = ArgAction::SetFalse)]
    pub flag: bool,
}

fn main() {
    let args = Args::from(serde_yaml::from_str::<<Args as ClapSerde>::Opt>("number: 12").unwrap())
        .merge_clap();
    println!("{:?}", args);
}
Run Code Online (Sandbox Code Playgroud)

请注意,上述内容需要以下内容Cargo.toml

[dependencies]
clap = "*"
serde = "*"
serde_yaml = "*"
clap-serde-derive = "*"
Run Code Online (Sandbox Code Playgroud)

Cargo 上已经有很多 crate 旨在实现类似的结果(例如 viperus、twelf、layeredconf),但它们使用旧版本的 clap 而不需要导出和/或没有办法为 clap 和 serde 定义唯一的默认值。
我希望这个派生宏会有用。

更新

您可以通过这种方式轻松地从命令行获取配置文件路径。

use std::{fs::File, io::BufReader};

use clap_serde_derive::{
    clap::{self, Parser},
    ClapSerde,
};

#[derive(Parser)]
#[clap(author, version, about)]
struct Args {
    /// Input files
    input: Vec<std::path::PathBuf>,

    /// Config file
    #[clap(short, long = "config", default_value = "config.yml")]
    config_path: std::path::PathBuf,

    /// Rest of arguments
    #[clap(flatten)]
    pub config: <Config as ClapSerde>::Opt,
}

#[derive(ClapSerde)]
struct Config {
    /// String argument
    #[clap(short, long)]
    name: String,
}

fn main() {
    // Parse whole args with clap
    let mut args = Args::parse();

    // Get config file
    let config = if let Ok(f) = File::open(&args.config_path) {
        // Parse config with serde
        match serde_yaml::from_reader::<_, <Config as ClapSerde>::Opt>(BufReader::new(f)) {
            // merge config already parsed from clap
            Ok(config) => Config::from(config).merge(&mut args.config),
            Err(err) => panic!("Error in configuration file:\n{}", err),
        }
    } else {
        // If there is not config file return only config parsed from clap
        Config::from(&mut args.config)
    };
}
Run Code Online (Sandbox Code Playgroud)


小智 4

来自 clap 的文档default_value

注意:如果用户在运行时不使用此参数ArgMatches::is_present仍将返回 true。如果您希望确定该参数是否在运行时使用,请考虑如果该参数在运行时未使用则ArgMatches::occurrences_of返回哪个。0

https://docs.rs/clap/2.32.0/clap/struct.Arg.html#method.default_value

这可以用来获得您所描述的行为:

extern crate clap;
use clap::{App, Arg};
use std::fs::File;
use std::io::prelude::*;

fn main() {
    let matches = App::new("MyApp")
        .version("0.1.0")
        .about("Example for StackOverflow")
        .arg(
            Arg::with_name("config")
                .short("c")
                .long("config")
                .value_name("FILE")
                .help("Sets a custom config file"),
        )
        .arg(
            Arg::with_name("example")
                .short("e")
                .long("example")
                .help("Sets an example parameter")
                .default_value("default_value")
                .takes_value(true),
        )
        .get_matches();

    let mut value = String::new();

    if let Some(c) = matches.value_of("config") {
        let file = File::open(c);
        match file {
            Ok(mut f) => {
                // Note: I have a file `config.txt` that has contents `file_value`
                f.read_to_string(&mut value).expect("Error reading value");
            }
            Err(_) => println!("Error reading file"),
        }

        // Note: this lets us override the config file value with the
        // cli argument, if provided
        if matches.occurrences_of("example") > 0 {
            value = matches.value_of("example").unwrap().to_string();
        }
    } else {
        value = matches.value_of("example").unwrap().to_string();
    }

    println!("Value for config: {}", value);
}

// Code above licensed CC0
// https://creativecommons.org/share-your-work/public-domain/cc0/ 
Run Code Online (Sandbox Code Playgroud)

导致行为:

./target/debug/example
Value for config: default_value
./target/debug/example --example cli_value
Value for config: cli_value
./target/debug/example --config config.txt
Value for config: file_value
./target/debug/example --example cli_value --config config.txt
Value for config: cli_value
Run Code Online (Sandbox Code Playgroud)