Skip to content

Commit

Permalink
du: implement files0-from (#5721)
Browse files Browse the repository at this point in the history
* du: implement files0-from

Should make tests/du/files0-from-dir pass

* du: prepare tests/du/files0-from.pl

* fix the build on Windows

* add testfile to the ignore list

* remove useless comment

Co-authored-by: Daniel Hofstetter <[email protected]>

* mkdir is enough

Co-authored-by: Daniel Hofstetter <[email protected]>

* address review comments

---------

Co-authored-by: Daniel Hofstetter <[email protected]>
  • Loading branch information
sylvestre and cakebaker authored Dec 26, 2023
1 parent 4acc96f commit 30eb77a
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 9 deletions.
81 changes: 74 additions & 7 deletions src/uu/du/src/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down Expand Up @@ -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<Vec<PathBuf>, std::io::Error> {
let reader: Box<dyn BufRead> = 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<()> {
Expand All @@ -601,13 +645,28 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
summarize,
)?;

let files = match matches.get_one::<String>(options::FILE) {
Some(_) => matches
.get_many::<String>(options::FILE)
.unwrap()
.map(PathBuf::from)
.collect(),
None => vec![PathBuf::from(".")],
let files = if let Some(file_from) = matches.get_one::<String>(options::FILES0_FROM) {
if file_from == "-" && matches.get_one::<String>(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::<String>(options::FILE).unwrap().quote()
),
)
.into());
}

read_files_from(file_from)?
} else {
match matches.get_one::<String>(options::FILE) {
Some(_) => matches
.get_many::<String>(options::FILE)
.unwrap()
.map(PathBuf::from)
.collect(),
None => vec![PathBuf::from(".")],
}
};

let time = matches.contains_id(options::TIME).then(|| {
Expand Down Expand Up @@ -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)
Expand Down
74 changes: 72 additions & 2 deletions tests/by-util/test_du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"))]
Expand Down Expand Up @@ -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"));
}
4 changes: 4 additions & 0 deletions util/build-gnu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SIZE>' 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
Expand Down

0 comments on commit 30eb77a

Please sign in to comment.