Skip to content

Commit b711846

Browse files
authored
Merge pull request #209 from solidiquis/config-hacks-begone
Argument reconciliation trait
2 parents 5e0e229 + 0966782 commit b711846

File tree

17 files changed

+200
-139
lines changed

17 files changed

+200
-139
lines changed

example/.erdtree.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ human = true
1818
level = 1
1919
suppress-size = true
2020
long = true
21+
no-ignore = true
22+
hidden = true
2123

2224
# How many lines of Rust are in this code base?
2325
[rs]

rustfmt.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
match_block_trailing_comma = true

src/context/args.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use super::{config, error::Error, Context};
2+
use clap::{
3+
builder::ArgAction, parser::ValueSource, ArgMatches, Command, CommandFactory, FromArgMatches,
4+
};
5+
use std::{
6+
ffi::{OsStr, OsString},
7+
path::PathBuf,
8+
};
9+
10+
/// Allows the implementor to compute [`ArgMatches`] that reconciles arguments from both the
11+
/// command-line as well as the config file that gets loaded.
12+
pub trait Reconciler: CommandFactory + FromArgMatches {
13+
/// Loads in arguments from both the command-line as well as the config file and reconciles
14+
/// identical arguments between the two using these rules:
15+
///
16+
/// 1. If no config file is present, use arguments strictly from the command-line.
17+
/// 2. If an argument was provided via the CLI then override the argument from the config.
18+
/// 3. If an argument is sourced from its default value because a user didn't provide it via
19+
/// the CLI, then select the argument from the config if it exists.
20+
fn compute_args() -> Result<ArgMatches, Error> {
21+
let cmd = Self::command().args_override_self(true);
22+
23+
let user_args = Command::clone(&cmd).get_matches();
24+
25+
if user_args.get_one::<bool>("no_config").is_some_and(|b| *b) {
26+
return Ok(user_args);
27+
}
28+
29+
let maybe_config_args = {
30+
if let Some(rc) = load_rc_config_args() {
31+
Some(rc)
32+
} else {
33+
let named_table = user_args.get_one::<String>("config");
34+
35+
load_toml_config_args(named_table.map(String::as_str))?
36+
}
37+
};
38+
39+
let Some(config_args) = maybe_config_args else {
40+
return Ok(user_args);
41+
};
42+
43+
let mut final_args = init_empty_args();
44+
45+
for arg in cmd.get_arguments() {
46+
let arg_id = arg.get_id();
47+
let id_str = arg_id.as_str();
48+
49+
if id_str == "dir" {
50+
if let Some(dir) = user_args.try_get_one::<PathBuf>(id_str)? {
51+
final_args.push(OsString::from(dir));
52+
}
53+
continue;
54+
}
55+
56+
let argument_source = user_args
57+
.value_source(id_str)
58+
.map_or(&config_args, |source| {
59+
if matches!(source, ValueSource::CommandLine) {
60+
&user_args
61+
} else {
62+
&config_args
63+
}
64+
});
65+
66+
let Some(key) = arg.get_long().map(|l| format!("--{l}")).map(OsString::from) else {
67+
continue
68+
};
69+
70+
match arg.get_action() {
71+
ArgAction::SetTrue => {
72+
if argument_source
73+
.try_get_one::<bool>(id_str)?
74+
.is_some_and(|b| *b)
75+
{
76+
final_args.push(key);
77+
};
78+
},
79+
ArgAction::SetFalse => continue,
80+
_ => {
81+
let Ok(Some(raw)) = argument_source.try_get_raw(id_str) else {
82+
continue;
83+
};
84+
final_args.push(key);
85+
final_args.extend(raw.map(OsStr::to_os_string));
86+
},
87+
}
88+
}
89+
90+
Ok(cmd.get_matches_from(final_args))
91+
}
92+
}
93+
94+
impl Reconciler for Context {}
95+
96+
/// Creates a properly formatted `Vec<OsString>` that [`clap::Command`] would understand.
97+
#[inline]
98+
fn init_empty_args() -> Vec<OsString> {
99+
vec![OsString::from("--")]
100+
}
101+
102+
/// Loads an [`ArgMatches`] from `.erdtreerc`.
103+
#[inline]
104+
fn load_rc_config_args() -> Option<ArgMatches> {
105+
if let Some(rc_config) = config::rc::read_config_to_string() {
106+
let parsed_args = config::rc::parse(&rc_config);
107+
let config_args = Context::command().get_matches_from(parsed_args);
108+
109+
return Some(config_args);
110+
}
111+
112+
None
113+
}
114+
115+
/// Loads an [`ArgMatches`] from `.erdtree.toml`.
116+
#[inline]
117+
fn load_toml_config_args(named_table: Option<&str>) -> Result<Option<ArgMatches>, Error> {
118+
if let Ok(toml_config) = config::toml::load() {
119+
let parsed_args = config::toml::parse(toml_config, named_table)?;
120+
let config_args = Context::command().get_matches_from(parsed_args);
121+
122+
return Ok(Some(config_args));
123+
}
124+
125+
Ok(None)
126+
}

src/context/config/rc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{env, fs, path::PathBuf};
22

3-
/// Reads the config file into a `String` if there is one. When `None` is provided then the config
3+
/// Reads the config file into a `String` if there is one, otherwise returns `None`.
44
/// is looked for in the following locations in order:
55
///
66
/// - `$ERDTREE_CONFIG_PATH`

src/context/config/toml/mod.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ enum ArgInstructions {
2424
}
2525

2626
/// Takes in a `Config` that is generated from [`load`] returning a `Vec<OsString>` which
27-
/// represents command-line arguments from `.erdtree.toml`. If a `nested_table` is provided then
27+
/// represents command-line arguments from `.erdtree.toml`. If a `named_table` is provided then
2828
/// the top-level table in `.erdtree.toml` is ignored and the configurations specified in the
29-
/// `nested_table` will be used instead.
30-
pub fn parse(config: Config, nested_table: Option<&str>) -> Result<Vec<OsString>, Error> {
29+
/// `named_table` will be used instead.
30+
pub fn parse(config: Config, named_table: Option<&str>) -> Result<Vec<OsString>, Error> {
3131
let mut args_map = config.cache.into_table()?;
3232

33-
if let Some(table) = nested_table {
33+
if let Some(table) = named_table {
3434
let new_conf = args_map
3535
.get(table)
3636
.and_then(|conf| conf.clone().into_table().ok())
@@ -51,12 +51,12 @@ pub fn parse(config: Config, nested_table: Option<&str>) -> Result<Vec<OsString>
5151
let fmt_key = process_key(k);
5252
parsed_args.push(fmt_key);
5353
parsed_args.push(parsed_value);
54-
}
54+
},
5555

5656
ArgInstructions::PushKeyOnly => {
5757
let fmt_key = process_key(k);
5858
parsed_args.push(fmt_key);
59-
}
59+
},
6060

6161
ArgInstructions::Pass => continue,
6262
}
@@ -110,7 +110,7 @@ fn parse_argument(keyword: &str, arg: &Value) -> Result<ArgInstructions, Error>
110110
} else {
111111
Ok(ArgInstructions::Pass)
112112
}
113-
}
113+
},
114114
ValueKind::String(val) => Ok(ArgInstructions::PushKeyValue {
115115
parsed_value: OsString::from(val),
116116
}),

src/context/error.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::config::toml::error::Error as TomlError;
2-
use clap::Error as ClapError;
2+
use clap::{parser::MatchesError, Error as ClapError};
33
use ignore::Error as IgnoreError;
44
use regex::Error as RegexError;
55
use std::convert::From;
@@ -26,6 +26,9 @@ pub enum Error {
2626

2727
#[error("{0}")]
2828
ConfigError(TomlError),
29+
30+
#[error("{0}")]
31+
MatchError(#[from] MatchesError),
2932
}
3033

3134
impl From<TomlError> for Error {

src/context/mod.rs

Lines changed: 14 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use super::disk_usage::{file_size::DiskUsage, units::PrefixKind};
22
use crate::tty;
3-
use clap::{parser::ValueSource, ArgMatches, CommandFactory, FromArgMatches, Parser};
3+
use args::Reconciler;
4+
use clap::{FromArgMatches, Parser};
45
use color::Coloring;
56
use error::Error;
67
use ignore::{
@@ -11,12 +12,15 @@ use regex::Regex;
1112
use std::{
1213
borrow::Borrow,
1314
convert::From,
14-
ffi::{OsStr, OsString},
1515
num::NonZeroUsize,
1616
path::{Path, PathBuf},
1717
thread::available_parallelism,
1818
};
1919

20+
/// Concerned with figuring out how to reconcile arguments provided via the command-line with
21+
/// arguments that come from a config file.
22+
pub mod args;
23+
2024
/// Operations to load in defaults from configuration file.
2125
pub mod config;
2226

@@ -253,73 +257,8 @@ impl Context {
253257
/// Initializes [Context], optionally reading in the configuration file to override defaults.
254258
/// Arguments provided will take precedence over config.
255259
pub fn try_init() -> Result<Self, Error> {
256-
// User-provided arguments from command-line.
257-
let user_args = Self::command().args_override_self(true).get_matches();
258-
259-
// User provides `--no-config`.
260-
if user_args.get_one::<bool>("no_config").is_some_and(|b| *b) {
261-
return Self::from_arg_matches(&user_args).map_err(Error::ArgParse);
262-
}
263-
264-
// Load in `.erdtreerc` or `.erdtree.toml`.
265-
let config_args = if let Some(config) = config::rc::read_config_to_string() {
266-
let raw_args = config::rc::parse(&config);
267-
268-
Self::command().get_matches_from(raw_args)
269-
} else if let Ok(config) = config::toml::load() {
270-
let named_table = user_args.get_one::<String>("config");
271-
let raw_args = config::toml::parse(config, named_table.map(String::as_str))?;
272-
273-
Self::command().get_matches_from(raw_args)
274-
} else {
275-
return Self::from_arg_matches(&user_args).map_err(Error::ArgParse);
276-
};
277-
278-
// If the user did not provide any arguments just read from config.
279-
if !user_args.args_present() {
280-
return Self::from_arg_matches(&config_args).map_err(Error::Config);
281-
}
282-
283-
let mut args = vec![OsString::from("--")];
284-
285-
let ids = Self::command()
286-
.get_arguments()
287-
.map(|arg| arg.get_id().clone())
288-
.collect::<Vec<_>>();
289-
290-
for id in ids {
291-
let id_str = id.as_str();
292-
293-
if id_str == "dir" {
294-
if let Ok(Some(dir)) = user_args.try_get_one::<PathBuf>(id_str) {
295-
args.push(dir.as_os_str().to_owned());
296-
continue;
297-
}
298-
}
299-
300-
let Some(source) = user_args.value_source(id_str) else {
301-
if let Some(params) = Self::extract_args_from(id_str, &config_args) {
302-
args.extend(params);
303-
}
304-
continue;
305-
};
306-
307-
let higher_precedent = match source {
308-
// User provided argument takes precedent over argument from config
309-
ValueSource::CommandLine => &user_args,
310-
311-
// otherwise prioritize argument from the config
312-
_ => &config_args,
313-
};
314-
315-
if let Some(params) = Self::extract_args_from(id_str, higher_precedent) {
316-
args.extend(params);
317-
}
318-
}
319-
320-
let clargs = Self::command().get_matches_from(args);
321-
322-
Self::from_arg_matches(&clargs).map_err(Error::Config)
260+
let args = Self::compute_args()?;
261+
Self::from_arg_matches(&args).map_err(Error::Config)
323262
}
324263

325264
/// Determines whether or not it's appropriate to display color in output based on
@@ -373,26 +312,6 @@ impl Context {
373312
self.file_type.unwrap_or_default()
374313
}
375314

376-
/// Used to pick either from config or user args when constructing [Context].
377-
#[inline]
378-
fn extract_args_from(id: &str, matches: &ArgMatches) -> Option<Vec<OsString>> {
379-
let Ok(Some(raw)) = matches.try_get_raw(id) else {
380-
return None
381-
};
382-
383-
let kebap = format!("--{}", id.replace('_', "-"));
384-
385-
let raw_args = raw
386-
.map(OsStr::to_owned)
387-
.map(|s| [OsString::from(&kebap), s])
388-
.filter(|[_key, val]| val != "false")
389-
.flatten()
390-
.filter(|s| s != "true")
391-
.collect::<Vec<_>>();
392-
393-
Some(raw_args)
394-
}
395-
396315
/// Predicate used for filtering via regular expressions and file-type. When matching regular
397316
/// files, directories will always be included since matched files will need to be bridged back
398317
/// to the root node somehow. Empty sets not producing an output is handled by [`Tree`].
@@ -428,11 +347,11 @@ impl Context {
428347
match file_type {
429348
file::Type::File if entry_type.map_or(true, |ft| !ft.is_file()) => {
430349
return false
431-
}
350+
},
432351
file::Type::Link if entry_type.map_or(true, |ft| !ft.is_symlink()) => {
433352
return false
434-
}
435-
_ => {}
353+
},
354+
_ => {},
436355
}
437356
let file_name = dir_entry.file_name().to_string_lossy();
438357
re.is_match(&file_name)
@@ -497,11 +416,11 @@ impl Context {
497416
match file_type {
498417
file::Type::File if entry_type.map_or(true, |ft| !ft.is_file()) => {
499418
return false
500-
}
419+
},
501420
file::Type::Link if entry_type.map_or(true, |ft| !ft.is_symlink()) => {
502421
return false
503-
}
504-
_ => {}
422+
},
423+
_ => {},
505424
}
506425

507426
let matched = overrides.matched(dir_entry.path(), false);

src/disk_usage/file_size/byte.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl Display for Metric {
123123
} else {
124124
format!("{} {}", self.value, SiPrefix::Base)
125125
}
126-
}
126+
},
127127
PrefixKind::Bin => {
128128
if self.human_readable {
129129
let unit = BinPrefix::from(self.value);
@@ -138,7 +138,7 @@ impl Display for Metric {
138138
} else {
139139
format!("{} {}", self.value, BinPrefix::Base)
140140
}
141-
}
141+
},
142142
};
143143

144144
write!(f, "{display}")?;

src/icons/fs.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub fn compute_with_color(
6161
let ansi_string: ANSIGenericString<str> = fg.bold().paint(icon);
6262
let styled_icon = ansi_string.to_string();
6363
Cow::from(styled_icon)
64-
}
64+
},
6565
_ => icon,
6666
};
6767

0 commit comments

Comments
 (0)