diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 3ad6e07f916..a70a0269ce1 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -1080,6 +1080,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let (print_tx, rx) = mpsc::channel::>(); let printing_thread = thread::spawn(move || stat_printer.print_stats(&rx)); + // Check existence of path provided in argument + let mut seen_inodes: HashSet = HashSet::new(); + 'loop_file: for path in files { // Skip if we don't want to ignore anything if !&traversal_options.excludes.is_empty() { @@ -1098,9 +1101,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } - // Check existence of path provided in argument - let mut seen_inodes: HashSet = HashSet::new(); - // Determine which traversal method to use #[cfg(all(unix, not(target_os = "redox")))] let use_safe_traversal = traversal_options.dereference != Deref::All; @@ -1114,6 +1114,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Pre-populate seen_inodes with the starting directory to detect cycles if let Ok(stat) = Stat::new(&path, None, &traversal_options) { if let Some(inode) = stat.inode { + if !traversal_options.count_links && seen_inodes.contains(&inode) { + continue 'loop_file; + } seen_inodes.insert(inode); } } @@ -1147,6 +1150,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Use regular traversal (non-Linux or when -L is used) if let Ok(stat) = Stat::new(&path, None, &traversal_options) { if let Some(inode) = stat.inode { + if !traversal_options.count_links && seen_inodes.contains(&inode) { + continue 'loop_file; + } seen_inodes.insert(inode); } let stat = du_regular( diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 38d64d5b8b8..c89cd46678c 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -2003,3 +2003,58 @@ fn test_du_long_path_from_unreadable() { perms.set_mode(0o755); fs::set_permissions(&inaccessible_path, perms).unwrap(); } + +#[test] +#[cfg(target_os = "linux")] +fn test_du_hard_links_multiple_dirs_in_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir1"); + at.mkdir("dir2"); + at.write("dir1/file", "hello world"); + at.hard_link("dir1/file", "dir2/link"); + + let result = ts.ucmd().args(&["dir1", "dir2"]).succeeds(); + let lines: Vec<&str> = result.stdout_str().lines().collect(); + let size = |i: usize| lines[i].split_once('\t').unwrap().0.parse::().unwrap(); + assert!(size(0) > size(1)); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_du_hard_links_multiple_links_in_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir1"); + at.write("dir1/file", "hello world"); + at.hard_link("dir1/file", "dir1/link"); + + let result = ts.ucmd().args(&["dir1/file", "dir1/link"]).succeeds(); + result.stdout_contains("dir1/file"); + result.stdout_does_not_contain("dir1/link"); + + let result = ts.ucmd().args(&["-L", "dir1/file", "dir1/link"]).succeeds(); + result.stdout_contains("dir1/file"); + result.stdout_does_not_contain("dir1/link"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_du_symlinks_multiple_links_in_args() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir1"); + at.write("dir1/file", "hello world"); + at.symlink_file("dir1/file", "dir1/link"); + + let result = ts.ucmd().args(&["dir1/file", "dir1/link"]).succeeds(); + result.stdout_contains("dir1/file"); + result.stdout_contains("dir1/link"); + + let result = ts.ucmd().args(&["-L", "dir1/file", "dir1/link"]).succeeds(); + result.stdout_contains("dir1/file"); + result.stdout_does_not_contain("dir1/link"); +}