Replies: 21 comments 50 replies
-
|
#2759's approach
|
Beta Was this translation helpful? Give feedback.
-
|
At one point, I would use a
|
Beta Was this translation helpful? Give feedback.
-
|
Recently, I've been using a
|
Beta Was this translation helpful? Give feedback.
-
|
There is also the popular config crate which is designed around layered config |
Beta Was this translation helpful? Give feedback.
-
|
Maintainer's notes:
Note: There's a lot of previous discussion on #748. Let's continue this discussion there? |
Beta Was this translation helpful? Give feedback.
-
|
For completeness, I'm going to mention argfile support. The formats currently supported by the argfile crate do not support comments and you can only specify it on the command line, you can't load it yourself to extend argv. This does have the benefit of allowing the user to control where the args are inserted. |
Beta Was this translation helpful? Give feedback.
-
|
Instead of a solution, this thread will be exploring requirements and their prioritization Various requirements include:
What additional requirements are there? Is there a viable subset that covers enough people for a shared solution? What building blocks can be made to help with all of this? |
Beta Was this translation helpful? Give feedback.
-
|
Problems with early error reporting:
|
Beta Was this translation helpful? Give feedback.
-
|
Apparently there are a few more options, but neither support Derive:
|
Beta Was this translation helpful? Give feedback.
-
|
Looking for feedback on the upcoming clap API design, in particular from a layered config perspective but also if people have thoughts in general See #3792 |
Beta Was this translation helpful? Give feedback.
-
|
After a 4-day diversion into a config system that involved becoming very familiar with the code for the twelf crate (some might call it a yak-shave), I have arrived at a fairly clear vision for the design of a a config layering feature that I personally would like to use. First a few observations:
Optional arguments aren’t enough to accomplish this however, because the user still needs to see the error if no layer was ultimately able to supply the argument value.
Most serde format crates have one or two line samples that provide an interface like this.
There are pros and cons to each. On the one hand, having to re-specify so many field attributes for Clap and Serde is a pain, and there is no struct if the Command builder API was used instead. But on the other hand, allowing the user to derive Deserialize independently gives a lot more control / flexibility. It sounds like pain either way. A third solution may be to side-step this concern altogether by providing a way to Deserialize directly to ArgMatches. So, for an API, I think an elegant design might be:
Finally, it could all be wrapped up in a convenience API that presents a nice “Layer” API. I am offering to prototype this if it sounds like a good design / direction to everyone. Although It will probably take me a few weeks / months to get back to this as I already lost too much time to config management and I’m behind on other work. |
Beta Was this translation helpful? Give feedback.
-
|
I think that if update_from_arg_matches() used use clap::{CommandFactory, FromArgMatches, Parser};
use serde::Deserialize;
#[derive(Debug, Parser, Deserialize)]
struct Opts {
#[clap(long)]
a: Option<String>,
#[serde(default)]
#[clap(long, default_value = "def")]
b: String,
#[clap(long)]
c: bool,
}
const CONFIG: &str = "
a: one
b: two
c: false
";
fn main() {
let matches = <Opts as CommandFactory>::command().get_matches();
let mut opts: Opts = serde_yaml::from_str(CONFIG).unwrap();
opts.update_from_arg_matches(&matches).unwrap();
dbg!(&opts);
}Basically creating an ArgMatches from the command-line args, parsing the config file into the Opts struct, and then updating it with the values from the command line if set. You'd probably have to keep the serde default value and the clap default value in sync though. |
Beta Was this translation helpful? Give feedback.
-
|
There is also the clap_serde_derive crate, which seems to offer a nice solution by creating a struct for your config that has everything wrapped in an I haven't used it or looked at the source due to the AGPL license, but if it handles subcommands properly it seems like a neat solution. |
Beta Was this translation helpful? Give feedback.
-
|
I want to express such a use case: use std::path::PathBuf;
use clap::Parser;
use figment::{
providers::{Env, Format, Serialized, Json},
Figment,
};
use serde::{Deserialize, Serialize};
fn conf_default_name() -> String {
"world".to_string()
}
#[derive(Parser, Debug, Serialize, Deserialize)]
struct Config {
/// A file to override default config.
#[clap(short, long, value_parser)]
config: Option<PathBuf>,
/// Name of the person to greet.
#[clap(short, long, value_parser, default_value_t = conf_default_name())]
#[serde(default = "conf_default_name")]
name: String,
}
fn main() {
// Create config builder.
let mut config: Figment = Figment::new();
// Parse CLI arguments.
let cli_args = Config::parse();
// Override with config file.
if let Some(config_path) = cli_args.config.clone() {
config = config.merge(Json::file(config_path));
}
// Override with `APP_`-prefixed environment variables and CLI args.
let config: Config = config
.merge(Env::prefixed("APP_"))
.merge(Serialized::defaults(cli_args))
.extract()
.unwrap();
println!("{:?}", config);
}In this case, I expect the override priority to be: cargo run -- --config config.jsonSome people may say that setting the |
Beta Was this translation helpful? Give feedback.
-
|
@luketpeterson did you ever start a prototype for this? If so I'd love to take a look. |
Beta Was this translation helpful? Give feedback.
-
|
For the first time in forever (literally) I write something that needs both command line input as well as config and environment and I end up here. A few thoughts on potential design;
Adding everything together I think the solution would look something like this (using owned datatypes and leaving out a lot of necessary generic-related stuff): #[derive(Parser, Debug)]
#[command(loader_t = foo)] // this is inherited by all values
struct Config {
#[arg(short, long, default_value = "::1", loader_key = "listen.address")] // specifying an explicit config key
address: IpAddr,
#[arg(short, long, default_value_t = 8080)] // uses its internal Id as key
port: u16,
#[arg(short, long, loader = secret_loader)] //
secret: String,
}
// this should be allowed to be associated with a struct of sorts to allow for caching keys instead of loading the file each time
// T here is the type, meaning `foo::<IpAddr>("listen.address".to_string(), None)` will be called if I don't provide an address myself
// The default value for clap is therefore invisible to the loader in this bit of design, which seems reasonable if the loader is instructed to just hand out a value if it has one (as with config files), but I can imagine there to be use-cases that need the loader to provide a value only when no default is provided (but I can't think of any)
// Then again the default value could also be a loader of sorts.
fn foo<T>(key: String, provided: Option<T>) -> Option<T> {
// the provided value is what clap parsed from the CLI, this allows precedence higher than CLI for some loaders
// others, like this one can short circuit
if provided.is_some() { return provided; }
let config = serde::from_file("my_config.toml");
if let Ok(Some(value)) = config.get(key) {
// this will make clap use this value instead of the (optional) provided one
return Some(value);
// note that we used `loader_t` (similar to `default_value_t`) above, which means this uses T directly
// there could be a `loader` one which returns string-like types which are then still parsed by clap, with error reporting from clap
// for that there should be some sort of additional context which the loader can provide, such that users get errors like:
// value "foo" from loader "TOML parser, file $f, line $n" is not valid for "port" of type u16
// (just more user friendly)
// (it could also integrate with Result to allow the loader to return something like a syntax error in a file via clap)
}
// when returning None, this will make clap panic if the value is required, same as with the CLI
None
}
// fn secret_loader() could query additional configuration files (such as encrypted secrets or a secret store a la Vault)Note that something about hierarchical structures is missing here, maybe something like With this kind of thing we can also have composite loaders which implement precedence like the twelf layers system. This would pull all of the complexity of parsing files (or making network connections for something like cloud-init) out of clap. This is just me, someone who writes Rust every other year or so for a project (or AoC), suggesting an API that may or may not even work when generics and lifetimes get involved. FWIW, right now clap sort of has a loader that goes like this: fn loader(...) {
if let Some(v) = provided { return v; }
if let Some(v) = getenv(format!("{}{}", env_prefix, key.upcase()) { return v; }
if let Some(v) = default_value { return v; }
None
}And I think factoring that into a separate bit of code which can be replaced (or augmented, since one loader could just chain another like warp filters do) by another crate (on user request) would be ideal. Aight, that's all I have on my mind right now, hopefully this pushes anything or anyone in a good direction ^^ |
Beta Was this translation helpful? Give feedback.
-
|
My use case for this feature is that I have a very large Parameters struct that takes many many parameters to configure a model. Because the struct is so large (and there are no obvious defaults), I definitely need support for a configuration file. However only having a configuration file without clap would mean that there is no obvious place to put documentation for each parameter (I currently have it in the conf file itself). Integrating both would make for a much more ergonomic user experience. |
Beta Was this translation helpful? Give feedback.
-
|
Some config libraries that use a partial/layered approach can support this with a single struct that derives both the config library's macros and clap's macros without introducing any coupling at the library level. I like The basic strategy is to derive your configuration plus This works with use clap::{Args, Parser};
use schematic::Config;
#[derive(Config, Debug)]
#[config(partial(derive(Args)))]
pub struct AppConfig {
/// app port
#[setting(default = 3000, partial(arg(long, env = "APP_PORT")))]
port: usize,
/// allowed hosts
#[setting(
default = vec!["localhost".to_string()],
partial(arg(long, env = "APP_HOSTS")))]
allowed_hosts: Vec<String>,
}
#[derive(Parser, Debug)]
struct Cli {
// Note: PartialAppConfig is a struct generated by schematic that has all types wrapped in Option
// The attributes wrapped in partial() are applied to this struct
#[command(flatten)]
app_config: PartialAppConfig,
...
}The values in Here's the same approach using use clap::{Args, Parser};
use confique::Config;
#[derive(Config, Debug)]
#[config(partial_attr(derive(Args, Debug)))]
pub struct AppConfig {
/// app port
#[config(default = 3000, partial_attr(arg(long)))]
port: usize,
/// allowed hosts
#[config(default = ["localhost"], partial_attr(arg(long)))]
allowed_hosts: Vec<String>,
}
#[derive(Parser, Debug)]
struct Cli {
#[command(flatten)]
config: PartialAppConfig,
...
} |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for doing those PRs @aschey. Kind of surprised there isn't one blessed unified thing for this yet. I'm testing out the schematic one and I noticed one thing The default value doesn't get passed to clap. I think? I didn't look at the code, just testing experimentially. The reason I'd want that is clap adds it to the automatic help. I had to add "3000" twice to get it to print. #[setting(default = 3000, partial(arg(long, default_value = "3000")), env = "SOME_PORT")]
port: usize #[setting(default = 3000, partial(arg(long)), env = "SOME_PORT")]
port: usizeI imagine this being hard to figure out. |
Beta Was this translation helpful? Give feedback.
-
|
I wanted an approach which:
I hacked something up which appears to work. It uses
/// A registrar for a module's configuration.
pub trait ConfigRegistrar {
/// The name of the module, used as a key in config files and for namespacing.
fn name(&self) -> &'static str;
/// A function that adds this module's command-line arguments to the main `clap` command.
fn add_args(&self, cmd: Command) -> Command;
/// A function to extract this module's config from the final Figment
/// and initialize the module's static config.
fn initalize_args(
&self, matches_without_defaults: &ArgMatches, matches_without_defaults: &ArgMatches,
);
}
#[distributed_slice]
pub static CONFIG_REGISTRARS: [&'static (dyn ConfigRegistrar + Sync)];
/// Parses the configuration for the application based on the command-line arguments.
///
/// # Panics
///
/// - Panics if any operation related to clearing default values from `matches_without_defaults`
/// fails unexpectedly, such as attempting to clear a non-existent argument.
pub fn parse_config() {
let mut command = Command::new(constants::BINARY_NAME)
.ignore_errors(false) // TODO: parameterize this
.about(constants::ABOUT)
.version(constants::BINARY_VERSION_CORE)
.long_version(constants::VERSION_STRING_LONG.as_str());
for registrar in CONFIG_REGISTRARS {
command = registrar.add_args(command);
}
let matches = command.get_matches();
let mut matches_without_defaults = matches.clone();
for arg_id in matches.ids() {
let key = arg_id.as_str();
if matches.value_source(key).unwrap() == ValueSource::DefaultValue {
matches_without_defaults.try_clear_id(key).unwrap();
}
}
for registrar in CONFIG_REGISTRARS {
registrar.initalize_args(&matches, &matches_without_defaults);
}
}pub mod prelude {
pub use clap::{ArgMatches, Args, Command, FromArgMatches};
pub use figment::{
Figment,
providers::{Env, Format, Serialized, Toml},
};
pub use serde::{Deserialize, Serialize};
}
/// Implements `ConfigRegistrar` for a given struct and registers it
/// in the `CONFIG_REGISTRARS` distributed slice.
#[macro_export]
macro_rules! register_config {
($struct_type:ty, $config_name:literal, $config_static_name:ident) => {
struct ConfigRegistrarImpl;
impl $crate::config::ConfigRegistrar for ConfigRegistrarImpl {
fn name(&self) -> &'static str {
$config_name
}
fn add_args(&self, app: $crate::prelude::Command) -> $crate::prelude::Command {
<$struct_type as $crate::prelude::Args>::augment_args(app)
}
fn initalize_args(
&self, matches: &$crate::prelude::ArgMatches,
matches_without_defaults: &$crate::prelude::ArgMatches,
) {
use $crate::prelude::FromArgMatches;
let figment = $crate::prelude::Figment::new()
.merge($crate::prelude::Serialized::defaults(
<$struct_type>::from_arg_matches(matches).unwrap(),
))
.merge($crate::config::config_toml())
.merge($crate::config::config_env());
let mut config: $struct_type = figment.extract().unwrap();
config.update_from_arg_matches(matches_without_defaults).unwrap();
$config_static_name.set(config).unwrap();
}
}
static $config_static_name: std::sync::OnceLock<$struct_type> = std::sync::OnceLock::new();
#[linkme::distributed_slice($crate::config::CONFIG_REGISTRARS)]
static CONFIG_REGISTRAR: &(dyn $crate::config::ConfigRegistrar + Sync) =
&ConfigRegistrarImpl;
};
}#[cfg(test)]
mod tests {
use clap::Args;
use serde::{Deserialize, Serialize};
use tracing_test::traced_test;
use super::*;
use crate::register_config;
#[derive(Serialize, Deserialize, Debug, Args)]
pub struct Config {
#[arg(
long = "message",
env = "APP_MODULE_A_MESSAGE",
default_value = "foo",
env = "FOO_ENV"
)]
pub message: String,
#[arg(long = "retries", default_value_t = 3)]
pub retries: u32,
}
register_config!(Config, "unit_test", CONFIG);
#[test]
#[traced_test(level = "info")]
fn test_run_logic() {
parse_config();
println!("{}", CONFIG.get().unwrap().message.as_str());
}
}One downside to the above code is that the business logic of merging is defined in the macro. This isn't convenient, and requires all modules to be rebuilt on any change to that logic. This is lazyness on my part due to the |
Beta Was this translation helpful? Give feedback.
-
|
After coming across this thread, I managed to come up with an example that uses the derive method in combination with figment to load defaults -> toml file -> clap cli arguments in that order |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
This is inspired by #2759 in which there is a proposal for one approach for layering. Thought the actual layering best practices would best be split out in a discussion
Beta Was this translation helpful? Give feedback.
All reactions