summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsolidiquis <benjamin.van.nguyen@gmail.com>2024-05-04 19:39:02 -0700
committersolidiquis <benjamin.van.nguyen@gmail.com>2024-05-04 19:39:02 -0700
commit5e4da1367c640e2fffc13e11f36a4c9de3b14da0 (patch)
tree6347c26a079d8693e9f68a35492ea17f0bf9af43
parentd9151325d4d7d3280f31c2d356b7b701673cfae4 (diff)
-rw-r--r--Cargo.lock29
-rw-r--r--Cargo.toml4
-rw-r--r--src/file/mod.rs17
-rw-r--r--src/file/tree/filter.rs13
-rw-r--r--src/file/unix/ug.rs8
-rw-r--r--src/render/row/long.rs12
-rw-r--r--src/render/row/mod.rs4
-rw-r--r--src/user/config/mod.rs4
-rw-r--r--src/user/config/parse.rs53
-rw-r--r--src/user/config/test.rs9
-rw-r--r--src/user/config/toml.rs19
-rw-r--r--src/user/mod.rs199
-rw-r--r--src/user/test.rs117
13 files changed, 334 insertions, 154 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cce99c5..505a964 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index e344913..78c9603 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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()
+}