diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 896e0c923e1..10d72341c9e 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -13,7 +13,9 @@ use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write}; use std::num::TryFromIntError; #[cfg(unix)] use std::os::fd::AsFd; -use std::path::{Path, PathBuf}; +#[cfg(windows)] +use std::path::Path; +use std::path::PathBuf; use thiserror::Error; use uucore::display::{Quotable, print_verbatim}; use uucore::error::{FromIo, UError, UResult, USimpleError}; @@ -447,34 +449,66 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { Ok(()) } else { - // Stat the path first so we know whether to print the header. + // When 0 bytes or 0 lines are requested, there is nothing to + // read, so we should succeed on directories just like GNU head + // does. Skip opening the file entirely in that case. + let zero_output = matches!(options.mode, Mode::FirstBytes(0) | Mode::FirstLines(0)); + // GNU head prints "==> name <==" for existing files and // directories, but NOT for nonexistent ones — those produce // only an error message. - let metadata = match Path::new(file).metadata() { - Ok(m) => m, + let mut print_header = || -> UResult<()> { + if (options.files.len() > 1 && !options.quiet) || options.verbose { + if !first { + writeln!(stdout)?; + } + write!(stdout, "==> ")?; + print_verbatim(file).unwrap(); + writeln!(stdout, " <==")?; + first = false; + } + Ok(()) + }; + + let mut file_handle = match File::open(file) { + Ok(f) => f, Err(err) => { + #[cfg(windows)] + // On Windows, `File::open` on a directory fails with "Permission denied"). + if err.kind() == io::ErrorKind::PermissionDenied { + if let Ok(m) = Path::new(file).metadata() { + if m.is_dir() { + // We need to print the header, as we have an existing directory + print_header()?; + if !zero_output { + show!(USimpleError::new( + 1, + translate!("head-error-reading-file", "name" => file.quote(), "err" => "Is a directory") + )); + } + continue; + } + } + } + show!(err.map_err_context( || translate!("head-error-cannot-open", "name" => file.quote()) )); continue; } }; - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - writeln!(stdout)?; + + let metadata = match file_handle.metadata() { + Ok(m) => m, + Err(err) => { + show!(err.map_err_context( + || translate!("head-error-cannot-open", "name" => file.quote()) + )); + continue; } - write!(stdout, "==> ")?; - print_verbatim(file).unwrap(); - writeln!(stdout, " <==")?; - first = false; - } - // When 0 bytes or 0 lines are requested, there is nothing to - // read, so we should succeed on directories just like GNU head - // does. Skip opening the file entirely in that case (also - // avoids platform differences: on Windows, `File::open` on a - // directory fails with "Permission denied"). - let zero_output = matches!(options.mode, Mode::FirstBytes(0) | Mode::FirstLines(0)); + }; + + print_header()?; if metadata.is_dir() { if !zero_output { show!(USimpleError::new( @@ -484,15 +518,6 @@ fn uu_head(options: &HeadOptions) -> UResult<()> { } continue; } - let mut file_handle = match File::open(file) { - Ok(f) => f, - Err(err) => { - show!(err.map_err_context( - || translate!("head-error-cannot-open", "name" => file.quote()) - )); - continue; - } - }; head_file(&mut file_handle, options)?; Ok(()) };