diff --git a/src/uu/tail/src/paths.rs b/src/uu/tail/src/paths.rs index 158535ea2..5c56ff844 100644 --- a/src/uu/tail/src/paths.rs +++ b/src/uu/tail/src/paths.rs @@ -78,14 +78,18 @@ impl Input { path.canonicalize().ok() } InputKind::File(_) | InputKind::Stdin => { - if cfg!(unix) { - match PathBuf::from(text::DEV_STDIN).canonicalize().ok() { - Some(path) if path != PathBuf::from(text::FD0) => Some(path), - Some(_) | None => None, - } - } else { + // on macOS, /dev/fd isn't backed by /proc and canonicalize() + // on dev/fd/0 (or /dev/stdin) will fail (NotFound), + // so we treat stdin as a pipe here + // https://github.com/rust-lang/rust/issues/95239 + #[cfg(target_os = "macos")] + { None } + #[cfg(not(target_os = "macos"))] + { + PathBuf::from(text::FD0).canonicalize().ok() + } } } } diff --git a/src/uu/tail/src/tail.rs b/src/uu/tail/src/tail.rs index 665ee1ea9..2a6f9eb23 100644 --- a/src/uu/tail/src/tail.rs +++ b/src/uu/tail/src/tail.rs @@ -189,6 +189,29 @@ fn tail_stdin( input: &Input, observer: &mut Observer, ) -> UResult<()> { + // on macOS, resolve() will always return None for stdin, + // we need to detect if stdin is a directory ourselves. + // fstat-ing certain descriptors under /dev/fd fails with + // bad file descriptor or might not catch directory cases + // e.g. see the differences between running ls -l /dev/stdin /dev/fd/0 + // on macOS and Linux. + #[cfg(target_os = "macos")] + { + if let Ok(mut stdin_handle) = Handle::stdin() { + if let Ok(meta) = stdin_handle.as_file_mut().metadata() { + if meta.file_type().is_dir() { + set_exit_code(1); + show_error!( + "cannot open '{}' for reading: {}", + input.display_name, + text::NO_SUCH_FILE + ); + return Ok(()); + } + } + } + } + match input.resolve() { // fifo Some(path) => { diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index b5fba824d..736182bfe 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -346,6 +346,55 @@ fn test_stdin_redirect_dir_when_target_os_is_macos() { .stderr_is("tail: cannot open 'standard input' for reading: No such file or directory\n"); } +#[test] +#[cfg(unix)] +fn test_stdin_via_script_redirection_and_pipe() { + // $ touch file.txt + // $ echo line1 > file.txt + // $ echo line2 >> file.txt + // $ chmod +x test.sh + // $ ./test.sh < file.txt + // line1 + // line2 + // $ cat file.txt | ./test.sh + // line1 + // line2 + use std::os::unix::fs::PermissionsExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let data = "line1\nline2\n"; + + at.write("file.txt", data); + + let mut script = at.make_file("test.sh"); + writeln!(script, "#!/usr/bin/env sh").unwrap(); + writeln!(script, "tail").unwrap(); + script + .set_permissions(PermissionsExt::from_mode(0o755)) + .unwrap(); + + drop(script); // close the file handle to ensure file is not busy + + // test with redirection + scene + .cmd("sh") + .current_dir(at.plus("")) + .arg("-c") + .arg("./test.sh < file.txt") + .succeeds() + .stdout_only(data); + + // test with pipe + scene + .cmd("sh") + .current_dir(at.plus("")) + .arg("-c") + .arg("cat file.txt | ./test.sh") + .succeeds() + .stdout_only(data); +} + #[test] fn test_follow_stdin_descriptor() { let ts = TestScenario::new(util_name!());