use std::process;
#[cfg(unix)]
use std::sync::atomic::{AtomicBool, Ordering};
const TOOL_NAME: &str = "yes";
const VERSION: &str = env!("CARGO_PKG_VERSION");
const BUF_SIZE: usize = 128 * 1024;
#[cfg(unix)]
static INHERITED_SIGPIPE_IGN: AtomicBool = AtomicBool::new(false);
#[cfg(unix)]
unsafe extern "C" fn sigpipe_check_init() {
unsafe {
let mut old: libc::sigaction = std::mem::zeroed();
if libc::sigaction(libc::SIGPIPE, std::ptr::null(), &mut old) == 0
&& old.sa_sigaction == libc::SIG_IGN
{
INHERITED_SIGPIPE_IGN.store(true, Ordering::Relaxed);
}
}
}
#[cfg(target_os = "linux")]
#[used]
#[unsafe(link_section = ".init_array")]
static SIGPIPE_INIT: unsafe extern "C" fn() = sigpipe_check_init;
#[cfg(target_os = "macos")]
#[used]
#[unsafe(link_section = "__DATA,__mod_init_func")]
static SIGPIPE_INIT: unsafe extern "C" fn() = sigpipe_check_init;
fn main() {
#[cfg(unix)]
unsafe {
if !INHERITED_SIGPIPE_IGN.load(Ordering::Relaxed) {
let mut sa: libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = libc::SIG_DFL;
sa.sa_flags = 0;
libc::sigemptyset(&mut sa.sa_mask);
libc::sigaction(libc::SIGPIPE, &sa, std::ptr::null_mut());
}
}
let raw_args: Vec<String> = std::env::args().skip(1).collect();
for arg in &raw_args {
if arg == "--" {
break; }
match arg.as_str() {
"--help" => {
println!("Usage: {} [STRING]...", TOOL_NAME);
println!(" or: {} OPTION", TOOL_NAME);
println!("Repeatedly output a line with all specified STRING(s), or 'y'.");
println!();
println!(" --help display this help and exit");
println!(" --version output version information and exit");
process::exit(0);
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
process::exit(0);
}
s if s.starts_with("--") => {
eprintln!(
"{}: unrecognized option '{}'\nTry '{} --help' for more information.",
TOOL_NAME, s, TOOL_NAME
);
process::exit(1);
}
s if s.starts_with('-') && s.len() > 1 => {
let first_char = s.as_bytes()[1] as char;
eprintln!(
"{}: invalid option -- '{}'\nTry '{} --help' for more information.",
TOOL_NAME, first_char, TOOL_NAME
);
process::exit(1);
}
_ => {}
}
}
let mut end_of_opts = false;
let mut output_args: Vec<&str> = Vec::new();
for arg in &raw_args {
if end_of_opts {
output_args.push(arg.as_str());
continue;
}
if arg == "--" {
end_of_opts = true;
continue;
}
output_args.push(arg.as_str());
}
let line = if output_args.is_empty() {
"y\n".to_string()
} else {
let mut s = output_args.join(" ");
s.push('\n');
s
};
let line_bytes = line.as_bytes();
let line_len = line_bytes.len();
#[cfg(target_os = "linux")]
let is_pipe = unsafe { libc::fcntl(1, libc::F_GETPIPE_SZ) > 0 };
#[cfg(not(target_os = "linux"))]
let is_pipe = false;
const GNU_BUFSIZ: usize = 8192;
let buf_target = if is_pipe { GNU_BUFSIZ } else { BUF_SIZE };
let buf = if line_len >= buf_target {
line_bytes.to_vec()
} else {
let copies = (buf_target / line_len).max(1);
let mut v = Vec::with_capacity(copies * line_len);
for _ in 0..copies {
v.extend_from_slice(line_bytes);
}
v
};
let total = buf.len();
let ptr = buf.as_ptr();
if is_pipe {
write_loop_libc(ptr, total);
} else {
write_loop_fast(ptr, total);
}
}
#[inline(never)]
fn write_loop_fast(ptr: *const u8, total: usize) -> ! {
let total_isize = total as isize;
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
loop {
let ret: isize;
unsafe {
std::arch::asm!(
"syscall",
in("rax") 1_u64, in("rdi") 1_u64, in("rsi") ptr, in("rdx") total, lateout("rax") ret,
lateout("rcx") _, lateout("r11") _, options(nostack),
);
}
if ret == total_isize {
continue; }
if ret > 0 {
drain_partial(ptr, total, ret as usize);
continue;
}
if ret == 0 {
process::exit(1);
}
let errno = (-ret) as i32;
if errno == libc::EINTR {
continue;
}
let err = std::io::Error::from_raw_os_error(errno);
write_error_and_exit(&err);
}
#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
loop {
let ret = unsafe { libc::write(1, ptr as *const libc::c_void, total as _) };
if ret as isize == total_isize {
continue;
}
if ret > 0 {
drain_partial(ptr, total, ret as usize);
continue;
}
if ret == 0 {
process::exit(1);
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
write_error_and_exit(&err);
}
}
#[inline(never)]
fn write_loop_libc(ptr: *const u8, total: usize) -> ! {
let total_isize = total as isize;
loop {
let ret = unsafe { libc::write(1, ptr as *const libc::c_void, total as _) };
if ret as isize == total_isize {
continue;
}
if ret > 0 {
drain_partial(ptr, total, ret as usize);
continue;
}
if ret == 0 {
process::exit(1);
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
write_error_and_exit(&err);
}
}
#[cold]
#[inline(never)]
fn drain_partial(ptr: *const u8, total: usize, initial: usize) {
let mut written = initial;
while written < total {
let r = unsafe {
libc::write(
1,
ptr.add(written) as *const libc::c_void,
(total - written) as _,
)
};
if r > 0 {
written += r as usize;
} else if r == 0 {
process::exit(1);
} else {
let e = std::io::Error::last_os_error();
if e.kind() == std::io::ErrorKind::Interrupted {
continue;
}
write_error_and_exit(&e);
}
}
}
#[cold]
#[inline(never)]
fn write_error_and_exit(err: &std::io::Error) -> ! {
#[cfg(target_os = "linux")]
{
if let Some(errno) = err.raw_os_error() {
unsafe {
let mut strerr_buf = [0u8; 256];
let rc = libc::strerror_r(
errno,
strerr_buf.as_mut_ptr() as *mut libc::c_char,
strerr_buf.len(),
);
let err_str = if rc == 0 {
std::ffi::CStr::from_ptr(strerr_buf.as_ptr() as *const libc::c_char)
.to_str()
.unwrap_or("Unknown error")
} else {
"Unknown error"
};
let msg = format!("yes: standard output: {}\n", err_str);
libc::write(2, msg.as_ptr() as *const libc::c_void, msg.len() as _);
libc::_exit(1);
}
}
}
let msg = coreutils_rs::common::io_error_msg(err);
let error_line = format!("{}: standard output: {}\n", TOOL_NAME, msg);
let _ = unsafe {
libc::write(
2,
error_line.as_ptr() as *const libc::c_void,
error_line.len() as _,
)
};
#[cfg(unix)]
unsafe {
libc::_exit(1)
};
#[cfg(not(unix))]
process::exit(1);
}
#[cfg(test)]
mod tests {
use std::io::Read;
use std::process::{Command, Stdio};
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fyes");
Command::new(path)
}
fn cmd_path() -> String {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fyes");
path.to_string_lossy().into_owned()
}
#[test]
fn test_yes_default_y() {
let mut child = cmd().stdout(Stdio::piped()).spawn().unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 10 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(
lines.len() >= 5,
"Expected at least 5 lines, got {}",
lines.len()
);
for line in &lines[..5] {
assert_eq!(*line, "y");
}
}
#[test]
fn test_yes_custom_string() {
let mut child = cmd().arg("hello").stdout(Stdio::piped()).spawn().unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 20 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(
lines.len() >= 3,
"Expected at least 3 lines, got {}",
lines.len()
);
for line in &lines[..3] {
assert_eq!(*line, "hello");
}
}
#[test]
fn test_yes_multiple_args() {
let mut child = cmd()
.args(["a", "b"])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 20 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(
lines.len() >= 2,
"Expected at least 2 lines, got {}",
lines.len()
);
for line in &lines[..2] {
assert_eq!(*line, "a b");
}
}
#[test]
fn test_yes_dash_dash_strips_separator() {
let mut child = cmd()
.args(["--", "foo"])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 20 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(lines.len() >= 2);
for line in &lines[..2] {
assert_eq!(*line, "foo");
}
}
#[test]
fn test_yes_dash_dash_alone_gives_y() {
let mut child = cmd().arg("--").stdout(Stdio::piped()).spawn().unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 20 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(lines.len() >= 2);
for line in &lines[..2] {
assert_eq!(*line, "y");
}
}
#[test]
fn test_yes_pipe_closes() {
let mut child = cmd()
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let child_stdout = child.stdout.take().unwrap();
let head = Command::new("head")
.arg("-n")
.arg("1")
.stdin(child_stdout)
.stdout(Stdio::piped())
.output()
.unwrap();
let status = child.wait().unwrap();
assert_eq!(head.status.code(), Some(0));
let text = String::from_utf8_lossy(&head.stdout);
assert_eq!(text.trim(), "y");
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal() == Some(13) || status.code() == Some(1),
"yes should be killed by SIGPIPE or exit 1, got status: {:?}",
status
);
}
}
#[test]
#[cfg(unix)]
fn test_yes_broken_pipe_terminates() {
let mut child = cmd()
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = [0u8; 4];
let _ = std::io::Read::read(&mut stdout, &mut buf);
drop(stdout);
let status = child.wait().unwrap();
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal() == Some(13) || status.code() == Some(1),
"yes should be killed by SIGPIPE or exit 0/1, got status: {:?}",
status
);
}
#[test]
#[cfg(unix)]
fn test_yes_matches_gnu() {
let gnu = Command::new("sh")
.args(["-c", "yes | head -n 1000"])
.output();
if let Ok(gnu) = gnu {
let ours = Command::new("sh")
.args([
"-c",
&format!("{} | head -n 1000", cmd().get_program().to_str().unwrap()),
])
.output()
.unwrap();
assert_eq!(
String::from_utf8_lossy(&ours.stdout),
String::from_utf8_lossy(&gnu.stdout),
"Output mismatch with GNU yes"
);
}
}
#[cfg(unix)]
fn assert_padded_string_unique(pad_len: usize) {
let padded: String = " ".repeat(pad_len);
let mut child = cmd().arg(&padded).stdout(Stdio::piped()).spawn().unwrap();
let child_stdout = child.stdout.take().unwrap();
let head = Command::new("head")
.args(["-n", "2"])
.stdin(child_stdout)
.stdout(Stdio::piped())
.output()
.unwrap();
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&head.stdout);
let lines: Vec<&str> = text.lines().collect();
assert_eq!(
lines.len(),
2,
"pad_len={}: expected 2 lines from head, got {}",
pad_len,
lines.len()
);
assert_eq!(
lines[0],
lines[1],
"pad_len={}: the two lines differ (buffer split mid-line)\n line0 len={}\n line1 len={}",
pad_len,
lines[0].len(),
lines[1].len()
);
assert_eq!(
lines[0].len(),
pad_len,
"pad_len={}: line length mismatch",
pad_len
);
}
#[test]
#[cfg(unix)]
fn test_yes_1999_char_padded_string() {
assert_padded_string_unique(1999);
}
#[test]
#[cfg(unix)]
fn test_yes_4095_char_padded_string() {
assert_padded_string_unique(4095);
}
#[test]
#[cfg(unix)]
fn test_yes_4096_char_padded_string() {
assert_padded_string_unique(4096);
}
#[test]
#[cfg(unix)]
fn test_yes_8191_char_padded_string() {
assert_padded_string_unique(8191);
}
#[test]
#[cfg(unix)]
fn test_yes_8192_char_padded_string() {
assert_padded_string_unique(8192);
}
#[test]
#[cfg(unix)]
fn test_yes_pipeline_terminates() {
let mut child = cmd()
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let child_stdout = child.stdout.take().unwrap();
let head = Command::new("head")
.args(["-n", "5"])
.stdin(child_stdout)
.stdout(Stdio::piped())
.output()
.unwrap();
let status = child.wait().unwrap();
assert_eq!(head.status.code(), Some(0));
use std::os::unix::process::ExitStatusExt;
assert!(
status.signal() == Some(13) || status.code() == Some(1),
"yes should be killed by SIGPIPE or exit 0/1, got status: {:?}",
status
);
}
#[test]
fn test_yes_unknown_long_option() {
let output = cmd()
.arg("--badopt")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("yes: unrecognized option '--badopt'"),
"stderr should contain unrecognized option message, got: {}",
stderr
);
assert!(
stderr.contains("Try 'yes --help' for more information."),
"stderr should contain help hint, got: {}",
stderr
);
}
#[test]
fn test_yes_unknown_short_option() {
let output = cmd()
.arg("-z")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("yes: invalid option -- 'z'"),
"stderr should contain invalid option message, got: {}",
stderr
);
assert!(
stderr.contains("Try 'yes --help' for more information."),
"stderr should contain help hint, got: {}",
stderr
);
}
#[test]
fn test_yes_bare_dash_is_literal() {
let mut child = cmd().arg("-").stdout(Stdio::piped()).spawn().unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 10 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(lines.len() >= 2);
for line in &lines[..2] {
assert_eq!(*line, "-");
}
}
#[test]
fn test_yes_option_after_dashdash_is_literal() {
let mut child = cmd()
.args(["--", "--badopt"])
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
while buf.len() < 20 {
let n = stdout.read(&mut tmp).unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf);
let lines: Vec<&str> = text.lines().collect();
assert!(lines.len() >= 2);
for line in &lines[..2] {
assert_eq!(*line, "--badopt");
}
}
#[cfg(unix)]
#[test]
fn test_yes_pipe_head() {
let output = std::process::Command::new("sh")
.args(["-c"])
.arg(format!("{} | head -1", cmd_path()))
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "y");
}
#[test]
fn test_yes_epipe_clean_exit() {
let mut child = cmd()
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = [0u8; 64];
let _ = stdout.read(&mut buf);
drop(stdout);
let result = child.wait_with_output().unwrap();
let stderr = String::from_utf8_lossy(&result.stderr);
assert!(
!stderr.contains("panicked"),
"fyes should not panic on EPIPE, stderr: {}",
stderr
);
}
#[test]
fn test_yes_consistent_output() {
let mut child = cmd().stdout(Stdio::piped()).spawn().unwrap();
let mut stdout = child.stdout.take().unwrap();
let mut buf = vec![0u8; 8192];
let mut total = 0;
while total < buf.len() {
let n = stdout.read(&mut buf[total..]).unwrap();
if n == 0 {
break;
}
total += n;
}
drop(stdout);
let _ = child.kill();
let _ = child.wait();
let text = String::from_utf8_lossy(&buf[..total]);
for line in text.lines() {
assert_eq!(
line.trim_end_matches('\r'),
"y",
"Expected 'y' but got '{}'",
line
);
}
}
}