Compare commits

...

4 Commits

Author SHA1 Message Date
andy.boot
7f93c90f5e tests: Cleanup test_exact_ouput 2024-06-19 22:39:20 +01:00
n4n5
08e4240b41 add cli 2024-06-19 21:48:55 +01:00
n4n5
028ca1fdc7 eprint problematic folders 2024-06-19 21:48:55 +01:00
andy.boot
4f6255971b refactor: cleanup -j / --output-json flag
Stop -j writing to a file, print to stdout instead.
2024-06-19 21:39:09 +01:00
15 changed files with 148 additions and 91 deletions

View File

@@ -91,6 +91,7 @@ Usage: dust -R (For screen readers. Removes bars/symbols. Adds new column: depth
Usage: dust -S (Custom Stack size - Use if you see: 'fatal runtime error: stack overflow' (default allocation: low memory=1048576, high memory=1073741824)"),
Usage: dust --skip-total (No total row will be displayed)
Usage: dust -z 40000/30MB/20kib (Exclude output files/directories below size 40000 bytes / 30MB / 20KiB)
Usage: dust -j (Prints JSON representation of directories, try: dust -j | jq)
```
## Config file

View File

@@ -66,6 +66,7 @@ _dust() {
'(-d --depth -D --only-dir)--file_types[show only these file types]' \
'-P[Disable the progress indication.]' \
'--no-progress[Disable the progress indication.]' \
'--print-errors[Print path with errors.]' \
'(-F --only-file -t --file_types)-D[Only directories will be displayed.]' \
'(-F --only-file -t --file_types)--only-dir[Only directories will be displayed.]' \
'(-D --only-dir)-F[Only files will be displayed. (Finds your largest files)]' \

View File

@@ -72,6 +72,7 @@ Register-ArgumentCompleter -Native -CommandName 'dust' -ScriptBlock {
[CompletionResult]::new('--file_types', 'file_types', [CompletionResultType]::ParameterName, 'show only these file types')
[CompletionResult]::new('-P', 'P ', [CompletionResultType]::ParameterName, 'Disable the progress indication.')
[CompletionResult]::new('--no-progress', 'no-progress', [CompletionResultType]::ParameterName, 'Disable the progress indication.')
[CompletionResult]::new('--print-errors', 'print-errors', [CompletionResultType]::ParameterName, 'Print path with errors.')
[CompletionResult]::new('-D', 'D ', [CompletionResultType]::ParameterName, 'Only directories will be displayed.')
[CompletionResult]::new('--only-dir', 'only-dir', [CompletionResultType]::ParameterName, 'Only directories will be displayed.')
[CompletionResult]::new('-F', 'F ', [CompletionResultType]::ParameterName, 'Only files will be displayed. (Finds your largest files)')

View File

@@ -19,7 +19,7 @@ _dust() {
case "${cmd}" in
dust)
opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -h -V --depth --threads --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --no-progress --only-dir --only-file --output-format --stack-size --output-json --help --version [PATH]..."
opts="-d -T -n -p -X -I -L -x -s -r -c -C -b -B -z -R -f -i -v -e -t -w -P -D -F -o -S -j -h -V --depth --threads --number-of-lines --full-paths --ignore-directory --ignore-all-in-file --dereference-links --limit-filesystem --apparent-size --reverse --no-colors --force-colors --no-percent-bars --bars-on-right --min-size --screen-reader --skip-total --filecount --ignore_hidden --invert-filter --filter --file_types --terminal_width --no-progress --print-errors --only-dir --only-file --output-format --stack-size --output-json --help --version [PATH]..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0

View File

@@ -69,6 +69,7 @@ set edit:completion:arg-completer[dust] = {|@words|
cand --file_types 'show only these file types'
cand -P 'Disable the progress indication.'
cand --no-progress 'Disable the progress indication.'
cand --print-errors 'Print path with errors.'
cand -D 'Only directories will be displayed.'
cand --only-dir 'Only directories will be displayed.'
cand -F 'Only files will be displayed. (Finds your largest files)'

View File

@@ -24,6 +24,7 @@ complete -c dust -s f -l filecount -d 'Directory \'size\' is number of child fil
complete -c dust -s i -l ignore_hidden -d 'Do not display hidden files'
complete -c dust -s t -l file_types -d 'show only these file types'
complete -c dust -s P -l no-progress -d 'Disable the progress indication.'
complete -c dust -l print-errors -d 'Print path with errors.'
complete -c dust -s D -l only-dir -d 'Only directories will be displayed.'
complete -c dust -s F -l only-file -d 'Only files will be displayed. (Finds your largest files)'
complete -c dust -s j -l output-json -d 'Output the directory tree as json to the current directory'

View File

@@ -4,7 +4,7 @@
.SH NAME
Dust \- Like du but more intuitive
.SH SYNOPSIS
\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore_hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file_types\fR] [\fB\-w\fR|\fB\-\-terminal_width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR]
\fBdust\fR [\fB\-d\fR|\fB\-\-depth\fR] [\fB\-T\fR|\fB\-\-threads\fR] [\fB\-n\fR|\fB\-\-number\-of\-lines\fR] [\fB\-p\fR|\fB\-\-full\-paths\fR] [\fB\-X\fR|\fB\-\-ignore\-directory\fR] [\fB\-I\fR|\fB\-\-ignore\-all\-in\-file\fR] [\fB\-L\fR|\fB\-\-dereference\-links\fR] [\fB\-x\fR|\fB\-\-limit\-filesystem\fR] [\fB\-s\fR|\fB\-\-apparent\-size\fR] [\fB\-r\fR|\fB\-\-reverse\fR] [\fB\-c\fR|\fB\-\-no\-colors\fR] [\fB\-C\fR|\fB\-\-force\-colors\fR] [\fB\-b\fR|\fB\-\-no\-percent\-bars\fR] [\fB\-B\fR|\fB\-\-bars\-on\-right\fR] [\fB\-z\fR|\fB\-\-min\-size\fR] [\fB\-R\fR|\fB\-\-screen\-reader\fR] [\fB\-\-skip\-total\fR] [\fB\-f\fR|\fB\-\-filecount\fR] [\fB\-i\fR|\fB\-\-ignore_hidden\fR] [\fB\-v\fR|\fB\-\-invert\-filter\fR] [\fB\-e\fR|\fB\-\-filter\fR] [\fB\-t\fR|\fB\-\-file_types\fR] [\fB\-w\fR|\fB\-\-terminal_width\fR] [\fB\-P\fR|\fB\-\-no\-progress\fR] [\fB\-\-print\-errors\fR] [\fB\-D\fR|\fB\-\-only\-dir\fR] [\fB\-F\fR|\fB\-\-only\-file\fR] [\fB\-o\fR|\fB\-\-output\-format\fR] [\fB\-S\fR|\fB\-\-stack\-size\fR] [\fB\-j\fR|\fB\-\-output\-json\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fIPATH\fR]
.SH DESCRIPTION
Like du but more intuitive
.SH OPTIONS
@@ -81,6 +81,9 @@ Specify width of output overriding the auto detection of terminal width
\fB\-P\fR, \fB\-\-no\-progress\fR
Disable the progress indication.
.TP
\fB\-\-print\-errors\fR
Print path with errors.
.TP
\fB\-D\fR, \fB\-\-only\-dir\fR
Only directories will be displayed.
.TP

View File

@@ -193,6 +193,12 @@ pub fn build_cli() -> Command {
.action(clap::ArgAction::SetTrue)
.help("Disable the progress indication."),
)
.arg(
Arg::new("print_errors")
.long("print-errors")
.action(clap::ArgAction::SetTrue)
.help("Print path with errors."),
)
.arg(
Arg::new("only_dir")
.short('D')

View File

@@ -31,6 +31,7 @@ pub struct Config {
pub stack_size: Option<usize>,
pub threads: Option<usize>,
pub output_json: Option<bool>,
pub print_errors: Option<bool>,
}
impl Config {
@@ -102,6 +103,10 @@ impl Config {
pub fn get_only_dir(&self, options: &ArgMatches) -> bool {
Some(true) == self.only_dir || options.get_flag("only_dir")
}
pub fn get_print_errors(&self, options: &ArgMatches) -> bool {
Some(true) == self.print_errors || options.get_flag("print_errors")
}
pub fn get_only_file(&self, options: &ArgMatches) -> bool {
Some(true) == self.only_file || options.get_flag("only_file")
}

View File

@@ -142,44 +142,49 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
.into_iter()
.par_bridge()
.filter_map(|entry| {
if let Ok(ref entry) = entry {
// uncommenting the below line gives simpler code but
// rayon doesn't parallelize as well giving a 3X performance drop
// hence we unravel the recursion a bit
match entry {
Ok(ref entry) => {
// uncommenting the below line gives simpler code but
// rayon doesn't parallelize as well giving a 3X performance drop
// hence we unravel the recursion a bit
// return walk(entry.path(), walk_data, depth)
// return walk(entry.path(), walk_data, depth)
if !ignore_file(entry, walk_data) {
if let Ok(data) = entry.file_type() {
if data.is_dir()
|| (walk_data.follow_links && data.is_symlink())
{
return walk(entry.path(), walk_data, depth + 1);
if !ignore_file(entry, walk_data) {
if let Ok(data) = entry.file_type() {
if data.is_dir()
|| (walk_data.follow_links && data.is_symlink())
{
return walk(entry.path(), walk_data, depth + 1);
}
let node = build_node(
entry.path(),
vec![],
walk_data.filter_regex,
walk_data.invert_filter_regex,
walk_data.use_apparent_size,
data.is_symlink(),
data.is_file(),
walk_data.by_filecount,
depth,
);
prog_data.num_files.fetch_add(1, ORDERING);
if let Some(ref file) = node {
prog_data
.total_file_size
.fetch_add(file.size, ORDERING);
}
return node;
}
let node = build_node(
entry.path(),
vec![],
walk_data.filter_regex,
walk_data.invert_filter_regex,
walk_data.use_apparent_size,
data.is_symlink(),
data.is_file(),
walk_data.by_filecount,
depth,
);
prog_data.num_files.fetch_add(1, ORDERING);
if let Some(ref file) = node {
prog_data.total_file_size.fetch_add(file.size, ORDERING);
}
return node;
}
}
} else {
let mut editable_error = errors.lock().unwrap();
editable_error.no_permissions = true
Err(ref failed) => {
let mut editable_error = errors.lock().unwrap();
editable_error.no_permissions.insert(failed.to_string());
}
}
None
})
@@ -189,7 +194,9 @@ fn walk(dir: PathBuf, walk_data: &WalkData, depth: usize) -> Option<Node> {
let mut editable_error = errors.lock().unwrap();
match failed.kind() {
std::io::ErrorKind::PermissionDenied => {
editable_error.no_permissions = true;
editable_error
.no_permissions
.insert(dir.to_string_lossy().into());
}
std::io::ErrorKind::NotFound => {
editable_error.file_not_found.insert(failed.to_string());

View File

@@ -1,6 +1,8 @@
use std::path::PathBuf;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
use serde::Serialize;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize)]
pub struct DisplayNode {
// Note: the order of fields in important here, for PartialEq and PartialOrd
pub size: u64,

View File

@@ -39,11 +39,6 @@ use terminal_size::{terminal_size, Height, Width};
use utils::get_filesystem_devices;
use utils::simplify_dir_names;
use crate::node::Node;
use chrono::Local;
use std::fs::File;
use std::io::Write;
static DEFAULT_NUMBER_OF_LINES: usize = 30;
static DEFAULT_TERMINAL_WIDTH: usize = 80;
@@ -234,18 +229,6 @@ fn main() {
let top_level_nodes = walk_it(simplified_dirs, &walk_data);
if config.get_output_json(&options) {
let datetime = Local::now().format("%Y%m%d%H%M%S").to_string();
let output_filename = format!("node-{}.json", datetime);
let result = output_json(&output_filename, &top_level_nodes);
match result {
Ok(..) => {}
Err(err) => {
eprintln!("Error: {}", err)
}
}
}
let tree = match summarize_file_types {
true => get_all_file_types(&top_level_nodes, number_of_lines),
false => {
@@ -269,7 +252,6 @@ fn main() {
}
let final_errors = walk_data.errors.lock().unwrap();
let failed_permissions = final_errors.no_permissions;
if !final_errors.file_not_found.is_empty() {
let err = final_errors
.file_not_found
@@ -279,8 +261,20 @@ fn main() {
.join(", ");
eprintln!("No such file or directory: {}", err);
}
if failed_permissions {
eprintln!("Did not have permissions for all directories");
if !final_errors.no_permissions.is_empty() {
if config.get_print_errors(&options) {
let err = final_errors
.no_permissions
.iter()
.map(|a| a.as_ref())
.collect::<Vec<&str>>()
.join(", ");
eprintln!("Did not have permissions for directories: {}", err);
} else {
eprintln!(
"Did not have permissions for all directories (add --print-errors to see errors)"
);
}
}
if !final_errors.unknown_error.is_empty() {
let err = final_errors
@@ -302,13 +296,18 @@ fn main() {
output_format,
bars_on_right: config.get_bars_on_right(&options),
};
draw_it(
idd,
config.get_no_bars(&options),
terminal_width,
&root_node,
config.get_skip_total(&options),
)
if config.get_output_json(&options) {
println!("{}", serde_json::to_string(&root_node).unwrap());
} else {
draw_it(
idd,
config.get_no_bars(&options),
terminal_width,
&root_node,
config.get_skip_total(&options),
)
}
}
}
@@ -323,13 +322,6 @@ fn init_rayon(stack_size: &Option<usize>, threads: &Option<usize>) {
}
}
fn output_json(output_filename: &str, top_level_nodes: &Vec<Node>) -> std::io::Result<()> {
let serialized: String = serde_json::to_string(&top_level_nodes).unwrap();
let mut file = File::create(output_filename)?;
file.write_all(serialized.as_bytes())?;
Ok(())
}
fn build_thread_pool(
stack: Option<usize>,
threads: Option<usize>,

View File

@@ -3,11 +3,10 @@ use crate::utils::is_filtered_out_due_to_invert_regex;
use crate::utils::is_filtered_out_due_to_regex;
use regex::Regex;
use serde::Serialize;
use std::cmp::Ordering;
use std::path::PathBuf;
#[derive(Debug, Eq, Clone, Serialize)]
#[derive(Debug, Eq, Clone)]
pub struct Node {
pub name: PathBuf,
pub size: u64,

View File

@@ -70,7 +70,7 @@ impl PAtomicInfo {
#[derive(Default)]
pub struct RuntimeErrors {
pub no_permissions: bool,
pub no_permissions: HashSet<String>,
pub file_not_found: HashSet<String>,
pub unknown_error: HashSet<String>,
pub abort: bool,

View File

@@ -1,5 +1,6 @@
use assert_cmd::Command;
use std::ffi::OsStr;
use std::process::Output;
use std::str;
use std::sync::Once;
@@ -38,29 +39,44 @@ fn initialize() {
copy_test_data("tests/test_dir");
copy_test_data("tests/test_dir2");
copy_test_data("tests/test_dir_unicode");
Command::new("sh")
.arg("-c")
.arg("mkdir -p /tmp/unreadable_folder && chmod 000 /tmp/unreadable_folder")
.output()
.unwrap();
});
}
fn exact_output_test<T: AsRef<OsStr>>(valid_outputs: Vec<String>, command_args: Vec<T>) {
fn run_cmd<T: AsRef<OsStr>>(command_args: Vec<T>) -> Output {
initialize();
let mut a = &mut Command::cargo_bin("dust").unwrap();
let mut to_run = &mut Command::cargo_bin("dust").unwrap();
for p in command_args {
a = a.arg(p);
to_run = to_run.arg(p);
}
to_run.unwrap()
}
let output = str::from_utf8(&a.unwrap().stdout).unwrap().to_owned();
fn exact_stdout_test<T: AsRef<OsStr>>(command_args: Vec<T>, valid_stdout: Vec<String>) {
let to_run = run_cmd(command_args);
let will_fail = valid_outputs.iter().any(|i| output.contains(i));
let stdout_output = str::from_utf8(&to_run.stdout).unwrap().to_owned();
let will_fail = valid_stdout.iter().any(|i| stdout_output.contains(i));
if !will_fail {
eprintln!(
"output:\n{}\ndoes not contain any of:\n{}",
output,
valid_outputs.join("\n\n")
"output(stdout):\n{}\ndoes not contain any of:\n{}",
stdout_output,
valid_stdout.join("\n\n")
);
}
assert!(will_fail)
assert!(will_fail);
}
fn exact_stderr_test<T: AsRef<OsStr>>(command_args: Vec<T>, valid_stderr: String) {
let to_run = run_cmd(command_args);
let stderr_output = str::from_utf8(&to_run.stderr).unwrap().trim();
assert_eq!(stderr_output, valid_stderr);
}
// "windows" result data can vary by host (size seems to be variable by one byte); fix code vs test and re-enable
@@ -68,7 +84,7 @@ fn exact_output_test<T: AsRef<OsStr>>(valid_outputs: Vec<String>, command_args:
#[test]
pub fn test_main_basic() {
// -c is no color mode - This makes testing much simpler
exact_output_test(main_output(), vec!["-c", "-B", "/tmp/test_dir/"])
exact_stdout_test(vec!["-c", "-B", "/tmp/test_dir/"], main_output());
}
#[cfg_attr(target_os = "windows", ignore)]
@@ -81,7 +97,7 @@ pub fn test_main_multi_arg() {
"/tmp/test_dir",
"/tmp/test_dir",
];
exact_output_test(main_output(), command_args);
exact_stdout_test(command_args, main_output());
}
fn main_output() -> Vec<String> {
@@ -112,7 +128,7 @@ fn main_output() -> Vec<String> {
#[test]
pub fn test_main_long_paths() {
let command_args = vec!["-c", "-p", "-B", "/tmp/test_dir/"];
exact_output_test(main_output_long_paths(), command_args);
exact_stdout_test(command_args, main_output_long_paths());
}
fn main_output_long_paths() -> Vec<String> {
@@ -140,7 +156,7 @@ fn main_output_long_paths() -> Vec<String> {
#[test]
pub fn test_substring_of_names_and_long_names() {
let command_args = vec!["-c", "-B", "/tmp/test_dir2"];
exact_output_test(no_substring_of_names_output(), command_args);
exact_stdout_test(command_args, no_substring_of_names_output());
}
fn no_substring_of_names_output() -> Vec<String> {
@@ -174,7 +190,7 @@ fn no_substring_of_names_output() -> Vec<String> {
#[test]
pub fn test_unicode_directories() {
let command_args = vec!["-c", "-B", "/tmp/test_dir_unicode"];
exact_output_test(unicode_dir(), command_args);
exact_stdout_test(command_args, unicode_dir());
}
fn unicode_dir() -> Vec<String> {
@@ -201,7 +217,7 @@ fn unicode_dir() -> Vec<String> {
#[test]
pub fn test_apparent_size() {
let command_args = vec!["-c", "-s", "-b", "/tmp/test_dir"];
exact_output_test(apparent_size_output(), command_args);
exact_stdout_test(command_args, apparent_size_output());
}
fn apparent_size_output() -> Vec<String> {
@@ -222,3 +238,25 @@ fn apparent_size_output() -> Vec<String> {
vec![one_space_before, two_space_before]
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_permission_normal() {
let command_args = vec!["/tmp/unreadable_folder"];
let permission_msg =
r#"Did not have permissions for all directories (add --print-errors to see errors)"#
.trim()
.to_string();
exact_stderr_test(command_args, permission_msg);
}
#[cfg_attr(target_os = "windows", ignore)]
#[test]
pub fn test_permission_flag() {
// add the flag to CLI
let command_args = vec!["--print-errors", "/tmp/unreadable_folder"];
let permission_msg = r#"Did not have permissions for directories: /tmp/unreadable_folder"#
.trim()
.to_string();
exact_stderr_test(command_args, permission_msg);
}