diff options
author | solidiquis <benjamin.van.nguyen@gmail.com> | 2024-05-04 19:39:02 -0700 |
---|---|---|
committer | solidiquis <benjamin.van.nguyen@gmail.com> | 2024-05-04 19:39:02 -0700 |
commit | 5e4da1367c640e2fffc13e11f36a4c9de3b14da0 (patch) | |
tree | 6347c26a079d8693e9f68a35492ea17f0bf9af43 | |
parent | d9151325d4d7d3280f31c2d356b7b701673cfae4 (diff) |
configssolidiquis/traversal
-rw-r--r-- | Cargo.lock | 29 | ||||
-rw-r--r-- | Cargo.toml | 4 | ||||
-rw-r--r-- | src/file/mod.rs | 17 | ||||
-rw-r--r-- | src/file/tree/filter.rs | 13 | ||||
-rw-r--r-- | src/file/unix/ug.rs | 8 | ||||
-rw-r--r-- | src/render/row/long.rs | 12 | ||||
-rw-r--r-- | src/render/row/mod.rs | 4 | ||||
-rw-r--r-- | src/user/config/mod.rs | 4 | ||||
-rw-r--r-- | src/user/config/parse.rs | 53 | ||||
-rw-r--r-- | src/user/config/test.rs | 9 | ||||
-rw-r--r-- | src/user/config/toml.rs | 19 | ||||
-rw-r--r-- | src/user/mod.rs | 199 | ||||
-rw-r--r-- | src/user/test.rs | 117 |
13 files changed, 334 insertions, 154 deletions
@@ -103,17 +103,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] -name = "async-trait" -version = "0.1.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -222,16 +211,15 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "config" -version = "0.13.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" dependencies = [ - "async-trait", "lazy_static", "nom", "pathdiff", "serde", - "toml 0.5.11", + "toml", ] [[package]] @@ -329,7 +317,7 @@ dependencies = [ "tempfile", "terminal_size", "thiserror", - "toml 0.8.8", + "toml", "winapi", ] @@ -938,15 +926,6 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" @@ -15,7 +15,7 @@ keywords = ["tree", "find", "ls", "du", "commandline"] exclude = ["assets/*", "scripts/*", "example/*"] readme = "README.md" license = "MIT" -rust-version = "1.76.0" +rust-version = "1.78.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -36,7 +36,7 @@ anyhow = "1.0.75" chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } clap = { version = "4.4.10", features = ["derive"] } clap_complete = "4.1.1" -config = { version = "0.13.3", default-features = false, features = ["toml"] } +config = { version = "0.14.0", default-features = false, features = ["toml"] } crossterm = "0.26.1" ctrlc = "3.4.0" dirs = "5.0" diff --git a/src/file/mod.rs b/src/file/mod.rs index 29a8297..473bbfd 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -112,6 +112,7 @@ impl File { #[cfg(unix)] let unix_attrs = long + .long .then(|| unix::Attrs::from((&metadata, &data))) .unwrap_or_else(unix::Attrs::default); @@ -173,7 +174,7 @@ impl File { pub fn timestamp_from_ctx(&self, ctx: &Context) -> Option<String> { use chrono::{DateTime, Local}; - let system_time = match ctx.time { + let system_time = match ctx.long.time.unwrap_or_default() { TimeStamp::Mod => self.metadata().accessed().ok(), TimeStamp::Create => self.metadata().created().ok(), TimeStamp::Access => self.metadata().accessed().ok(), @@ -181,12 +182,14 @@ impl File { system_time .map(DateTime::<Local>::from) - .map(|local_time| match ctx.time_format { - TimeFormat::Default => local_time.format("%d %h %H:%M %g"), - TimeFormat::Iso => local_time.format("%Y-%m-%d %H:%M:%S"), - TimeFormat::IsoStrict => local_time.format("%Y-%m-%dT%H:%M:%S%Z"), - TimeFormat::Short => local_time.format("%Y-%m-%d"), - }) + .map( + |local_time| match ctx.long.time_format.unwrap_or_default() { + TimeFormat::Default => local_time.format("%d %h %H:%M %g"), + TimeFormat::Iso => local_time.format("%Y-%m-%d %H:%M:%S"), + TimeFormat::IsoStrict => local_time.format("%Y-%m-%dT%H:%M:%S%Z"), + TimeFormat::Short => local_time.format("%Y-%m-%d"), + }, + ) .map(|dt| format!("{dt}")) } diff --git a/src/file/tree/filter.rs b/src/file/tree/filter.rs index 8adac89..98e8ee1 100644 --- a/src/file/tree/filter.rs +++ b/src/file/tree/filter.rs @@ -3,7 +3,7 @@ use crate::{ file::{tree::Tree, File}, user::{ args::{FileType, Layout}, - Context, Globbing, + Context, Search, }, }; use ahash::HashSet; @@ -31,8 +31,8 @@ impl Tree { self.filter_file_type(ctx); } - if ctx.pattern.is_some() { - let Globbing { glob, iglob } = ctx.globbing; + if ctx.search.pattern.is_some() { + let Search { glob, iglob, .. } = ctx.search; if glob || iglob { self.filter_glob(ctx)?; @@ -123,7 +123,9 @@ impl Tree { pub fn filter_regex( &mut self, Context { - pattern, layout, .. + search: Search { pattern, .. }, + layout, + .. }: &Context, ) -> Result<()> { let re_pattern = pattern @@ -174,8 +176,7 @@ impl Tree { /// Filtering via globbing fn filter_glob(&mut self, ctx: &Context) -> Result<()> { let Context { - globbing: Globbing { iglob, .. }, - pattern, + search: Search { pattern, iglob, .. }, layout, .. } = ctx; diff --git a/src/file/unix/ug.rs b/src/file/unix/ug.rs index 5d7e534..7d08bbf 100644 --- a/src/file/unix/ug.rs +++ b/src/file/unix/ug.rs @@ -8,14 +8,6 @@ impl UserGroupInfo for Metadata {} /// Trait that allows for files to query their owner and group. pub trait UserGroupInfo: MetadataExt { - /// Attemps to query the owner of the implementor. - fn try_get_owner(&self) -> Result<String, Error> { - unsafe { - let uid = self.uid(); - try_get_user(uid) - } - } - /// Attempts to query both the owner and group of the implementor. fn try_get_owner_and_group(&self) -> Result<(Owner, Group), Error> { unsafe { diff --git a/src/render/row/long.rs b/src/render/row/long.rs index 2230275..cca17de 100644 --- a/src/render/row/long.rs +++ b/src/render/row/long.rs @@ -46,10 +46,14 @@ impl From<INodeError> for fmt::Error { impl Display for Format<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Context { - group: enable_group, - ino: enable_ino, - nlink: enable_nlink, - octal: enable_octal, + long: + crate::user::Long { + group: enable_group, + ino: enable_ino, + nlink: enable_nlink, + octal: enable_octal, + .. + }, column_metadata, .. } = self.ctx; diff --git a/src/render/row/mod.rs b/src/render/row/mod.rs index 870c0d1..2953d58 100644 --- a/src/render/row/mod.rs +++ b/src/render/row/mod.rs @@ -17,7 +17,7 @@ pub fn formatter<'a>(buf: &'a mut String, ctx: &'a Context) -> Result<RowFormatt Layout::Flat => { let root = ctx.dir_canonical()?; - match (ctx.long, ctx.suppress_size) { + match (ctx.long.long, ctx.suppress_size) { (false, false) => Ok(Box::new(move |file, prefix| { let size = format!("{}", file.size()); let base = root.ancestors().nth(1); @@ -75,7 +75,7 @@ pub fn formatter<'a>(buf: &'a mut String, ctx: &'a Context) -> Result<RowFormatt })), } }, - _ => match (ctx.long, ctx.suppress_size) { + _ => match (ctx.long.long, ctx.suppress_size) { (false, false) => Ok(Box::new(|file, prefix| { let size = format!("{}", file.size()); let name = file.display_name(); diff --git a/src/user/config/mod.rs b/src/user/config/mod.rs index 3322310..b2b0d9e 100644 --- a/src/user/config/mod.rs +++ b/src/user/config/mod.rs @@ -1,9 +1,7 @@ const ERDTREE_CONFIG_TOML: &str = ".erdtree.toml"; +const ERDTREE_CONFIG_FILE: &str = ".erdtree"; const ERDTREE_TOML_PATH: &str = "ERDTREE_TOML_PATH"; -const ERDTREE_CONFIG_NAME: &str = ".erdtreerc"; -const ERDTREE_CONFIG_PATH: &str = "ERDTREE_CONFIG_PATH"; - const ERDTREE_DIR: &str = "erdtree"; #[cfg(unix)] diff --git a/src/user/config/parse.rs b/src/user/config/parse.rs index 4233048..71d7828 100644 --- a/src/user/config/parse.rs +++ b/src/user/config/parse.rs @@ -1,13 +1,5 @@ -use crate::user; use ahash::HashMap; -use clap::{ - ArgMatches, - Command, - CommandFactory, - error::Error as ClapError, - FromArgMatches, - Parser -}; +use clap::{error::Error as ClapError, ArgMatches, CommandFactory}; use config::{Config, ConfigError}; use toml::Value; @@ -20,7 +12,7 @@ pub enum Error { TableNotFound(String), #[error("Error while parsing config arguments: {0}")] - Parse(#[from] ClapError) + Parse(#[from] ClapError), } type Result<T> = std::result::Result<T, Error>; @@ -31,7 +23,7 @@ pub fn args(conf: Config, table_name: Option<&str>) -> Result<Option<ArgMatches> let maybe_config = conf.try_deserialize::<toml::Table>().map(|raw_config| { deep_transform_keys(raw_config, |mut key| { key.make_ascii_lowercase(); - format!("--{}", key.replace("_", "-")) + format!("--{}", key.replace('_', "-")) }) })?; @@ -43,10 +35,10 @@ pub fn args(conf: Config, table_name: Option<&str>) -> Result<Option<ArgMatches> Some(name) => match config.remove(&format!("--{name}")) { Some(Value::Table(sub_table)) => { config = sub_table; - } + }, _ => return Err(Error::TableNotFound(name.to_string())), - } - None => remove_sub_tables(&mut config) + }, + None => remove_sub_tables(&mut config), } let arg_matches = into_args(config)?; @@ -66,21 +58,20 @@ fn into_args(conf: TomlConfig) -> Result<ArgMatches> { args.push(farg) } } - } + }, Value::Boolean(arg) if arg => { args.push(arg_name.clone()); - } + }, _ => { if let Some(farg) = fmt_arg(param) { args.push(arg_name.clone()); args.push(farg) } - } + }, } } - let cmd = crate::user::Context::command() - .try_get_matches_from(args)?; + let cmd = crate::user::Context::command().try_get_matches_from(args)?; Ok(cmd) } @@ -88,11 +79,11 @@ fn into_args(conf: TomlConfig) -> Result<ArgMatches> { /// Formats basic primitive types into OS args. Will ignore table and array types. fn fmt_arg(val: Value) -> Option<String> { match val { - Value::Float(p) => Some(format!("{p}")), - Value::String(p) => Some(format!("{p}")), - Value::Datetime(p) => Some(format!("{p}")), - Value::Integer(p) => Some(format!("{p}")), - _ => None + Value::Float(p) => Some(p.to_string()), + Value::String(p) => Some(p.to_string()), + Value::Datetime(p) => Some(p.to_string()), + Value::Integer(p) => Some(p.to_string()), + _ => None, } } @@ -115,7 +106,7 @@ fn deep_transform_keys(toml: TomlConfig, transformer: KeyTransformer) -> Option< let mut dfs_stack_src = vec![Value::Table(toml)]; let mut dfs_stack_dst = vec![("".to_string(), toml::map::Map::default())]; - let mut key_iters = HashMap::default(); + let mut key_iters = HashMap::default(); 'outer: while !dfs_stack_src.is_empty() { let Some(Value::Table(current_node)) = dfs_stack_src.last_mut() else { @@ -126,9 +117,9 @@ fn deep_transform_keys(toml: TomlConfig, transformer: KeyTransformer) -> Option< continue; }; - let keys = key_iters.entry(dst_key.clone()).or_insert_with(|| { - current_node.keys().cloned().collect::<Vec<_>>().into_iter() - }); + let keys = key_iters + .entry(dst_key.clone()) + .or_insert_with(|| current_node.keys().cloned().collect::<Vec<_>>().into_iter()); for key in keys { match current_node.remove(&key) { @@ -138,12 +129,12 @@ fn deep_transform_keys(toml: TomlConfig, transformer: KeyTransformer) -> Option< dfs_stack_dst.push((transformed_key, toml::map::Map::default())); dfs_stack_src.push(value); continue 'outer; - } + }, _ => { let transformed_key = transformer(key); copy_dst.insert(transformed_key, value); - } - } + }, + }, None => continue, } } diff --git a/src/user/config/test.rs b/src/user/config/test.rs index 4253d0b..1d67661 100644 --- a/src/user/config/test.rs +++ b/src/user/config/test.rs @@ -1,13 +1,12 @@ -use crate::user::args::{Layout, Metric}; use super::parse; -use std::env::current_dir; +use crate::user::args::{Layout, Metric}; use config::{Config, File}; #[test] fn test_toml_parse_top_level_table() { let config = load_example(); - let arg_matches = parse::args(config, None) + let arg_matches = parse::args(config, None) .expect("Failed to parse example config.") .expect("Expected top level table to be found."); @@ -19,7 +18,7 @@ fn test_toml_parse_top_level_table() { fn test_toml_parse_sub_table() { let config = load_example(); - let arg_matches = parse::args(config, Some("du")) + let arg_matches = parse::args(config, Some("du")) .expect("Failed to parse example config.") .expect("Expected sub table to be found."); @@ -34,7 +33,7 @@ fn test_toml_parse_sub_table() { } fn load_example() -> Config { - let example_config = current_dir() + let example_config = std::env::current_dir() .ok() .map(|p| p.join("example").join(super::ERDTREE_CONFIG_TOML)) .and_then(|p| p.as_path().to_str().map(File::with_name)) diff --git a/src/user/config/toml.rs b/src/user/config/toml.rs index b1ab279..d643536 100644 --- a/src/user/config/toml.rs +++ b/src/user/config/toml.rs @@ -36,13 +36,14 @@ fn toml_from_env() -> Option<Config> { /// Concerned with how to load `.erdtree.toml` on Unix systems. #[cfg(unix)] mod unix { - use super::super::{CONFIG_DIR, ERDTREE_CONFIG_TOML, ERDTREE_DIR, HOME, XDG_CONFIG_HOME}; + use super::super::{ + CONFIG_DIR, ERDTREE_CONFIG_FILE, ERDTREE_CONFIG_TOML, ERDTREE_DIR, HOME, XDG_CONFIG_HOME, + }; use config::{Config, File}; use std::{env, ffi::OsStr, path::PathBuf}; /// Looks for `.erdtree.toml` in the following locations in order: /// - /// - `$ERDTREE_TOML_PATH` /// - `$XDG_CONFIG_HOME/erdtree/.erdtree.toml` /// - `$XDG_CONFIG_HOME/.erdtree.toml` /// - `$HOME/.config/erdtree/.erdtree.toml` @@ -93,15 +94,14 @@ mod unix { None => return None, // Why don't you have `HOME` set? Weirdo. }; - let file = home + let source = home .join(CONFIG_DIR) .join(ERDTREE_DIR) - .join(ERDTREE_CONFIG_TOML) - .file_stem() - .and_then(OsStr::to_str) + .join(ERDTREE_CONFIG_FILE) + .to_str() .map(File::with_name)?; - if let Ok(config) = Config::builder().add_source(file).build() { + if let Ok(config) = Config::builder().add_source(source).build() { return Some(config); } @@ -118,7 +118,7 @@ mod unix { /// Concerned with how to load `.erdtree.toml` on Windows. #[cfg(windows)] mod windows { - use super::super::{ERDTREE_CONFIG_TOML, ERDTREE_DIR}; + use super::super::{ERDTREE_CONFIG_FILE, ERDTREE_DIR}; use config::{Config, File}; /// Try to read in config from the following location: @@ -133,9 +133,8 @@ mod windows { let file = dirs::config_dir().and_then(|config_dir| { config_dir .join(ERDTREE_DIR) - .join(ERDTREE_CONFIG_TOML) + .join(ERDTREE_CONFIG_FILE) .to_str() - .and_then(|s| s.strip_suffix(".toml")) .map(File::with_name) })?; diff --git a/src/user/mod.rs b/src/user/mod.rs index b84fd36..c6ae071 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -1,5 +1,6 @@ use crate::error::prelude::*; -use clap::{Args, Parser}; +use ahash::HashMap; +use clap::{parser::ValueSource, ArgMatches, Args, CommandFactory, FromArgMatches, Parser}; use std::{env, fs, path::PathBuf}; /// Enum definitions for enumerated command-line arguments. @@ -11,6 +12,9 @@ pub mod column; /// Concerned with loading and parsing the optional `erdtree.toml` config file. mod config; +#[cfg(test)] +mod test; + /// Defines the CLI whose purpose is to capture user arguments and reconcile them with arguments /// found with a config file if relevant. #[derive(Parser, Debug)] @@ -65,45 +69,9 @@ pub struct Context { #[arg(long)] pub global_gitignore: bool, - /// Show extended metadata and attributes #[cfg(unix)] - #[arg(short, long, group = "ls-long")] - pub long: bool, - - /// Show file's groups - #[cfg(unix)] - #[arg(long, requires = "ls-long")] - pub group: bool, - - /// Show each file's ino - #[cfg(unix)] - #[arg(long, requires = "ls-long")] - pub ino: bool, - - /// Show the total number of hardlinks to the underlying inode - #[cfg(unix)] - #[arg(long, requires = "ls-long")] - pub nlink: bool, - - /// Show permissions in numeric octal format instead of symbolic - #[cfg(unix)] - #[arg(long, requires = "ls-long")] - pub octal: bool, - - /// Which kind of timestamp to use - #[cfg(unix)] - #[arg(long, value_enum, requires = "ls-long", default_value_t)] - pub time: args::TimeStamp, - - /// Which format to use for the timestamp; default by default - #[cfg(unix)] - #[arg( - long = "time-format", - requires = "ls-long", - value_enum, - default_value_t - )] - pub time_format: args::TimeFormat, + #[command(flatten)] + pub long: Long, /// Maximum depth to display #[arg(short = 'L', long, value_name = "NUM")] @@ -117,13 +85,8 @@ pub struct Context { #[arg(short, long)] pub no_config: bool, - /// Regular expression (or glob if '--glob' or '--iglob' is used) used to match files by their - /// relative path - #[arg(short, long, group = "searching")] - pub pattern: Option<String>, - #[command(flatten)] - pub globbing: Globbing, + pub search: Search, /// Omit empty directories from the output #[arg(short = 'P', long)] @@ -169,8 +132,13 @@ pub struct Context { } #[derive(Args, Debug)] -#[group(multiple = false)] -pub struct Globbing { +#[group(required = false, multiple = false)] +pub struct Search { + /// Regular expression (or glob if '--glob' or '--iglob' is used) used to match files by their + /// relative path + #[arg(short, long, group = "searching")] + pub pattern: Option<String>, + /// Enables glob based searching instead of regular expressions #[arg(long, requires = "searching")] pub glob: bool, @@ -180,16 +148,57 @@ pub struct Globbing { pub iglob: bool, } +#[cfg(unix)] +#[derive(Args, Debug)] +#[group(required = false, multiple = true)] +pub struct Long { + /// Show extended metadata and attributes + #[arg(short, long, group = "ls-long")] + pub long: bool, + + /// Show file's groups + #[arg(long, requires = "ls-long")] + pub group: bool, + + /// Show each file's ino + #[arg(long, requires = "ls-long")] + pub ino: bool, + + /// Show the total number of hardlinks to the underlying inode + #[arg(long, requires = "ls-long")] + pub nlink: bool, + + /// Show permissions in numeric octal format instead of symbolic + #[arg(long, requires = "ls-long")] + pub octal: bool, + + /// Which kind of timestamp to use + #[arg(long, value_enum, requires = "ls-long")] + pub time: Option<args::TimeStamp>, + + /// Which format to use for the timestamp; default by default + #[arg(long = "time-format", requires = "ls-long", value_enum)] + pub time_format: Option<args::TimeFormat>, +} + impl Context { pub fn init() -> Result<Self> { - let mut clargs = Self::parse(); + let clargs = Self::command().get_matches(); + let user_config = Self::load_config(&clargs)?; - if clargs.dir.is_none() { + let mut ctx = if let Some(ref config) = user_config { + let reconciled_args = Self::reconcile_args(&clargs, config); + Self::try_parse_from(reconciled_args).into_report(ErrorCategory::User)? + } else { + Self::from_arg_matches(&clargs).into_report(ErrorCategory::User)? + }; + + if ctx.dir.is_none() { let current_dir = Self::get_current_dir()?; - clargs.dir = Some(current_dir); + ctx.dir = Some(current_dir); } - Ok(clargs) + Ok(ctx) } pub fn dir(&self) -> Option<&PathBuf> { @@ -224,4 +233,92 @@ impl Context { fn default_num_threads() -> usize { std::thread::available_parallelism().map_or(3, usize::from) } + + fn load_config(clargs: &ArgMatches) -> Result<Option<ArgMatches>> { + let cmd = Self::from_arg_matches(clargs).into_report(ErrorCategory::User)?; + + if cmd.no_config { + return Ok(None); + } + + let Some(raw_config) = config::toml::load() else { + return Ok(None); + }; + + match config::parse::args(raw_config, cmd.config.as_deref()) { + Ok(config) => Ok(config), + Err(err) => match err { + config::parse::Error::TableNotFound(_) => Err(err).into_report(ErrorCategory::User), + _ => Err(err).into_report(ErrorCategory::Internal), + }, + } + } + + /// Reconcile args between command-line and user config. + fn reconcile_args(clargs: &ArgMatches, config: &ArgMatches) -> Vec<String> { + let mut arg_id_map = HashMap::<clap::Id, clap::Arg>::default(); + + for arg_def in Self::command().get_arguments() { + if arg_def.is_positional() { + continue; + } + arg_id_map.insert(arg_def.get_id().clone(), arg_def.clone()); + } + + let mut args = vec![crate::BIN_NAME.to_string()]; + + let mut push_args = |arg_name: String, arg_id: &str, src: &ArgMatches| { + if let Ok(Some(mut bool_args)) = src.try_get_many::<bool>(arg_id) { + if bool_args.all(|arg| *arg) { + args.push(arg_name); + } + return; + } + + let vals = src + .get_raw_occurrences(arg_id) + .unwrap() + .flat_map(|i| { + i.map(|o| o.to_string_lossy().into_owned()) + .collect::<Vec<_>>() + }) + .collect::<Vec<_>>(); + + args.push(arg_name); + args.extend_from_slice(&vals); + }; + + for arg_id in arg_id_map.keys() { + let arg_id_str = arg_id.as_str(); + + let Some(arg_def) = arg_id_map.get(arg_id) else { + continue; + }; + + let arg_name = arg_def.get_long().map_or_else( + || arg_def.get_short().map(|c| format!("-{c}")).unwrap(), + |long| format!("--{long}"), + ); + + let confarg_vs = config.value_source(arg_id_str); + let clarg_vs = clargs.value_source(arg_id_str); + + match (clarg_vs, confarg_vs) { + (None, None) => continue, + (Some(_), None) => push_args(arg_name, arg_id_str, clargs), + (None, Some(_)) => push_args(arg_name, arg_id_str, config), + (Some(clarg), Some(conf)) => match (clarg, conf) { + // Prioritize config argument over default + (ValueSource::DefaultValue, ValueSource::CommandLine) => { + push_args(arg_name, arg_id_str, config) + }, + + // Prioritize user argument in all other cases + _ => push_args(arg_name, arg_id_str, clargs), + }, + } + } + + args.into_iter().collect::<Vec<_>>() + } } diff --git a/src/user/test.rs b/src/user/test.rs new file mode 100644 index 0000000..91bbc6c --- /dev/null +++ b/src/user/test.rs @@ -0,0 +1,117 @@ +use super::Context; +use crate::user::config; +use ::config::{Config, File, FileFormat}; +use clap::{CommandFactory, Parser}; + +const MOCK_CONFIG_TOML: &'static str = r#" +icons = true +no-git = true +threads = 3 + +[du] +metric = "block" +icons = true +layout = "flat" +level = 1 + +# Do as `ls -l` +[ls] +icons = true +level = 1 +suppress-size = true +long = true +gitignore = true +no_hidden = true + +# How many lines of Rust are in this code base? +[rs] +metric = "line" +level = 1 +pattern = "\\.rs$" +"#; + +#[test] +fn test_reconcile_arg_matches_top_level_table() { + let user_args = vec![ + crate::BIN_NAME.to_string(), + "--threads".to_string(), + "6".to_string(), + ]; + let clargs = Context::command().get_matches_from(user_args); + + let config = load_config(MOCK_CONFIG_TOML); + let config_args = config::parse::args(config, None).unwrap().unwrap(); + + let args = Context::reconcile_args(&clargs, &config_args); + let ctx = Context::try_parse_from(args).unwrap(); + + // Config args + assert!(ctx.icons); + assert!(ctx.no_git); + + // Default args + assert!(matches!( + ctx.byte_units, + crate::user::args::BytePresentation::Raw + )); + assert!(!ctx.follow); + assert!(!ctx.no_hidden); + + // User args takes precedence over config + assert_eq!(ctx.threads, 6) +} + +#[test] +fn test_reconcile_arg_matches_sub_table_du() { + let user_args = vec![crate::BIN_NAME.to_string()]; + let clargs = Context::command().get_matches_from(user_args); + + let config = load_config(MOCK_CONFIG_TOML); + let config_args = config::parse::args(config, Some("du")).unwrap().unwrap(); + + let args = Context::reconcile_args(&clargs, &config_args); + let ctx = Context::try_parse_from(args).unwrap(); + + // Config args + assert!(matches!(ctx.metric, crate::user::args::Metric::Block)); + assert!(ctx.icons); + assert!(matches!(ctx.layout, crate::user::args::Layout::Flat)); + assert_eq!(ctx.level, Some(1)); + + // Default args + assert!(matches!( + ctx.byte_units, + crate::user::args::BytePresentation::Raw + )); + assert!(!ctx.follow); + assert!(!ctx.no_hidden); +} + +#[test] +fn test_reconcile_arg_matches_sub_table_rs() { + let user_args = vec![crate::BIN_NAME.to_string()]; + let clargs = Context::command().get_matches_from(user_args); + + let config = load_config(MOCK_CONFIG_TOML); + let config_args = config::parse::args(config, Some("rs")).unwrap().unwrap(); + + let args = Context::reconcile_args(&clargs, &config_args); + let ctx = Context::try_parse_from(args).unwrap(); + + // Config args + assert!(matches!(ctx.metric, crate::user::args::Metric::Line)); + assert_eq!(ctx.level, Some(1)); + assert_eq!(ctx.search.pattern, Some("\\.rs$".to_string())); + + // Default args + assert!(!ctx.follow); + assert!(!ctx.no_hidden); + assert!(!ctx.icons); +} + +fn load_config(config: &str) -> Config { + Config::builder() + .add_source(File::from_str(config, FileFormat::Toml)) + .build() + .unwrap() +} |