我正在使用 clap 编写 CLI 来解析我的论点。我想为选项提供默认值,但如果有配置文件,配置文件应该胜过默认值。
将命令行参数优先于默认值很容易,但我想要的优先顺序是:
如果配置文件不是由命令行选项设置的,设置它也很容易,只需在运行之前解析配置文件
parse_args
,并将解析的配置文件中的值提供给default_value
。问题是,如果您在命令行中指定配置文件,则只有在解析之后才能更改默认值。
我能想到的唯一方法是不设置
default_value
,然后在""
中手动匹配value_of
。问题是,在那种情况下,clap 将无法构建有用的--help
.
有没有办法让 clap 读取配置文件本身?
对于受益于派生宏的 clap v3 或 clap v4 用户,我解决了这个问题,制作了两个结构:一个是目标结构,另一个是相同的,但所有字段都是可选的。我用 serde 从配置文件和命令行用 clap 解析第二个结构,然后这些结构可以合并到第一个结构中:配置/命令行参数中不存在 None 的元素。
为了促进这一点,我创建了一个派生宏(ClapSerde),它自动:
// 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 = "*"
cargo 上已经有许多箱子旨在实现类似的结果(例如 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)
};
}
来自 clap 在
default_value
上的文档:
注意:如果用户在运行时不使用此参数,
仍将返回 true。如果您想确定参数是否在运行时使用,请考虑ArgMatches::is_present
如果参数在运行时未使用,它将返回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/
导致行为:
./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
我的解决方案是使用 clap(版本 4.2.1)+ confy(版本 0.5.1)。
“confy 负责在读写配置之前找出特定于操作系统和环境的路径。”
此解决方案不需要在命令行上指定配置文件。
配置文件将自动生成,并与主程序同名。
我用以下文件创建了一个名为“make_args”的程序:
我的 Cargo.toml:
[package]
name = "make_args"
version = "0.1.0"
edition = "2021"
[dependencies]
confy = "0.5"
serde_derive = "1"
serde = { version = "1", features = [ "derive" ] }
clap = { version = "4", features = [
"derive",
"color",
"env",
"help",
] }
主要.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_derive::{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(Parser, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[command(author, version, about, long_about = None, next_line_help = true)] // Read from `Cargo.toml`
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>,
}
/// confy needs to implement the default Arguments.
impl default::Default for Arguments {
fn default() -> Self {
Arguments {
file1: None,
file2: None,
delimiter1: Some(';'),
delimiter2: Some(';'),
}
}
}
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);
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)
}
// This function can be omitted.
/// Print configuration file
fn print_config_file (self, app_name: &str) -> Result<Self, Box<dyn Error>> {
println!("Configuration file: {:?}", confy::get_configuration_file_path(app_name, None)?);
let arguments = self.clone();
if let Some(file1) = arguments.file1 {
println!("\tfile1: {file1:#?}");
}
if let Some(file2) = arguments.file2 {
println!("\tfile2: {file2:#?}");
}
if let Some(delimiter1) = arguments.delimiter1 {
println!("\tdelimiter1: {delimiter1:#?}");
}
if let Some(delimiter2) = arguments.delimiter2 {
println!("\tdelimiter2: {delimiter2:#?}\n");
}
Ok(self)
}
}
在没有 args 的情况下运行 cargo 后,输出为:
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'.
请注意,“必需”选项可以更改为“真”或“假”。
#[arg(short('1'), long, required = true)]
并使用一些参数运行 cargo,输出为:
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: ';'