My solution was to use clap (version 4.2.1) + confy (version 0.5.1).
"confy takes care of figuring out operating system specific and environment paths before reading and writing a configuration."
This solution doesn't need to specify the config file on the command line.
The configuration file will be generated automatically and will have the same name as the main program.
I created a program called 'make_args' with the following files:
My Cargo.toml:
[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",
] }
The main.rs:
use std::error::Error;
mod args;
use args::Arguments;
fn main() -> Result<(), Box<dyn Error>> {
let _args: Arguments = Arguments::build()?;
Ok(())
}
And the module 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', "\n\t"));
}
Ok(self)
}
}
After running cargo without args, the output was:
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'.
Note that the 'required' option can be changed to 'true' or 'false'.
#[arg(short('1'), long, required = true)]
And running cargo with some arguments, the output was:
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