From ad19ba5bfe304280d428e2cdbf97c13e2d554eee Mon Sep 17 00:00:00 2001 From: "andy.boot" Date: Mon, 22 Aug 2022 11:17:21 +0100 Subject: [PATCH 1/4] Feature: Add min-size parameter Add often requested feature. '--min-size 50000' will only include files above a size of 50kB --- completions/_dust | 2 ++ completions/_dust.ps1 | 2 ++ completions/dust.bash | 10 +++++- completions/dust.elv | 2 ++ completions/dust.fish | 1 + src/cli.rs | 8 +++++ src/config.rs | 73 +++++++++++++++++++++++++++++++++++++++++++ src/display.rs | 2 +- src/filter.rs | 16 +++++----- src/main.rs | 5 +-- 10 files changed, 109 insertions(+), 12 deletions(-) diff --git a/completions/_dust b/completions/_dust index b9d50af5..2391d2df 100644 --- a/completions/_dust +++ b/completions/_dust @@ -21,6 +21,8 @@ _dust() { '--number-of-lines=[Number of lines of output to show. (Default is terminal_height - 10)]: : ' \ '*-X+[Exclude any file or directory with this name]: : ' \ '*--ignore-directory=[Exclude any file or directory with this name]: : ' \ +'-z+[Minimum size file to include in output]: : ' \ +'--min-size=[Minimum size file to include in output]: : ' \ '(-e --filter -t --file_types)*-v+[Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ]: : ' \ '(-e --filter -t --file_types)*--invert-filter=[Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ]: : ' \ '(-t --file_types)*-e+[Only include filepaths matching this regex. For png files type: -e "\\.png$" ]: : ' \ diff --git a/completions/_dust.ps1 b/completions/_dust.ps1 index e08646e9..18e30376 100644 --- a/completions/_dust.ps1 +++ b/completions/_dust.ps1 @@ -27,6 +27,8 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock { [CompletionResult]::new('--number-of-lines', 'number-of-lines', [CompletionResultType]::ParameterName, 'Number of lines of output to show. (Default is terminal_height - 10)') [CompletionResult]::new('-X', 'X', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this name') [CompletionResult]::new('--ignore-directory', 'ignore-directory', [CompletionResultType]::ParameterName, 'Exclude any file or directory with this name') + [CompletionResult]::new('-z', 'z', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') + [CompletionResult]::new('--min-size', 'min-size', [CompletionResultType]::ParameterName, 'Minimum size file to include in output') [CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ') [CompletionResult]::new('--invert-filter', 'invert-filter', [CompletionResultType]::ParameterName, 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ') [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Only include filepaths matching this regex. For png files type: -e "\.png$" ') diff --git a/completions/dust.bash b/completions/dust.bash index aabf8ec8..f261db5e 100644 --- a/completions/dust.bash +++ b/completions/dust.bash @@ -19,7 +19,7 @@ _dust() { case "${cmd}" in dust) - opts="-h -V -d -n -p -X -x -s -r -c -b -f -i -v -e -t -w -H --help --version --depth --number-of-lines --full-paths --ignore-directory --limit-filesystem --apparent-size --reverse --no-colors --no-percent-bars --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --si ..." + opts="-h -V -d -n -p -X -x -s -r -c -b -z -f -i -v -e -t -w -H --help --version --depth --number-of-lines --full-paths --ignore-directory --limit-filesystem --apparent-size --reverse --no-colors --no-percent-bars --min-size --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --si ..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -49,6 +49,14 @@ _dust() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; + --min-size) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; + -z) + COMPREPLY=($(compgen -f "${cur}")) + return 0 + ;; --invert-filter) COMPREPLY=($(compgen -f "${cur}")) return 0 diff --git a/completions/dust.elv b/completions/dust.elv index 4ef4edea..e0ddc2ac 100644 --- a/completions/dust.elv +++ b/completions/dust.elv @@ -24,6 +24,8 @@ set edit:completion:arg-completer[dust] = {|@words| cand --number-of-lines 'Number of lines of output to show. (Default is terminal_height - 10)' cand -X 'Exclude any file or directory with this name' cand --ignore-directory 'Exclude any file or directory with this name' + cand -z 'Minimum size file to include in output' + cand --min-size 'Minimum size file to include in output' cand -v 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ' cand --invert-filter 'Exclude filepaths matching this regex. To ignore png files type: -v "\.png$" ' cand -e 'Only include filepaths matching this regex. For png files type: -e "\.png$" ' diff --git a/completions/dust.fish b/completions/dust.fish index a3c5e9fe..1c2ccb3d 100644 --- a/completions/dust.fish +++ b/completions/dust.fish @@ -1,6 +1,7 @@ complete -c dust -s d -l depth -d 'Depth to show' -r complete -c dust -s n -l number-of-lines -d 'Number of lines of output to show. (Default is terminal_height - 10)' -r complete -c dust -s X -l ignore-directory -d 'Exclude any file or directory with this name' -r +complete -c dust -s z -l min-size -d 'Minimum size file to include in output' -r complete -c dust -s v -l invert-filter -d 'Exclude filepaths matching this regex. To ignore png files type: -v "\\.png$" ' -r complete -c dust -s e -l filter -d 'Only include filepaths matching this regex. For png files type: -e "\\.png$" ' -r complete -c dust -s w -l terminal_width -d 'Specify width of output overriding the auto detection of terminal width' -r diff --git a/src/cli.rs b/src/cli.rs index 08d67287..b4b46b99 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -64,6 +64,14 @@ pub fn build_cli() -> Command<'static> { .long("no-percent-bars") .help("No percent bars or percentages will be displayed"), ) + .arg( + Arg::new("min_size") + .short('z') + .long("min-size") + .takes_value(true) + .number_of_values(1) + .help("Minimum size file to include in output"), + ) .arg( Arg::new("skip_total") .long("skip-total") diff --git a/src/config.rs b/src/config.rs index 25e64bb6..183a2e91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,8 @@ use serde::Deserialize; use std::path::Path; use std::path::PathBuf; +use crate::display::UNITS; + #[derive(Deserialize, Default)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] @@ -16,6 +18,7 @@ pub struct Config { pub skip_total: Option, pub ignore_hidden: Option, pub iso: Option, + pub min_size: Option, } impl Config { @@ -43,6 +46,46 @@ impl Config { pub fn get_skip_total(&self, options: &ArgMatches) -> bool { Some(true) == self.skip_total || options.is_present("skip_total") } + pub fn get_min_size(&self, options: &ArgMatches, iso: bool) -> Option { + let size_from_param = options.value_of("min_size"); + self._get_min_size(size_from_param, iso) + } + fn _get_min_size(&self, min_size: Option<&str>, iso: bool) -> Option { + let size_from_param = min_size.and_then(|a| convert_min_size(a, iso)); + + if size_from_param.is_none() { + self.min_size + .as_ref() + .and_then(|a| convert_min_size(a.as_ref(), iso)) + } else { + size_from_param + } + } +} + +fn convert_min_size(input: &str, iso: bool) -> Option { + let chars_as_vec: Vec = input.chars().collect(); + match chars_as_vec.split_last() { + Some((last, start)) => { + let mut starts: String = start.iter().collect::(); + + for (i, u) in UNITS.iter().enumerate() { + if Some(*u) == last.to_uppercase().next() { + return match starts.parse::() { + Ok(pure) => { + let num: usize = if iso { 1000 } else { 1024 }; + let marker = pure * num.pow((UNITS.len() - i) as u32); + Some(marker) + } + Err(_) => None, + }; + } + } + starts.push(*last); + starts.parse().ok() + } + None => None, + } } fn get_config_locations(base: &Path) -> Vec { @@ -66,3 +109,33 @@ pub fn get_config() -> Config { ..Default::default() } } + +mod tests { + #[allow(unused_imports)] + use super::*; + + #[test] + fn test_conversion() { + assert_eq!(convert_min_size("55", false), Some(55)); + assert_eq!(convert_min_size("12344321", false), Some(12344321)); + assert_eq!(convert_min_size("95RUBBISH", false), None); + assert_eq!(convert_min_size("10K", false), Some(10 * 1024)); + assert_eq!(convert_min_size("10M", false), Some(10 * 1024usize.pow(2))); + assert_eq!(convert_min_size("10M", true), Some(10 * 1000usize.pow(2))); + assert_eq!(convert_min_size("2G", false), Some(2 * 1024usize.pow(3))); + } + + #[test] + fn test_config_min_size() { + let c = Config { + min_size: Some("1K".to_owned()), + ..Default::default() + }; + assert_eq!(c._get_min_size(None, false), Some(1024)); + assert_eq!(c._get_min_size(Some("100"), false), Some(100)); + assert_eq!(c._get_min_size(Some("2K"), false), Some(2048)); + + assert_eq!(c._get_min_size(None, true), Some(1000)); + assert_eq!(c._get_min_size(Some("2K"), true), Some(2000)); + } +} diff --git a/src/display.rs b/src/display.rs index 233d4fee..332f0b81 100644 --- a/src/display.rs +++ b/src/display.rs @@ -14,7 +14,7 @@ use std::iter::repeat; use std::path::Path; use thousands::Separable; -static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; +pub static UNITS: [char; 4] = ['T', 'G', 'M', 'K']; static BLOCKS: [char; 5] = ['█', '▓', '▒', '░', ' ']; pub struct DisplayData { diff --git a/src/filter.rs b/src/filter.rs index ed4be0a0..8f1d7b3e 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; pub fn get_biggest( top_level_nodes: Vec, + min_size: Option, n: usize, depth: usize, using_a_filter: bool, @@ -22,14 +23,14 @@ pub fn get_biggest( let mut allowed_nodes = HashSet::new(); allowed_nodes.insert(root.name.as_path()); - heap = add_children(using_a_filter, &root, depth, heap); + heap = add_children(using_a_filter, min_size, &root, depth, heap); for _ in number_top_level_nodes..n { let line = heap.pop(); match line { Some(line) => { allowed_nodes.insert(line.name.as_path()); - heap = add_children(using_a_filter, line, depth, heap); + heap = add_children(using_a_filter, min_size, line, depth, heap); } None => break, } @@ -39,17 +40,16 @@ pub fn get_biggest( fn add_children<'a>( using_a_filter: bool, + min_size: Option, file_or_folder: &'a Node, depth: usize, mut heap: BinaryHeap<&'a Node>, ) -> BinaryHeap<&'a Node> { if depth > file_or_folder.depth { - heap.extend( - file_or_folder - .children - .iter() - .filter(|c| !using_a_filter || c.name.is_file() || c.size > 0), - ) + heap.extend(file_or_folder.children.iter().filter(|c| match min_size { + Some(ms) => c.size > ms as u64, + None => !using_a_filter || c.name.is_file() || c.size > 0, + })) } heap } diff --git a/src/main.rs b/src/main.rs index cc9df046..f020f1ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -91,7 +91,6 @@ fn get_regex_value(maybe_value: Option) -> Vec { fn main() { let options = build_cli().get_matches(); - let config = get_config(); let target_dirs = options @@ -161,12 +160,14 @@ fn main() { .build_global() .unwrap(); + let iso = config.get_iso(&options); let (top_level_nodes, has_errors) = walk_it(simplified_dirs, walk_data); let tree = match summarize_file_types { true => get_all_file_types(&top_level_nodes, number_of_lines), false => get_biggest( top_level_nodes, + config.get_min_size(&options, iso), number_of_lines, depth, options.values_of("filter").is_some() || options.value_of("invert_filter").is_some(), @@ -185,7 +186,7 @@ fn main() { terminal_width, by_filecount, &root_node, - config.get_iso(&options), + iso, config.get_skip_total(&options), ) } From 74b8366fd2708e06adce238e66a15757d954c260 Mon Sep 17 00:00:00 2001 From: "andy.boot" Date: Tue, 23 Aug 2022 10:33:47 +0100 Subject: [PATCH 2/4] Refactor: Tidy up use of UNITS: k,m,g,t --- src/config.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index 183a2e91..3cd20197 100644 --- a/src/config.rs +++ b/src/config.rs @@ -69,12 +69,12 @@ fn convert_min_size(input: &str, iso: bool) -> Option { Some((last, start)) => { let mut starts: String = start.iter().collect::(); - for (i, u) in UNITS.iter().enumerate() { + for (i, u) in UNITS.iter().rev().enumerate() { if Some(*u) == last.to_uppercase().next() { return match starts.parse::() { Ok(pure) => { let num: usize = if iso { 1000 } else { 1024 }; - let marker = pure * num.pow((UNITS.len() - i) as u32); + let marker = pure * num.pow((i + 1) as u32); Some(marker) } Err(_) => None, From 0fa1bc5ef3ca00776b542aa52945cf62d3f6836a Mon Sep 17 00:00:00 2001 From: "andy.boot" Date: Tue, 23 Aug 2022 10:42:49 +0100 Subject: [PATCH 3/4] Feature: Add error message on bad min-size Log to stderr if the min-size from either parameter or config is invalid --- src/config.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3cd20197..80c22165 100644 --- a/src/config.rs +++ b/src/config.rs @@ -77,12 +77,20 @@ fn convert_min_size(input: &str, iso: bool) -> Option { let marker = pure * num.pow((i + 1) as u32); Some(marker) } - Err(_) => None, + Err(_) => { + eprintln!("Ignoring invalid min-size: {}", input); + None + } }; } } starts.push(*last); - starts.parse().ok() + starts + .parse() + .map_err(|_| { + eprintln!("Ignoring invalid min-size: {}", input); + }) + .ok() } None => None, } @@ -126,13 +134,12 @@ mod tests { } #[test] - fn test_config_min_size() { + fn test_min_size_from_config_applied_or_overridden() { let c = Config { min_size: Some("1K".to_owned()), ..Default::default() }; assert_eq!(c._get_min_size(None, false), Some(1024)); - assert_eq!(c._get_min_size(Some("100"), false), Some(100)); assert_eq!(c._get_min_size(Some("2K"), false), Some(2048)); assert_eq!(c._get_min_size(None, true), Some(1000)); From 9c846496b58cb647081adc5444fb0d1a8407e083 Mon Sep 17 00:00:00 2001 From: "andy.boot" Date: Tue, 23 Aug 2022 10:44:43 +0100 Subject: [PATCH 4/4] README: Update readme to include max-size --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e0f5612a..f2011130 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Usage: dust -i (Do not show hidden files) Usage: dust -c (No colors [monochrome]) Usage: dust -f (Count files instead of diskspace) Usage: dust -t (Group by filetype) +Usage: dust -z 10M (min-size, Only include files larger than 10M) Usage: dust -e regex (Only include files matching this regex (eg dust -e "\.png$" would match png files)) Usage: dust -v regex (Exculde files matching this regex (eg dust -v "\.png$" would ignore png files))