diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 1213e004f15..e3f5a718654 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -65,6 +65,7 @@ mod options { pub const INODES: &str = "inodes"; pub const EXCLUDE: &str = "exclude"; pub const EXCLUDE_FROM: &str = "exclude-from"; + pub const FILES0_FROM: &str = "files0-from"; pub const VERBOSE: &str = "verbose"; pub const FILE: &str = "FILE"; } @@ -587,6 +588,49 @@ pub fn div_ceil(a: u64, b: u64) -> u64 { (a + b - 1) / b } +// Read file paths from the specified file, separated by null characters +fn read_files_from(file_name: &str) -> Result, std::io::Error> { + let reader: Box = if file_name == "-" { + // Read from standard input + Box::new(BufReader::new(std::io::stdin())) + } else { + // First, check if the file_name is a directory + let path = PathBuf::from(file_name); + if path.is_dir() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("{}: read error: Is a directory", file_name), + )); + } + + // Attempt to open the file and handle the error if it does not exist + match File::open(file_name) { + Ok(file) => Box::new(BufReader::new(file)), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "cannot open '{}' for reading: No such file or directory", + file_name + ), + )) + } + Err(e) => return Err(e), + } + }; + + let mut paths = Vec::new(); + + for line in reader.split(b'\0') { + let path = line?; + if !path.is_empty() { + paths.push(PathBuf::from(String::from_utf8_lossy(&path).to_string())); + } + } + + Ok(paths) +} + #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -601,13 +645,28 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { summarize, )?; - let files = match matches.get_one::(options::FILE) { - Some(_) => matches - .get_many::(options::FILE) - .unwrap() - .map(PathBuf::from) - .collect(), - None => vec![PathBuf::from(".")], + let files = if let Some(file_from) = matches.get_one::(options::FILES0_FROM) { + if file_from == "-" && matches.get_one::(options::FILE).is_some() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "extra operand {}\nfile operands cannot be combined with --files0-from", + matches.get_one::(options::FILE).unwrap().quote() + ), + ) + .into()); + } + + read_files_from(file_from)? + } else { + match matches.get_one::(options::FILE) { + Some(_) => matches + .get_many::(options::FILE) + .unwrap() + .map(PathBuf::from) + .collect(), + None => vec![PathBuf::from(".")], + } }; let time = matches.contains_id(options::TIME).then(|| { @@ -954,6 +1013,14 @@ pub fn uu_app() -> Command { .help("exclude files that match any pattern in FILE") .action(ArgAction::Append) ) + .arg( + Arg::new(options::FILES0_FROM) + .long("files0-from") + .value_name("FILE") + .value_hint(clap::ValueHint::FilePath) + .help("summarize device usage of the NUL-terminated file names specified in file F; if F is -, then read names from standard input") + .action(ArgAction::Append) + ) .arg( Arg::new(options::TIME) .long(options::TIME) diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 72ffb22ffb5..27560cbdcca 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -3,10 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (paths) sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink +// spell-checker:ignore (paths) sublink subwords azerty azeaze xcwww azeaz amaz azea qzerty tazerty tsublink testfile1 testfile2 filelist testdir testfile #[cfg(not(windows))] use regex::Regex; -#[cfg(not(windows))] use std::io::Write; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -991,3 +990,74 @@ fn test_du_symlink_multiple_fail() { assert_eq!(result.code(), 1); result.stdout_contains("4\tfile1\n"); } + +#[test] +// Disable on Windows because of different path separators and handling of null characters +#[cfg(not(target_os = "windows"))] +fn test_du_files0_from() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let mut file1 = at.make_file("testfile1"); + file1.write_all(b"content1").unwrap(); + let mut file2 = at.make_file("testfile2"); + file2.write_all(b"content2").unwrap(); + + at.mkdir("testdir"); + let mut file3 = at.make_file("testdir/testfile3"); + file3.write_all(b"content3").unwrap(); + + let mut file_list = at.make_file("filelist"); + write!(file_list, "testfile1\0testfile2\0testdir\0").unwrap(); + + ts.ucmd() + .arg("--files0-from=filelist") + .succeeds() + .stdout_contains("testfile1") + .stdout_contains("testfile2") + .stdout_contains("testdir"); +} + +#[test] +fn test_du_files0_from_stdin() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let mut file1 = at.make_file("testfile1"); + file1.write_all(b"content1").unwrap(); + let mut file2 = at.make_file("testfile2"); + file2.write_all(b"content2").unwrap(); + + let input = "testfile1\0testfile2\0"; + + ts.ucmd() + .arg("--files0-from=-") + .pipe_in(input) + .succeeds() + .stdout_contains("testfile1") + .stdout_contains("testfile2"); +} + +#[test] +fn test_du_files0_from_dir() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir"); + + let result = ts.ucmd().arg("--files0-from=dir").fails(); + assert_eq!(result.stderr_str(), "du: dir: read error: Is a directory\n"); +} + +#[test] +fn test_du_files0_from_combined() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.mkdir("dir"); + + let result = ts.ucmd().arg("--files0-from=-").arg("foo").fails(); + let stderr = result.stderr_str(); + + assert!(stderr.contains("file operands cannot be combined with --files0-from")); +} diff --git a/util/build-gnu.sh b/util/build-gnu.sh index dbddc8a315e..18545dd4817 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -283,6 +283,10 @@ sed -i -E "s|^([^#]*2_31.*)$|#\1|g" tests/printf/printf-cov.pl sed -i -e "s/du: invalid -t argument/du: invalid --threshold argument/" -e "s/du: option requires an argument/error: a value is required for '--threshold ' but none was supplied/" -e "/Try 'du --help' for more information./d" tests/du/threshold.sh +# Remove the extra output check +sed -i -e "s|Try '\$prog --help' for more information.\\\n||" tests/du/files0-from.pl +sed -i -e "s|when reading file names from stdin, no file name of\"|-: No such file or directory\n\"|" -e "s| '-' allowed\\\n||" tests/du/files0-from.pl + awk 'BEGIN {count=0} /compare exp out2/ && count < 6 {sub(/compare exp out2/, "grep -q \"cannot be used with\" out2"); count++} 1' tests/df/df-output.sh > tests/df/df-output.sh.tmp && mv tests/df/df-output.sh.tmp tests/df/df-output.sh # with ls --dired, in case of error, we have a slightly different error position