From f11c3fc899b2f9857116b7769620c15d58fd6399 Mon Sep 17 00:00:00 2001 From: KP64 Date: Thu, 27 Apr 2023 19:36:32 +0200 Subject: [PATCH 01/14] refactor: :recycle: removal of "Glob" booleans through addition of Glob Enum. The Glob enum has been introduced in order to get rid of duplicated settings. This makes the Checking of the provided arguments easier in source code since Enums are Exhaustive. This comes at a minimal usage friendliness degradation since now it would be needed to specify "--glob insensitive" instead of "--iglob". Even then I think it is worth it :3 --- src/main.rs | 13 ++------- src/render/context/mod.rs | 46 ++++++++++++++++++++------------ src/render/tree/display/theme.rs | 6 +++-- src/render/tree/mod.rs | 41 ++++++++++++---------------- src/render/tree/node/mod.rs | 16 ++++++++--- src/tty/windows.rs | 5 ++-- 6 files changed, 66 insertions(+), 61 deletions(-) diff --git a/src/main.rs b/src/main.rs index aace35df..8dcabacd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ use render::{ Tree, }, }; -use std::{io::stdout, process::ExitCode}; +use std::{error::Error, io::stdout}; /// Operations to wrangle ANSI escaped strings. mod ansi; @@ -43,16 +43,7 @@ mod tty; /// Common utilities across all modules. mod utils; -fn main() -> ExitCode { - if let Err(e) = run() { - eprintln!("{e}"); - return ExitCode::FAILURE; - } - - ExitCode::SUCCESS -} - -fn run() -> Result<(), Box> { +fn main() -> Result<(), Box> { let ctx = Context::init()?; if let Some(shell) = ctx.completions { diff --git a/src/render/context/mod.rs b/src/render/context/mod.rs index 78e0af66..5f7c06fe 100644 --- a/src/render/context/mod.rs +++ b/src/render/context/mod.rs @@ -52,6 +52,19 @@ pub enum Coloring { /// Turn on colorization always Forced, } + +#[derive(Clone, Copy, Debug, clap::ValueEnum, PartialEq, Eq, Default)] +pub enum Glob { + // Disables glob based searching + #[default] + None, + + /// Enables glob based searching + Sensitive, + + /// Enables case-insensitive glob based searching + Insensitive, +} /// Defines the CLI. #[derive(Parser, Debug)] #[command(name = "erdtree")] @@ -114,12 +127,8 @@ pub struct Context { pub pattern: Option, /// Enables glob based searching - #[arg(long, requires = "pattern")] - pub glob: bool, - - /// Enables case-insensitive glob based searching - #[arg(long, requires = "pattern")] - pub iglob: bool, + #[arg(long, requires = "pattern", value_enum, default_value_t = Glob::default())] + pub glob: Glob, /// Restrict regex or glob search to a particular file-type #[arg(short = 't', long, requires = "pattern", value_enum)] @@ -220,6 +229,15 @@ impl Context { /// Initializes [Context], optionally reading in the configuration file to override defaults. /// Arguments provided will take precedence over config. pub fn init() -> Result { + trait IntoVecOfStr { + fn as_vec_of_str(&self) -> Vec<&str>; + } + impl IntoVecOfStr for ArgMatches { + fn as_vec_of_str(&self) -> Vec<&str> { + self.ids().map(Id::as_str).collect() + } + } + let user_args = Self::command().args_override_self(true).get_matches(); let no_config = user_args @@ -244,16 +262,13 @@ impl Context { // user arguments. let mut args = vec![OsString::from("--")]; - let mut ids = user_args.ids().map(Id::as_str).collect::>(); + let mut ids = user_args.as_vec_of_str(); - ids.extend(config_args.ids().map(Id::as_str).collect::>()); + ids.extend(config_args.as_vec_of_str()); ids = crate::utils::uniq(ids); - for id in ids { - if id == "Context" { - continue; - } + for id in ids.into_iter().filter(|&id| id != "Context") { if id == "dir" { if let Ok(Some(raw)) = user_args.try_get_raw(id) { let raw_args = raw.map(OsStr::to_owned).collect::>(); @@ -394,11 +409,8 @@ impl Context { let mut negated_glob = false; - let overrides = if !self.glob && !self.iglob { - // Shouldn't really ever be hit but placing here as a safeguard. - return Err(Error::EmptyGlob); - } else { - if self.iglob { + let overrides = { + if self.glob == Glob::Insensitive { builder.case_insensitive(true)?; } diff --git a/src/render/tree/display/theme.rs b/src/render/tree/display/theme.rs index 3df91c43..f8b04de6 100644 --- a/src/render/tree/display/theme.rs +++ b/src/render/tree/display/theme.rs @@ -3,15 +3,17 @@ use crate::render::{ tree::Node, }; +type Theme = Box &'static ThemesMap>; + /// Returns a closure that retrieves the regular theme. -pub fn regular_theme_getter() -> Box &'static ThemesMap> { +pub fn regular_theme_getter() -> Theme { Box::new(|_node: &Node| styles::get_tree_theme().unwrap()) } /// Returns a closure that can smartly determine when a symlink is being followed and when it is /// not being followed. When a symlink is being followed, all of its descendents should have tree /// branches that are colored differently. -pub fn link_theme_getter() -> Box &'static ThemesMap> { +pub fn link_theme_getter() -> Theme { let mut link_depth = None; Box::new(move |node: &Node| { diff --git a/src/render/tree/mod.rs b/src/render/tree/mod.rs index 9104c4b9..ee835c79 100644 --- a/src/render/tree/mod.rs +++ b/src/render/tree/mod.rs @@ -1,7 +1,7 @@ use crate::{ fs::inode::Inode, render::{ - context::{file, output::ColumnProperties, Context}, + context::{self, file, output::ColumnProperties, Context}, disk_usage::file_size::FileSize, styles, }, @@ -282,19 +282,14 @@ where /// Function to remove empty directories. fn prune_directories(root_id_id: NodeId, tree: &mut Arena) { - let mut to_prune = vec![]; - - for node_id in root_id_id.descendants(tree).skip(1) { - let node = tree[node_id].get(); - - if !node.is_dir() { - continue; - } - - if node_id.children(tree).count() == 0 { - to_prune.push(node_id); - } - } + let to_prune = root_id_id + .descendants(tree) + .skip(1) + .map(|node_id| (node_id, tree[node_id].get())) + .filter(|(_, node)| node.is_dir()) + .map(|(node_id, _)| node_id) + .filter(|node_id| node_id.children(tree).count() == 0) + .collect::>(); if to_prune.is_empty() { return; @@ -309,13 +304,11 @@ where /// Filter for only directories. fn filter_directories(root_id: NodeId, tree: &mut Arena) { - let mut to_detach = vec![]; - - for descendant_id in root_id.descendants(tree).skip(1) { - if !tree[descendant_id].get().is_dir() { - to_detach.push(descendant_id); - } - } + let to_detach = root_id + .descendants(tree) + .skip(1) + .filter(|&descendant_id| !tree[descendant_id].get().is_dir()) + .collect::>(); for descendant_id in to_detach { descendant_id.detach(tree); @@ -405,10 +398,10 @@ impl TryFrom<&Context> for WalkParallel { .threads(ctx.threads); if ctx.pattern.is_some() { - if ctx.glob || ctx.iglob { - builder.filter_entry(ctx.glob_predicate()?); - } else { + if ctx.glob == context::Glob::None { builder.filter_entry(ctx.regex_predicate()?); + } else { + builder.filter_entry(ctx.glob_predicate()?); } } diff --git a/src/render/tree/node/mod.rs b/src/render/tree/node/mod.rs index 521d21d7..a346c0b5 100644 --- a/src/render/tree/node/mod.rs +++ b/src/render/tree/node/mod.rs @@ -128,13 +128,21 @@ impl Node { } /// Returns the underlying `ino` of the [`DirEntry`]. - pub fn ino(&self) -> Option { - self.inode.map(|inode| inode.ino) + pub const fn ino(&self) -> Option { + if let Some(inode) = self.inode { + Some(inode.ino) + } else { + None + } } /// Returns the underlying `nlink` of the [`DirEntry`]. - pub fn nlink(&self) -> Option { - self.inode.map(|inode| inode.nlink) + pub const fn nlink(&self) -> Option { + if let Some(inode) = self.inode { + Some(inode.nlink) + } else { + None + } } /// Returns `true` if node is a directory. diff --git a/src/tty/windows.rs b/src/tty/windows.rs index fb793e3e..796236b2 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -26,8 +26,7 @@ pub(super) unsafe fn win_width() -> Option { }; (GetConsoleScreenBufferInfo(stdout_handle, &mut console_data) != 0) - .then(|| console_data.srWindow.Right - console_data.srWindow.Left + 1) + .then_some(console_data.srWindow.Right - console_data.srWindow.Left + 1) .map(usize::try_from) - .map(Result::ok) - .flatten() + .and_then(Result::ok) } From 40833f32fde2784deaa90b9f03381093b0ac6544 Mon Sep 17 00:00:00 2001 From: KP64 Date: Thu, 27 Apr 2023 19:44:13 +0200 Subject: [PATCH 02/14] Update all Tests to the new "Glob" Usage --- src/render/tree/mod.rs | 11 ++++++----- tests/glob.rs | 11 +++++++---- tests/prune.rs | 9 ++++++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/render/tree/mod.rs b/src/render/tree/mod.rs index ee835c79..146eb006 100644 --- a/src/render/tree/mod.rs +++ b/src/render/tree/mod.rs @@ -398,11 +398,12 @@ impl TryFrom<&Context> for WalkParallel { .threads(ctx.threads); if ctx.pattern.is_some() { - if ctx.glob == context::Glob::None { - builder.filter_entry(ctx.regex_predicate()?); - } else { - builder.filter_entry(ctx.glob_predicate()?); - } + match ctx.glob { + context::Glob::None => builder.filter_entry(ctx.regex_predicate()?), + context::Glob::Sensitive | context::Glob::Insensitive => { + builder.filter_entry(ctx.glob_predicate()?) + } + }; } Ok(builder.build_parallel()) diff --git a/tests/glob.rs b/tests/glob.rs index ff370aa3..a1b7f740 100644 --- a/tests/glob.rs +++ b/tests/glob.rs @@ -5,7 +5,7 @@ mod utils; #[test] fn glob() { assert_eq!( - utils::run_cmd(&["--glob", "--pattern", "*.txt", "tests/data"]), + utils::run_cmd(&["--glob", "sensitive", "--pattern", "*.txt", "tests/data"]), indoc!( "100 B ┌─ nylarlathotep.txt 161 B ├─ nemesis.txt @@ -24,7 +24,7 @@ fn glob() { #[test] fn glob_negative() { assert_eq!( - utils::run_cmd(&["--glob", "--pattern", "!*.txt", "tests/data"]), + utils::run_cmd(&["--glob", "sensitive", "--pattern", "!*.txt", "tests/data"]), indoc!( "143 B ┌─ cassildas_song.md 143 B ┌─ the_yellow_king @@ -38,7 +38,7 @@ fn glob_negative() { #[test] fn glob_case_insensitive() { assert_eq!( - utils::run_cmd(&["--iglob", "--pattern", "*.TXT", "tests/data"]), + utils::run_cmd(&["--glob", "insensitive", "--pattern", "*.TXT", "tests/data"]), indoc!( "100 B ┌─ nylarlathotep.txt 161 B ├─ nemesis.txt @@ -59,6 +59,7 @@ fn glob_with_filetype() { assert_eq!( utils::run_cmd(&[ "--glob", + "sensitive", "--pattern", "dream*", "--file-type", @@ -80,6 +81,7 @@ fn negated_glob_with_filetype() { assert_eq!( utils::run_cmd(&[ "--glob", + "sensitive", "--pattern", "!dream*", "--file-type", @@ -106,6 +108,7 @@ fn negated_glob_with_filetype() { fn glob_empty_set_dir() { utils::run_cmd(&[ "--glob", + "sensitive", "--pattern", "*.txt", "--file-type", @@ -117,5 +120,5 @@ fn glob_empty_set_dir() { #[test] #[should_panic] fn glob_empty_set_file() { - utils::run_cmd(&["--glob", "--pattern", "*weewoo*", "tests/data"]); + utils::run_cmd(&["--glob", "sensitive", "--pattern", "*weewoo*", "tests/data"]); } diff --git a/tests/prune.rs b/tests/prune.rs index 139d87df..96cc0637 100644 --- a/tests/prune.rs +++ b/tests/prune.rs @@ -5,7 +5,14 @@ mod utils; #[test] fn prune() { assert_eq!( - utils::run_cmd(&["--glob", "--pattern", "*.txt", "--prune", "tests/data"]), + utils::run_cmd(&[ + "--glob", + "sensitive", + "--pattern", + "*.txt", + "--prune", + "tests/data" + ]), indoc!( "100 B ┌─ nylarlathotep.txt 161 B ├─ nemesis.txt From 53db7f5043955f21454a0dfe3b12b2d3ed076186 Mon Sep 17 00:00:00 2001 From: KP64 Date: Thu, 27 Apr 2023 19:57:09 +0200 Subject: [PATCH 03/14] chore: :memo: Update README for new Globbing --- README.md | 176 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 91 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 0edecf4f..c12840b8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Crates.io](https://img.shields.io/crates/d/erdtree)](https://crates.io/crates/erdtree) `erdtree` is a modern, cross-platform, and multi-threaded filesystem and disk-usage analysis tool. The following are some feature highlights: + * Respects hidden file and gitignore rules by default. * Supports regular expressions and glob based searching by file-type. * Supports Unix-based file permissions (Unix systems only). @@ -24,36 +25,36 @@ You can think of `erdtree` as a combination of `du`, `tree`, `find`, and `ls`. * [Usage](#usage) * [Installation](#installation) * [Documentation](#documentation) - - [Configuration file](#configuration-file) - - [Hardlinks](#hardlinks) - - [Symlinks](#symlinks) - - [Disk usage](#disk-usage) - - [Flat view](#flat-view) - - [gitignore](#gitignore) - - [Hidden files](#hidden-files) - - [Icons](#icons) - - [Maximum depth](#maximum-depth) - - [Pruning empty directories](#pruning-empty-directories) - - [Sorting](#sorting) - - [Directories only](#directories-only) - - [Permissions](#permissions) - - [Regular expressions and globbing](#regular-expressions-and-globbing) - - [Truncating output](#truncating-output) - - [Redirecting output and colorization](#redirecting-output-and-colorization) - - [Parallelism](#parallelism) - - [Completions](#completions) + * [Configuration file](#configuration-file) + * [Hardlinks](#hardlinks) + * [Symlinks](#symlinks) + * [Disk usage](#disk-usage) + * [Flat view](#flat-view) + * [gitignore](#gitignore) + * [Hidden files](#hidden-files) + * [Icons](#icons) + * [Maximum depth](#maximum-depth) + * [Pruning empty directories](#pruning-empty-directories) + * [Sorting](#sorting) + * [Directories only](#directories-only) + * [Permissions](#permissions) + * [Regular expressions and globbing](#regular-expressions-and-globbing) + * [Truncating output](#truncating-output) + * [Redirecting output and colorization](#redirecting-output-and-colorization) + * [Parallelism](#parallelism) + * [Completions](#completions) * [Comparisons against similar programs](#comparisons-against-similar-programs) - - [exa](#exa) - - [dua](#dua) - - [dust](#dust) - - [fd](#fd) + * [exa](#exa) + * [dua](#dua) + * [dust](#dust) + * [fd](#fd) * [Rules for contributing](#rules-for-contributing) * [Security policy](#security-policy) * [Questions you might have](#questions-you-might-have) ## Usage -``` +```txt erdtree (erd) is a cross-platform multi-threaded filesystem and disk usage analysis tool. Usage: erd [OPTIONS] [DIR] @@ -73,9 +74,8 @@ Options: --octal Show permissions in numeric octal format instead of symbolic --time