summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Nguyen <benjamin.van.nguyen@gmail.com>2023-06-28 11:46:05 +0700
committerBenjamin Nguyen <benjamin.van.nguyen@gmail.com>2023-06-28 11:46:05 +0700
commit76f57132b28316c8353f1d975779fc3bb339f327 (patch)
tree2a709aadaaba7d90ac0f0771f4ad968a43250526
parentcb3a5df076c1746206c783657f1d5a67185edbdd (diff)
new trait to handle the reconciliation of arguments from CLI and config file
-rw-r--r--example/.erdtree.toml2
-rw-r--r--src/context/args.rs128
-rw-r--r--src/context/config/rc.rs2
-rw-r--r--src/context/config/toml/mod.rs8
-rw-r--r--src/context/error.rs5
-rw-r--r--src/context/mod.rs97
-rw-r--r--src/main.rs12
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(())
}