diff --git a/USAGE.md b/USAGE.md index 98d1f36c..ec64faa8 100644 --- a/USAGE.md +++ b/USAGE.md @@ -94,6 +94,11 @@ Minimal size proportion required to appear. Do not sort the branches in the tree. + +### `--dev` + +Stay on the same filesystem, do not cross mount points (POSIX only). + ### `--silent-errors` diff --git a/exports/completion.bash b/exports/completion.bash index 8b06a03b..a2a0de77 100644 --- a/exports/completion.bash +++ b/exports/completion.bash @@ -23,7 +23,7 @@ _pdu() { case "${cmd}" in pdu) - opts="-b -H -q -d -w -m -s -p -h -V --json-input --json-output --bytes-format --detect-links --dedupe-links --deduplicate-hardlinks --top-down --align-right --quantity --depth --max-depth --width --total-width --column-width --min-ratio --no-sort --no-errors --silent-errors --progress --threads --omit-json-shared-details --omit-json-shared-summary --help --version [FILES]..." + opts="-b -H -q -d -w -m -s -p -h -V --json-input --json-output --bytes-format --detect-links --dedupe-links --deduplicate-hardlinks --top-down --align-right --quantity --depth --max-depth --width --total-width --column-width --min-ratio --no-sort --dev --no-errors --silent-errors --progress --threads --omit-json-shared-details --omit-json-shared-summary --help --version [FILES]..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/exports/completion.elv b/exports/completion.elv index d3cda52c..f547a528 100644 --- a/exports/completion.elv +++ b/exports/completion.elv @@ -41,6 +41,7 @@ set edit:completion:arg-completer[pdu] = {|@words| cand --top-down 'Print the tree top-down instead of bottom-up' cand --align-right 'Set the root of the bars to the right' cand --no-sort 'Do not sort the branches in the tree' + cand --dev 'Stay on the same filesystem, do not cross mount points (POSIX only)' cand -s 'Prevent filesystem error messages from appearing in stderr' cand --silent-errors 'Prevent filesystem error messages from appearing in stderr' cand --no-errors 'Prevent filesystem error messages from appearing in stderr' diff --git a/exports/completion.fish b/exports/completion.fish index 41cc6448..8f16b3ce 100644 --- a/exports/completion.fish +++ b/exports/completion.fish @@ -15,6 +15,7 @@ complete -c pdu -s H -l deduplicate-hardlinks -l detect-links -l dedupe-links -d complete -c pdu -l top-down -d 'Print the tree top-down instead of bottom-up' complete -c pdu -l align-right -d 'Set the root of the bars to the right' complete -c pdu -l no-sort -d 'Do not sort the branches in the tree' +complete -c pdu -l dev -d 'Stay on the same filesystem, do not cross mount points (POSIX only)' complete -c pdu -s s -l silent-errors -l no-errors -d 'Prevent filesystem error messages from appearing in stderr' complete -c pdu -s p -l progress -d 'Report progress being made at the expense of performance' complete -c pdu -l omit-json-shared-details -d 'Do not output `.shared.details` in the JSON output' diff --git a/exports/completion.ps1 b/exports/completion.ps1 index 8814bf76..bddd181e 100644 --- a/exports/completion.ps1 +++ b/exports/completion.ps1 @@ -44,6 +44,7 @@ Register-ArgumentCompleter -Native -CommandName 'pdu' -ScriptBlock { [CompletionResult]::new('--top-down', '--top-down', [CompletionResultType]::ParameterName, 'Print the tree top-down instead of bottom-up') [CompletionResult]::new('--align-right', '--align-right', [CompletionResultType]::ParameterName, 'Set the root of the bars to the right') [CompletionResult]::new('--no-sort', '--no-sort', [CompletionResultType]::ParameterName, 'Do not sort the branches in the tree') + [CompletionResult]::new('--dev', '--dev', [CompletionResultType]::ParameterName, 'Stay on the same filesystem, do not cross mount points (POSIX only)') [CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Prevent filesystem error messages from appearing in stderr') [CompletionResult]::new('--silent-errors', '--silent-errors', [CompletionResultType]::ParameterName, 'Prevent filesystem error messages from appearing in stderr') [CompletionResult]::new('--no-errors', '--no-errors', [CompletionResultType]::ParameterName, 'Prevent filesystem error messages from appearing in stderr') diff --git a/exports/completion.zsh b/exports/completion.zsh index dec1cef4..68174ad3 100644 --- a/exports/completion.zsh +++ b/exports/completion.zsh @@ -46,6 +46,7 @@ block-count\:"Count numbers of blocks"))' \ '--top-down[Print the tree top-down instead of bottom-up]' \ '--align-right[Set the root of the bars to the right]' \ '--no-sort[Do not sort the branches in the tree]' \ +'--dev[Stay on the same filesystem, do not cross mount points (POSIX only)]' \ '-s[Prevent filesystem error messages from appearing in stderr]' \ '--silent-errors[Prevent filesystem error messages from appearing in stderr]' \ '--no-errors[Prevent filesystem error messages from appearing in stderr]' \ diff --git a/exports/long.help b/exports/long.help index efe31299..6ef9f669 100644 --- a/exports/long.help +++ b/exports/long.help @@ -69,6 +69,9 @@ Options: --no-sort Do not sort the branches in the tree + --dev + Stay on the same filesystem, do not cross mount points (POSIX only) + -s, --silent-errors Prevent filesystem error messages from appearing in stderr diff --git a/exports/short.help b/exports/short.help index 1835edbc..a947377f 100644 --- a/exports/short.help +++ b/exports/short.help @@ -30,6 +30,8 @@ Options: Minimal size proportion required to appear [default: 0.01] --no-sort Do not sort the branches in the tree + --dev + Stay on the same filesystem, do not cross mount points (POSIX only) -s, --silent-errors Prevent filesystem error messages from appearing in stderr [aliases: --no-errors] -p, --progress diff --git a/src/app.rs b/src/app.rs index 4569b257..94dde24c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -16,7 +16,7 @@ use crate::{ use clap::Parser; use hdd::any_path_is_in_hdd; use pipe_trait::Pipe; -use std::{io::stdin, time::Duration}; +use std::{io::stdin, path::PathBuf, time::Duration}; use sub::JsonOutputParam; use sysinfo::{Disk, Disks}; @@ -133,6 +133,17 @@ impl App { .pipe(Err); } + #[cfg(not(unix))] + if self.args.dev { + return crate::runtime_error::UnsupportedFeature::Dev + .pipe(RuntimeError::UnsupportedFeature) + .pipe(Err); + } + + if self.args.dev && self.args.files.len() > 1 { + return Err(RuntimeError::DevArgConflict); + } + let threads = match self.args.threads { Threads::Auto => { let disks = Disks::new_with_refreshed_list(); @@ -275,7 +286,7 @@ impl App { macro_rules! run { ($( $(#[$variant_attrs:meta])* - $size_getter:ident, $progress:literal, $hardlinks:ident; + $size_getter:ident, $progress:literal, $hardlinks:ident, $dev:ident; )*) => { match self.args {$( $(#[$variant_attrs])* Args { @@ -283,6 +294,8 @@ impl App { progress: $progress, #[cfg(unix)] deduplicate_hardlinks: $hardlinks, #[cfg(not(unix))] deduplicate_hardlinks: _, + #[cfg(unix)] dev: $dev, + #[cfg(not(unix))] dev: _, files, json_output, bytes_format, @@ -294,41 +307,78 @@ impl App { omit_json_shared_details, omit_json_shared_summary, .. - } => Sub { - direction: Direction::from_top_down(top_down), - bar_alignment: BarAlignment::from_align_right(align_right), - size_getter: <$size_getter as GetSizeUtils>::INSTANCE, - hardlinks_handler: <$size_getter as CreateHardlinksHandler<{ cfg!(unix) && $hardlinks }, $progress>>::create_hardlinks_handler(), - reporter: <$size_getter as CreateReporter<$progress>>::create_reporter(report_error), - bytes_format: <$size_getter as GetSizeUtils>::formatter(bytes_format), - files, - json_output: JsonOutputParam::from_cli_flags(json_output, omit_json_shared_details, omit_json_shared_summary), - column_width_distribution, - max_depth, - min_ratio, - no_sort, - } - .run(), + } => { + let root_dev = if cfg!(unix) && $dev { + get_root_dev(&files) + } else { + None + }; + Sub { + direction: Direction::from_top_down(top_down), + bar_alignment: BarAlignment::from_align_right(align_right), + size_getter: <$size_getter as GetSizeUtils>::INSTANCE, + hardlinks_handler: <$size_getter as CreateHardlinksHandler<{ cfg!(unix) && $hardlinks }, $progress>>::create_hardlinks_handler(), + reporter: <$size_getter as CreateReporter<$progress>>::create_reporter(report_error), + bytes_format: <$size_getter as GetSizeUtils>::formatter(bytes_format), + files, + json_output: JsonOutputParam::from_cli_flags(json_output, omit_json_shared_details, omit_json_shared_summary), + column_width_distribution, + max_depth, + min_ratio, + no_sort, + root_dev, + } + .run() + }, )*} }; } run! { - GetApparentSize, false, false; - GetApparentSize, true, false; - #[cfg(unix)] GetBlockSize, false, false; - #[cfg(unix)] GetBlockSize, true, false; - #[cfg(unix)] GetBlockCount, false, false; - #[cfg(unix)] GetBlockCount, true, false; - #[cfg(unix)] GetApparentSize, false, true; - #[cfg(unix)] GetApparentSize, true, true; - #[cfg(unix)] GetBlockSize, false, true; - #[cfg(unix)] GetBlockSize, true, true; - #[cfg(unix)] GetBlockCount, false, true; - #[cfg(unix)] GetBlockCount, true, true; + GetApparentSize, false, false, false; + GetApparentSize, true, false, false; + #[cfg(unix)] GetBlockSize, false, false, false; + #[cfg(unix)] GetBlockSize, true, false, false; + #[cfg(unix)] GetBlockCount, false, false, false; + #[cfg(unix)] GetBlockCount, true, false, false; + #[cfg(unix)] GetApparentSize, false, true, false; + #[cfg(unix)] GetApparentSize, true, true, false; + #[cfg(unix)] GetBlockSize, false, true, false; + #[cfg(unix)] GetBlockSize, true, true, false; + #[cfg(unix)] GetBlockCount, false, true, false; + #[cfg(unix)] GetBlockCount, true, true, false; + #[cfg(unix)] GetApparentSize, false, false, true; + #[cfg(unix)] GetApparentSize, true, false, true; + #[cfg(unix)] GetBlockSize, false, false, true; + #[cfg(unix)] GetBlockSize, true, false, true; + #[cfg(unix)] GetBlockCount, false, false, true; + #[cfg(unix)] GetBlockCount, true, false, true; + #[cfg(unix)] GetApparentSize, false, true, true; + #[cfg(unix)] GetApparentSize, true, true, true; + #[cfg(unix)] GetBlockSize, false, true, true; + #[cfg(unix)] GetBlockSize, true, true, true; + #[cfg(unix)] GetBlockCount, false, true, true; + #[cfg(unix)] GetBlockCount, true, true, true; } } } +/// Get the device ID of the root path for `--dev` filtering. +#[cfg(unix)] +fn get_root_dev(files: &[PathBuf]) -> Option { + use std::os::unix::fs::MetadataExt; + let root_path = files + .first() + .map(|p| p.as_path()) + .unwrap_or(std::path::Path::new(".")); + std::fs::symlink_metadata(root_path).ok().map(|m| m.dev()) +} + +/// Get the device ID of the root path for `--dev` filtering. +#[cfg(not(unix))] +fn get_root_dev(_files: &[PathBuf]) -> Option { + None +} + mod hdd; mod mount_point; mod overlapping_arguments; diff --git a/src/app/sub.rs b/src/app/sub.rs index 3500a5f3..0d3fe429 100644 --- a/src/app/sub.rs +++ b/src/app/sub.rs @@ -49,6 +49,8 @@ where pub min_ratio: Fraction, /// Preserve order of entries. pub no_sort: bool, + /// Device ID of the root directory. When `Some`, entries on different devices are skipped. + pub root_dev: Option, } impl Sub @@ -74,6 +76,7 @@ where reporter, min_ratio, no_sort, + root_dev, } = self; let max_depth = max_depth.get(); @@ -87,6 +90,7 @@ where size_getter, hardlinks_recorder: &hardlinks_handler, max_depth, + root_dev, } .into() }); diff --git a/src/args.rs b/src/args.rs index db6698c8..2e185e97 100644 --- a/src/args.rs +++ b/src/args.rs @@ -151,6 +151,11 @@ pub struct Args { #[clap(long)] pub no_sort: bool, + /// Stay on the same filesystem, do not cross mount points (POSIX only). + #[clap(long)] + #[cfg_attr(not(unix), clap(hide = true))] + pub dev: bool, + /// Prevent filesystem error messages from appearing in stderr. #[clap(long, short, visible_alias = "no-errors")] pub silent_errors: bool, diff --git a/src/fs_tree_builder.rs b/src/fs_tree_builder.rs index 37167b2c..7f44bf00 100644 --- a/src/fs_tree_builder.rs +++ b/src/fs_tree_builder.rs @@ -33,6 +33,7 @@ use std::{ /// size_getter: GetApparentSize, /// reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), /// max_depth: 10, +/// root_dev: None, /// }; /// let data_tree: DataTree = builder.into(); /// ``` @@ -54,6 +55,8 @@ where pub reporter: &'a Report, /// Deepest level of descendant display in the graph. The sizes beyond the max depth still count toward total. pub max_depth: u64, + /// Device ID of the root directory. When `Some`, entries on different devices are skipped. + pub root_dev: Option, } impl<'a, Size, SizeGetter, HardlinksRecorder, Report> @@ -67,12 +70,14 @@ where { /// Create a [`DataTree`] from an [`FsTreeBuilder`]. fn from(builder: FsTreeBuilder) -> Self { + #[allow(unused_variables)] // root_dev is only used in #[cfg(unix)] block below let FsTreeBuilder { root, size_getter, hardlinks_recorder, reporter, max_depth, + root_dev, } = builder; TreeBuilder:: { @@ -94,6 +99,17 @@ where }; } Ok(stats) => { + // When --dev is active, skip entries on different filesystems + #[cfg(unix)] + if let Some(root_dev) = root_dev { + use std::os::unix::fs::MetadataExt; + if stats.dev() != root_dev { + return Info { + size: Size::default(), + children: Vec::new(), + }; + } + } // `stats` should be dropped ASAP to avoid piling up kernel memory usage let is_dir = stats.is_dir(); let size = size_getter.get_size(&stats); diff --git a/src/runtime_error.rs b/src/runtime_error.rs index dba94c00..9a48b504 100644 --- a/src/runtime_error.rs +++ b/src/runtime_error.rs @@ -19,6 +19,9 @@ pub enum RuntimeError { /// When input JSON data is not a valid tree. #[display("InvalidInputReflection: {_0}")] InvalidInputReflection(#[error(not(source))] String), + /// When `--dev` is used with more than one argument. + #[display("DevArgConflict: --dev cannot be used with more than one path argument")] + DevArgConflict, /// When the user attempts to use unavailable platform-specific features. #[display("UnsupportedFeature: {_0}")] UnsupportedFeature(UnsupportedFeature), @@ -32,6 +35,10 @@ pub enum UnsupportedFeature { #[cfg(not(unix))] #[display("Feature --deduplicate-hardlinks is not available on this platform")] DeduplicateHardlink, + /// Using `--dev` on non-POSIX. + #[cfg(not(unix))] + #[display("Feature --dev is not available on this platform")] + Dev, } impl From for RuntimeError { @@ -48,7 +55,8 @@ impl RuntimeError { RuntimeError::DeserializationFailure(_) => 3, RuntimeError::JsonInputArgConflict => 4, RuntimeError::InvalidInputReflection(_) => 5, - RuntimeError::UnsupportedFeature(_) => 6, + RuntimeError::DevArgConflict => 6, + RuntimeError::UnsupportedFeature(_) => 7, }) } } diff --git a/tests/_utils.rs b/tests/_utils.rs index 75b4a326..eaa8e07a 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -374,6 +374,7 @@ where }), root: root.join(suffix), max_depth: 10, + root_dev: None, } .pipe(DataTree::::from) .into_par_sorted(|left, right| left.name().cmp(right.name())) diff --git a/tests/cli_errors.rs b/tests/cli_errors.rs index c33f0d9a..36718d35 100644 --- a/tests/cli_errors.rs +++ b/tests/cli_errors.rs @@ -136,6 +136,7 @@ fn fs_errors() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); diff --git a/tests/json.rs b/tests/json.rs index 95aee0b9..e139f743 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -86,6 +86,7 @@ fn json_output() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let expected = builder .pipe(DataTree::<_, Bytes>::from) diff --git a/tests/usual_cli.rs b/tests/usual_cli.rs index cbacb9f2..690a0bd9 100644 --- a/tests/usual_cli.rs +++ b/tests/usual_cli.rs @@ -46,6 +46,7 @@ fn total_width() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -85,6 +86,7 @@ fn column_width() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -124,6 +126,7 @@ fn min_ratio_0() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); @@ -162,6 +165,7 @@ fn min_ratio() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.1); @@ -201,6 +205,7 @@ fn max_depth_2() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 2, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -240,6 +245,7 @@ fn max_depth_1() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 1, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -278,6 +284,7 @@ fn top_down() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -316,6 +323,7 @@ fn align_right() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -354,6 +362,7 @@ fn quantity_apparent_size() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -393,6 +402,7 @@ fn quantity_block_size() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -432,6 +442,7 @@ fn quantity_block_count() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -471,6 +482,7 @@ fn bytes_format_plain() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -510,6 +522,7 @@ fn bytes_format_metric() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -549,6 +562,7 @@ fn bytes_format_binary() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -587,6 +601,7 @@ fn path_to_workspace() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); data_tree.par_cull_insignificant_data(0.01); @@ -630,6 +645,7 @@ fn multiple_names() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); *data_tree.name_mut() = OsStringDisplay::os_string_from(name); @@ -695,6 +711,7 @@ fn multiple_names_max_depth_2() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 1, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); *data_tree.name_mut() = OsStringDisplay::os_string_from(name); @@ -755,6 +772,7 @@ fn multiple_names_max_depth_1() { hardlinks_recorder: &HardlinkIgnorant, reporter: &ErrorOnlyReporter::new(ErrorReport::SILENT), max_depth: 10, + root_dev: None, }; let mut data_tree: DataTree = builder.into(); *data_tree.name_mut() = OsStringDisplay::os_string_from(name);