diff options
author | Benjamin Nguyen <benjamin.van.nguyen@gmail.com> | 2023-06-28 11:46:05 +0700 |
---|---|---|
committer | Benjamin Nguyen <benjamin.van.nguyen@gmail.com> | 2023-06-28 11:46:05 +0700 |
commit | 76f57132b28316c8353f1d975779fc3bb339f327 (patch) | |
tree | 2a709aadaaba7d90ac0f0771f4ad968a43250526 | |
parent | cb3a5df076c1746206c783657f1d5a67185edbdd (diff) |
new trait to handle the reconciliation of arguments from CLI and config file
-rw-r--r-- | example/.erdtree.toml | 2 | ||||
-rw-r--r-- | src/context/args.rs | 128 | ||||
-rw-r--r-- | src/context/config/rc.rs | 2 | ||||
-rw-r--r-- | src/context/config/toml/mod.rs | 8 | ||||
-rw-r--r-- | src/context/error.rs | 5 | ||||
-rw-r--r-- | src/context/mod.rs | 97 | ||||
-rw-r--r-- | src/main.rs | 12 |
7 files changed, 158 insertions, 96 deletions
diff --git a/example/.erdtree.toml b/example/.erdtree.toml index 1a84959..006cb2a 100644 --- a/example/.erdtree.toml +++ b/example/.erdtree.toml @@ -18,6 +18,8 @@ human = true level = 1 suppress-size = true long = true +no-ignore = true +hidden = true # How many lines of Rust are in this code base? [rs] diff --git a/src/context/args.rs b/src/context/args.rs new file mode 100644 index 0000000..ac617b0 --- /dev/null +++ b/src/context/args.rs @@ -0,0 +1,128 @@ +use super::{config, error::Error, Context}; +use clap::{ + builder::ArgAction, parser::ValueSource, ArgMatches, Command, CommandFactory, FromArgMatches, +}; +use std::{ + ffi::{OsStr, OsString}, + path::PathBuf, +}; + +/// Allows the implementor to compute [`ArgMatches`] that reonciles arguments from both the +/// command-line as well as the config file that gets loaded. +pub trait Reconciler: CommandFactory + FromArgMatches { + + /// Loads in arguments from both the command-line as well as the config file and reconciles + /// identical arguments between the two using these rules: + /// + /// 1. If no config file is present, use arguments strictly from the command-line. + /// 2. If an argument was provided via the CLI then override the argument from the config. + /// 3. If an argument is sourced from its default value because a user didn't provide it via + /// the CLI, then select the argument from the config if it exists. + fn compute_args() -> Result<ArgMatches, Error> { + let cmd = Self::command().args_override_self(true); + + let user_args = Command::clone(&cmd).get_matches(); + + if user_args.get_one::<bool>("no_config").is_some_and(|b| *b) { + return Ok(user_args); + } + + let maybe_config_args = { + if let Some(rc) = load_rc_config_args() { + Some(rc) + } else { + let named_table = user_args.get_one::<String>("config"); + + load_toml_config_args(named_table.map(String::as_str))? + } + }; + + let Some(config_args) = maybe_config_args else { + return Ok(user_args); + }; + + let mut final_args = init_empty_args(); + + for arg in cmd.get_arguments() { + let arg_id = arg.get_id(); + let id_str = arg_id.as_str(); + + if id_str == "dir" { + if let Some(dir) = user_args.try_get_one::<PathBuf>(id_str)? { + final_args.push(OsString::from(dir)); + } + continue; + } + + let argument_source = user_args + .value_source(id_str) + .map_or(&config_args, |source| { + if matches!(source, ValueSource::CommandLine) { + &user_args + } else { + &config_args + } + }); + + let Some(key) = arg.get_long().map(|l| format!("--{l}")).map(OsString::from) else { + continue + }; + + match arg.get_action() { + ArgAction::SetTrue => { + if argument_source + .try_get_one::<bool>(id_str)? + .is_some_and(|b| *b) + { + final_args.push(key); + }; + } + ArgAction::SetFalse => continue, + _ => { + let Ok(Some(raw)) = argument_source.try_get_raw(id_str) else { + continue; + }; + final_args.push(key); + final_args.extend(raw.map(OsStr::to_os_string)); + } + } + } + + Ok(cmd.get_matches_from(final_args)) + } +} + +impl Reconciler for Context {} + +/// Creates a properly formatted `Vec<OsString>` that [`clap::Command`] would understand. +#[inline] +fn init_empty_args() -> Vec<OsString> { + vec![OsString::from("--")] +} + + +/// Loads an [`ArgMatches`] from `.erdtreerc`. +#[inline] +fn load_rc_config_args() -> Option<ArgMatches> { + if let Some(rc_config) = config::rc::read_config_to_string() { + let parsed_args = config::rc::parse(&rc_config); + let config_args = Context::command().get_matches_from(parsed_args); + + return Some(config_args); + } + + None +} + +/// Loads an [`ArgMatches`] from `.erdtree.toml`. +#[inline] +fn load_toml_config_args(named_table: Option<&str>) -> Result<Option<ArgMatches>, Error> { + if let Ok(toml_config) = config::toml::load() { + let parsed_args = config::toml::parse(toml_config, named_table)?; + let config_args = Context::command().get_matches_from(parsed_args); + + return Ok(Some(config_args)); + } + + Ok(None) +} diff --git a/src/context/config/rc.rs b/src/context/config/rc.rs index 7e230c9..27192b7 100644 --- a/src/context/config/rc.rs +++ b/src/context/config/rc.rs @@ -1,6 +1,6 @@ use std::{env, fs, path::PathBuf}; -/// Reads the config file into a `String` if there is one. When `None` is provided then the config +/// Reads the config file into a `String` if there is one, otherwise returns `None`. /// is looked for in the following locations in order: /// /// - `$ERDTREE_CONFIG_PATH` diff --git a/src/context/config/toml/mod.rs b/src/context/config/toml/mod.rs index 6660ebe..4e7dad6 100644 --- a/src/context/config/toml/mod.rs +++ b/src/context/config/toml/mod.rs @@ -24,13 +24,13 @@ enum ArgInstructions { } /// Takes in a `Config` that is generated from [`load`] returning a `Vec<OsString>` which -/// represents command-line arguments from `.erdtree.toml`. If a `nested_table` is provided then +/// represents command-line arguments from `.erdtree.toml`. If a `named_table` is provided then /// the top-level table in `.erdtree.toml` is ignored and the configurations specified in the -/// `nested_table` will be used instead. -pub fn parse(config: Config, nested_table: Option<&str>) -> Result<Vec<OsString>, Error> { +/// `named_table` will be used instead. +pub fn parse(config: Config, named_table: Option<&str>) -> Result<Vec<OsString>, Error> { let mut args_map = config.cache.into_table()?; - if let Some(table) = nested_table { + if let Some(table) = named_table { let new_conf = args_map .get(table) .and_then(|conf| conf.clone().into_table().ok()) diff --git a/src/context/error.rs b/src/context/error.rs index 056d9fb..3726b83 100644 --- a/src/context/error.rs +++ b/src/context/error.rs @@ -1,5 +1,5 @@ use super::config::toml::error::Error as TomlError; -use clap::Error as ClapError; +use clap::{parser::MatchesError, Error as ClapError}; use ignore::Error as IgnoreError; use regex::Error as RegexError; use std::convert::From; @@ -26,6 +26,9 @@ pub enum Error { #[error("{0}")] ConfigError(TomlError), + + #[error("{0}")] + MatchError(#[from] MatchesError), } impl From<TomlError> for Error { diff --git a/src/context/mod.rs b/src/context/mod.rs index c7e060a..b515f34 100644 --- a/src/context/mod.rs +++ b/src/context/mod.rs @@ -1,6 +1,7 @@ use super::disk_usage::{file_size::DiskUsage, units::PrefixKind}; use crate::tty; -use clap::{parser::ValueSource, ArgMatches, CommandFactory, FromArgMatches, Parser}; +use args::Reconciler; +use clap::{FromArgMatches, Parser}; use color::Coloring; use error::Error; use ignore::{ @@ -11,12 +12,15 @@ use regex::Regex; use std::{ borrow::Borrow, convert::From, - ffi::{OsStr, OsString}, num::NonZeroUsize, path::{Path, PathBuf}, thread::available_parallelism, }; +/// Concerned with figuring out how to reconcile arguments provided via the command-line with +/// arguments that come from a config file. +pub mod args; + /// Operations to load in defaults from configuration file. pub mod config; @@ -253,73 +257,8 @@ impl Context { /// Initializes [Context], optionally reading in the configuration file to override defaults. /// Arguments provided will take precedence over config. pub fn try_init() -> Result<Self, Error> { - // User-provided arguments from command-line. - let user_args = Self::command().args_override_self(true).get_matches(); - - // User provides `--no-config`. - if user_args.get_one::<bool>("no_config").is_some_and(|b| *b) { - return Self::from_arg_matches(&user_args).map_err(Error::ArgParse); - } - - // Load in `.erdtreerc` or `.erdtree.toml`. - let config_args = if let Some(config) = config::rc::read_config_to_string() { - let raw_args = config::rc::parse(&config); - - Self::command().get_matches_from(raw_args) - } else if let Ok(config) = config::toml::load() { - let named_table = user_args.get_one::<String>("config"); - let raw_args = config::toml::parse(config, named_table.map(String::as_str))?; - - Self::command().get_matches_from(raw_args) - } else { - return Self::from_arg_matches(&user_args).map_err(Error::ArgParse); - }; - - // If the user did not provide any arguments just read from config. - if !user_args.args_present() { - return Self::from_arg_matches(&config_args).map_err(Error::Config); - } - - let mut args = vec![OsString::from("--")]; - - let ids = Self::command() - .get_arguments() - .map(|arg| arg.get_id().clone()) - .collect::<Vec<_>>(); - - for id in ids { - let id_str = id.as_str(); - - if id_str == "dir" { - if let Ok(Some(dir)) = user_args.try_get_one::<PathBuf>(id_str) { - args.push(dir.as_os_str().to_owned()); - continue; - } - } - - let Some(source) = user_args.value_source(id_str) else { - if let Some(params) = Self::extract_args_from(id_str, &config_args) { - args.extend(params); - } - continue; - }; - - let higher_precedent = match source { - // User provided argument takes precedent over argument from config - ValueSource::CommandLine => &user_args, - - // otherwise prioritize argument from the config - _ => &config_args, - }; - - if let Some(params) = Self::extract_args_from(id_str, higher_precedent) { - args.extend(params); - } - } - - let clargs = Self::command().get_matches_from(args); - - Self::from_arg_matches(&clargs).map_err(Error::Config) + let args = Self::compute_args()?; + Self::from_arg_matches(&args).map_err(Error::Config) } /// Determines whether or not it's appropriate to display color in output based on @@ -373,26 +312,6 @@ impl Context { self.file_type.unwrap_or_default() } - /// Used to pick either from config or user args when constructing [Context]. - #[inline] - fn extract_args_from(id: &str, matches: &ArgMatches) -> Option<Vec<OsString>> { - let Ok(Some(raw)) = matches.try_get_raw(id) else { - return None - }; - - let kebap = format!("--{}", id.replace('_', "-")); - - let raw_args = raw - .map(OsStr::to_owned) - .map(|s| [OsString::from(&kebap), s]) - .filter(|[_key, val]| val != "false") - .flatten() - .filter(|s| s != "true") - .collect::<Vec<_>>(); - - Some(raw_args) - } - /// Predicate used for filtering via regular expressions and file-type. When matching regular /// files, directories will always be included since matched files will need to be bridged back /// to the root node somehow. Empty sets not producing an output is handled by [`Tree`]. diff --git a/src/main.rs b/src/main.rs index b999124..7b6176b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,7 +116,17 @@ fn run() -> Result<(), Box<dyn Error>> { progress.join_handle.join().unwrap()?; } - println!("{output}"); + #[cfg(debug_assertions)] + { + if std::env::var_os("ERDTREE_DEBUG").is_none() { + println!("{output}"); + } + } + + #[cfg(not(debug_assertions))] + { + println!("{output}"); + } Ok(()) } |