Skip to content

Tail macos stdin ran from script fix #7844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 4, 2025
16 changes: 10 additions & 6 deletions src/uu/tail/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a comment explaining why macos is empty

None
}
#[cfg(not(target_os = "macos"))]
{
PathBuf::from(text::FD0).canonicalize().ok()
}
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions src/uu/tail/src/tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and add a comment here why macos is special :)

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) => {
Expand Down
49 changes: 49 additions & 0 deletions tests/by-util/test_tail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems that it is failing on freebsd:

  ──── TRY 1 STDOUT:       coreutils::tests test_tail::test_stdin_via_script_redirection_and_pipe
  
  running 1 test
  bin: "/home/runner/work/coreutils/coreutils/target/debug/coreutils"
  write(default): /tmp/.tmpqaoG0W/file.txt
  run: sh -c ./test.sh < file.txt
  test test_tail::test_stdin_via_script_redirection_and_pipe ... FAILED
  
  failures:
  
  failures:
      test_tail::test_stdin_via_script_redirection_and_pipe
  
  test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 3718 filtered out; finished in 0.04s
  
  ──── TRY 1 STDERR:       coreutils::tests test_tail::test_stdin_via_script_redirection_and_pipe
  
  thread 'test_tail::test_stdin_via_script_redirection_and_pipe' panicked at tests/by-util/test_tail.rs:385:10:
  Command was expected to succeed. code: 127
  stdout = 
   stderr = sh: ./test.sh: not found
  
  stack backtrace:
     0: rust_begin_unwind
               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/std/src/panicking.rs:695:5
     1: core::panicking::panic_fmt
               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/panicking.rs:75:14
     2: uutests::util::CmdResult::success
               at ./tests/uutests/src/lib/util.rs:442:9
     3: uutests::util::UCommand::succeeds
               at ./tests/uutests/src/lib/util.rs:1929:9
     4: tests::test_tail::test_stdin_via_script_redirection_and_pipe
               at ./tests/by-util/test_tail.rs:381:5
     5: tests::test_tail::test_stdin_via_script_redirection_and_pipe::{{closure}}
               at ./tests/by-util/test_tail.rs:351:48
     6: core::ops::function::FnOnce::call_once
               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/ops/function.rs:250:5
     7: core::ops::function::FnOnce::call_once
               at /rustc/05f9846f893b09a1be1fc8560e33fc3c815cfecb/library/core/src/ops/function.rs:250:5
  note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for pointing this out, subtleties in FreeBSD

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!());
Expand Down
Loading