Skip to content

Commit 28cff68

Browse files
sylvestrecakebaker
authored andcommitted
head: fix -c0 and -n0 on directories to match GNU behavior
When zero bytes or zero lines are requested, there is nothing to read, so we should not check whether the path is a directory. GNU head succeeds in this case because it never attempts to open/read the file. Previously, uutils head would stat the file and reject directories even when no reading was needed, causing a spurious 'Is a directory' error. Fixes #12215
1 parent 0b04f5e commit 28cff68

2 files changed

Lines changed: 78 additions & 9 deletions

File tree

src/uu/head/src/head.rs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -447,15 +447,12 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
447447

448448
Ok(())
449449
} else {
450-
if Path::new(file).is_dir() {
451-
show!(USimpleError::new(
452-
1,
453-
translate!("head-error-reading-file", "name" => file.quote(), "err" => "Is a directory")
454-
));
455-
continue;
456-
}
457-
let mut file_handle = match File::open(file) {
458-
Ok(f) => f,
450+
// Stat the path first so we know whether to print the header.
451+
// GNU head prints "==> name <==" for existing files and
452+
// directories, but NOT for nonexistent ones — those produce
453+
// only an error message.
454+
let metadata = match Path::new(file).metadata() {
455+
Ok(m) => m,
459456
Err(err) => {
460457
show!(err.map_err_context(
461458
|| translate!("head-error-cannot-open", "name" => file.quote())
@@ -470,7 +467,32 @@ fn uu_head(options: &HeadOptions) -> UResult<()> {
470467
write!(stdout, "==> ")?;
471468
print_verbatim(file).unwrap();
472469
writeln!(stdout, " <==")?;
470+
first = false;
473471
}
472+
// When 0 bytes or 0 lines are requested, there is nothing to
473+
// read, so we should succeed on directories just like GNU head
474+
// does. Skip opening the file entirely in that case (also
475+
// avoids platform differences: on Windows, `File::open` on a
476+
// directory fails with "Permission denied").
477+
let zero_output = matches!(options.mode, Mode::FirstBytes(0) | Mode::FirstLines(0));
478+
if metadata.is_dir() {
479+
if !zero_output {
480+
show!(USimpleError::new(
481+
1,
482+
translate!("head-error-reading-file", "name" => file.quote(), "err" => "Is a directory")
483+
));
484+
}
485+
continue;
486+
}
487+
let mut file_handle = match File::open(file) {
488+
Ok(f) => f,
489+
Err(err) => {
490+
show!(err.map_err_context(
491+
|| translate!("head-error-cannot-open", "name" => file.quote())
492+
));
493+
continue;
494+
}
495+
};
474496
head_file(&mut file_handle, options)?;
475497
Ok(())
476498
};

tests/by-util/test_head.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,3 +933,50 @@ fn test_do_not_attempt_to_read_a_directory() {
933933
.fails_with_code(1)
934934
.stderr_contains("error reading '.'");
935935
}
936+
937+
/// Regression test for https://github.com/uutils/coreutils/issues/12215
938+
/// `head -c0 <directory>` should succeed (nothing to read), matching GNU.
939+
#[test]
940+
fn test_zero_bytes_on_directory_succeeds() {
941+
new_ucmd!().args(&["-c", "0", "."]).succeeds().no_output();
942+
}
943+
944+
/// `head -n0 <directory>` should also succeed.
945+
#[test]
946+
fn test_zero_lines_on_directory_succeeds() {
947+
new_ucmd!().args(&["-n", "0", "."]).succeeds().no_output();
948+
}
949+
950+
/// GNU `head` prints the `==> name <==` header for every file argument
951+
/// (including directories) when invoked with multiple files. With non-zero
952+
/// output, directories also produce an "Is a directory" error after the
953+
/// header.
954+
#[cfg(not(windows))]
955+
#[test]
956+
fn test_directory_header_with_multiple_files() {
957+
let ts = TestScenario::new(util_name!());
958+
let at = &ts.fixtures;
959+
at.mkdir("d");
960+
at.write("f", "hello\n");
961+
ts.ucmd()
962+
.args(&["-c", "5", "d", "f"])
963+
.fails_with_code(1)
964+
.stdout_is("==> d <==\n\n==> f <==\nhello")
965+
.stderr_contains("Is a directory");
966+
}
967+
968+
/// With `-c 0` (zero output), the directory header is still printed but no
969+
/// error is emitted.
970+
#[cfg(not(windows))]
971+
#[test]
972+
fn test_directory_header_with_multiple_files_zero_output() {
973+
let ts = TestScenario::new(util_name!());
974+
let at = &ts.fixtures;
975+
at.mkdir("d");
976+
at.write("f", "hello\n");
977+
ts.ucmd()
978+
.args(&["-c", "0", "d", "f"])
979+
.succeeds()
980+
.stdout_is("==> d <==\n\n==> f <==\n")
981+
.no_stderr();
982+
}

0 commit comments

Comments
 (0)