是否有一种方法可以使拍手从文件中使用默认值



我正在使用拍手来解析我的参数。我想提供选项的默认值,但是如果有一个配置文件,则配置文件应赢得默认值。

很容易将命令行参数优先于默认值,但我希望优先顺序:

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

如果命令行选项未设置配置文件,则仅通过在运行parse_args之前解析配置文件并将解析的配置文件中的值分析到default_value中也很容易设置。问题是,如果您在命令行中指定配置文件,则无法在解析后更改默认值。

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

有没有办法拍手读取配置文件本身?

对于拍手V3或拍手V4的用户,这将受益于衍生的宏我解决了这个问题,这是两个结构:一个是目标结构,另一个是相同的,但所有字段都可选。我从配置文件和命令行中解析了第二个结构。

为了促进这一点,我创建了一个衍生的宏(clapserde),该宏(clapserde)自动:

  • 使用可选字段创建结构
  • 衍生出拍手解析器和Serde在其上进行了挑选
  • 提供了从拍手和从具有可选字段的选序(与Serde)结构合并到目标结构中的方法;这可以用于创建分层配置解析器,即为请求的情况
// Priority:
// 1. command line arguments (clap)
// 2. config file (serde)
// 3. defaults
Args::from(serde_parsed)
    .merge_clap();
  • 在目标函数上实现默认值(带有可能的自定义值),当两层都没有字段的值时,将使用它。

示例:

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

请注意以上需要Cargo.toml中的以下内容:

[dependencies]
clap = "*"
serde = "*"
serde_yaml = "*"
clap-serde-derive = "*"

货物上已经有很多板条箱,旨在取得相似的结果(例如Viperus,Twelf,LayeredConf),但它们使用旧版本的拍手,而无需衍生和/或没有一种方法来定义拍手和Serde的独特默认值。
我希望这个衍生的宏观有用。

update

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

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

来自default_value上的Clap文档:

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

https://docs.rs/clap/2.32.0/clap/sstruct.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/ 

导致行为:

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

我的解决方案是使用拍手(版本4.2.1) condy(版本0.5.1)。
&quot" condy在阅读和编写配置之前,请注意特定于操作系统和环境路径。

此解决方案不需要在命令行上指定配置文件。
配置文件将自动生成,并具有与主程序相同的名称。

我创建了一个名为" make_args"的程序,其中包括以下文件:

我的货物:

[package]
name = "make_args"
version = "0.1.0"
edition = "2021"
[dependencies]
confy = "0.5"
toml = "0.7"
serde_derive = "1"
serde = { version = "1", features = [ "derive" ] }
clap = { version = "4", features = [
    "derive",
    "color",
    "env",
    "help",
] }

main.rs:

use std::error::Error;
mod args;
use args::Arguments;
fn main() -> Result<(), Box<dyn Error>> {
    let _args: Arguments = Arguments::build()?;
    Ok(())
}

和模块args.rs:

use serde::{Serialize, Deserialize};
use clap::{Parser, CommandFactory, Command};
use std::{
    default,
    error::Error,
    path::PathBuf,
};
 
/// Read command line arguments with priority order:
/// 1. command line arguments
/// 2. environment
/// 3. config file
/// 4. defaults
///
/// At the end add or update config file.
/// 
#[derive(Debug, Clone, PartialEq, Parser, Serialize, Deserialize)]
#[command(author, version, about, long_about = None, next_line_help = true)]
pub struct Arguments {
    /// The first file with CSV format.
    #[arg(short('1'), long, required = true)]
    pub file1: Option<PathBuf>,
    /// The second file with CSV format.
    #[arg(short('2'), long, required = true)]
    pub file2: Option<PathBuf>,
    /// Optionally, enter the delimiter for the first file.
    /// The default delimiter is ';'.
    #[arg(short('a'), long, env("DELIMITER_FILE1"), required = false)]
    pub delimiter1: Option<char>,
    /// Optionally, enter the delimiter for the second file.
    /// The default delimiter is ';'.
    #[arg(short('b'), long, env("DELIMITER_FILE2"), required = false)]
    pub delimiter2: Option<char>,
    /// Print additional information in the terminal
    #[arg(short('v'), long, required = false)]
    verbose: Option<bool>,
}
/// confy needs to implement the default Arguments.
impl default::Default for Arguments {
    fn default() -> Self {
        Arguments {
            file1: None, 
            file2: None, 
            delimiter1: Some(';'), 
            delimiter2: Some(';'),  
            verbose: Some(true),
        }
    }
}
impl Arguments {
    /// Build Arguments struct
    pub fn build() -> Result<Self, Box<dyn Error>> {
        let app: Command = Arguments::command();
        let app_name: &str = app.get_name();
        let args: Arguments = Arguments::parse()
            .get_config_file(app_name)?
            .set_config_file(app_name)?
            .print_config_file(app_name)?;
        Ok(args)
    }
    /// Get configuration file.
    /// A new configuration file is created with default values if none exists.
    fn get_config_file(mut self, app_name: &str) -> Result<Self, Box<dyn Error>> {
        let config_file: Arguments = confy::load(app_name, None)?;
        self.file1 = self.file1.or(config_file.file1);
        self.file2 = self.file2.or(config_file.file2);
        self.delimiter1 = self.delimiter1.or(config_file.delimiter1);
        self.delimiter2 = self.delimiter2.or(config_file.delimiter2);
        self.verbose = self.verbose.or(config_file.verbose);
        Ok(self)
    }
    /// Save changes made to a configuration object
    fn set_config_file(self, app_name: &str) -> Result<Self, Box<dyn Error>> {
        confy::store(app_name, None, self.clone())?;
        Ok(self)
    }
    /// Print configuration file path and its contents
    fn print_config_file (self, app_name: &str) -> Result<Self, Box<dyn Error>> {
        if self.verbose.unwrap_or(true) {
            let file_path: PathBuf = confy::get_configuration_file_path(app_name, None)?;
            println!("Configuration file: '{}'", file_path.display());
            let toml: String = toml::to_string_pretty(&self)?;
            println!("t{}", toml.replace('n', "nt"));
        }
        Ok(self)
    }
}

无需ARG运行货物后,输出为:

cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/make_args`
error: the following required arguments were not provided:
  --file1 <FILE1>
  --file2 <FILE2>
Usage: make_args --file1 <FILE1> --file2 <FILE2>
For more information, try '--help'.

请注意,"必需"选项可以更改为" true"或" false"。

#[arg(short('1'), long, required = true)]

和带有一些参数的运行货物,输出为:

cargo run -- -1 /tmp/file1.csv -2 /tmp/file2.csv 
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/make_args -1 /tmp/file1.csv -2 /tmp/file2.csv`
Configuration file: '/home/claudio/.config/make_args/default-config.toml'
    file1 = "/tmp/file1.csv"
    file2 = "/tmp/file2.csv"
    delimiter1 = ";"
    delimiter2 = ";"
    verbose = true

最新更新