use std::io::{self, BufWriter, Write};
#[cfg(unix)]
use std::mem::ManuallyDrop;
#[cfg(unix)]
use std::os::unix::io::FromRawFd;
use std::path::Path;
use std::process;
#[cfg(unix)]
use coreutils_rs::common::io::try_mmap_stdin_with_hints;
use coreutils_rs::common::io::{FileData, MmapHints, read_file_with_hints, read_stdin};
use coreutils_rs::common::{enlarge_stdout_pipe, io_error_msg};
use coreutils_rs::tac;
struct Cli {
before: bool,
regex: bool,
separator: Option<String>,
files: Vec<String>,
}
fn parse_args() -> Cli {
let mut cli = Cli {
before: false,
regex: false,
separator: None,
files: Vec::new(),
};
let mut args = std::env::args_os().skip(1);
#[allow(clippy::while_let_on_iterator)]
while let Some(arg) = args.next() {
let bytes = arg.as_encoded_bytes();
if bytes == b"--" {
for a in args {
cli.files.push(a.to_string_lossy().into_owned());
}
break;
}
if bytes.starts_with(b"--") {
if bytes.starts_with(b"--separator=") {
let val = arg.to_string_lossy();
cli.separator = Some(val[12..].to_string());
continue;
}
match bytes {
b"--before" => cli.before = true,
b"--regex" => cli.regex = true,
b"--separator" => {
cli.separator = Some(
args.next()
.unwrap_or_else(|| {
eprintln!("tac: option '--separator' requires an argument");
process::exit(1);
})
.to_string_lossy()
.into_owned(),
);
}
b"--help" => {
print!(
"Usage: tac [OPTION]... [FILE]...\n\
Write each FILE to standard output, last line first.\n\n\
With no FILE, or when FILE is -, read standard input.\n\n\
Mandatory arguments to long options are mandatory for short options too.\n\
\x20 -b, --before attach the separator before instead of after\n\
\x20 -r, --regex interpret the separator as a regular expression\n\
\x20 -s, --separator=STRING use STRING as the separator instead of newline\n\
\x20 --help display this help and exit\n\
\x20 --version output version information and exit\n"
);
process::exit(0);
}
b"--version" => {
println!("tac (fcoreutils) {}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
_ => {
eprintln!("tac: unrecognized option '{}'", arg.to_string_lossy());
eprintln!("Try 'tac --help' for more information.");
process::exit(1);
}
}
} else if bytes.len() > 1 && bytes[0] == b'-' {
let mut i = 1;
while i < bytes.len() {
match bytes[i] {
b'b' => cli.before = true,
b'r' => cli.regex = true,
b's' => {
if i + 1 < bytes.len() {
let val = arg.to_string_lossy();
cli.separator = Some(val[i + 1..].to_string());
} else {
cli.separator = Some(
args.next()
.unwrap_or_else(|| {
eprintln!("tac: option requires an argument -- 's'");
process::exit(1);
})
.to_string_lossy()
.into_owned(),
);
}
break; }
_ => {
eprintln!("tac: invalid option -- '{}'", bytes[i] as char);
eprintln!("Try 'tac --help' for more information.");
process::exit(1);
}
}
i += 1;
}
} else {
cli.files.push(arg.to_string_lossy().into_owned());
}
}
cli
}
fn run(cli: &Cli, files: &[String], out: &mut impl Write) -> bool {
let mut had_error = false;
for filename in files {
let data: FileData = if filename == "-" {
#[cfg(unix)]
{
match try_mmap_stdin_with_hints(2 * 1024 * 1024, false) {
Some(mmap) => FileData::Mmap(mmap),
None => {
#[cfg(target_os = "linux")]
{
match coreutils_rs::common::io::splice_stdin_to_mmap() {
Ok(Some(mmap)) => FileData::Owned(mmap.to_vec()),
_ => match read_stdin() {
Ok(d) => FileData::Owned(d),
Err(e) => {
eprintln!("tac: standard input: {}", io_error_msg(&e));
had_error = true;
continue;
}
},
}
}
#[cfg(not(target_os = "linux"))]
match read_stdin() {
Ok(d) => FileData::Owned(d),
Err(e) => {
eprintln!("tac: standard input: {}", io_error_msg(&e));
had_error = true;
continue;
}
}
}
}
}
#[cfg(not(unix))]
match read_stdin() {
Ok(d) => FileData::Owned(d),
Err(e) => {
eprintln!("tac: standard input: {}", io_error_msg(&e));
had_error = true;
continue;
}
}
} else {
match read_file_with_hints(Path::new(filename), MmapHints::Lazy) {
Ok(d) => d,
Err(e) => {
eprintln!("tac: {}: {}", filename, io_error_msg(&e));
had_error = true;
continue;
}
}
};
let result = if cli.regex {
let bytes: &[u8] = &data;
let sep = cli.separator.as_deref().unwrap_or("\n");
tac::tac_regex_separator(bytes, sep, cli.before, out)
} else if let Some(ref sep) = cli.separator {
let bytes: &[u8] = &data;
if sep.is_empty() {
tac::tac_bytes(bytes, b'\0', cli.before, out)
} else if sep.len() == 1 {
#[cfg(unix)]
{
let _ = out.flush();
tac::tac_bytes_to_fd(bytes, sep.as_bytes()[0], cli.before, 1)
}
#[cfg(not(unix))]
tac::tac_string_separator(bytes, sep.as_bytes(), cli.before, out)
} else {
tac::tac_string_separator(bytes, sep.as_bytes(), cli.before, out)
}
} else {
let bytes: &[u8] = &data;
#[cfg(unix)]
{
let _ = out.flush();
tac::tac_bytes_to_fd(bytes, b'\n', cli.before, 1)
}
#[cfg(not(unix))]
tac::tac_bytes(bytes, b'\n', cli.before, out)
};
if let Err(e) = result {
if e.kind() == io::ErrorKind::BrokenPipe {
process::exit(0);
}
eprintln!("tac: write error: {}", io_error_msg(&e));
had_error = true;
}
}
had_error
}
fn main() {
coreutils_rs::common::reset_sigpipe();
enlarge_stdout_pipe();
let mut cli = parse_args();
let files: Vec<String> = if cli.files.is_empty() {
vec!["-".to_string()]
} else {
std::mem::take(&mut cli.files)
};
#[cfg(unix)]
let had_error = {
let raw = unsafe { ManuallyDrop::new(std::fs::File::from_raw_fd(1)) };
let mut writer = BufWriter::with_capacity(1024 * 1024, &*raw);
let err = run(&cli, &files, &mut writer);
let _ = writer.flush();
err
};
#[cfg(not(unix))]
let had_error = {
let stdout = io::stdout();
let lock = stdout.lock();
let mut writer = BufWriter::with_capacity(1024 * 1024, lock);
let err = run(&cli, &files, &mut writer);
let _ = writer.flush();
err
};
if had_error {
process::exit(1);
}
}
#[cfg(test)]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("ftac");
Command::new(path)
}
#[test]
fn test_tac_basic() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"a\nb\nc\n").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "c\nb\na\n");
}
#[test]
fn test_tac_empty_input() {
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
drop(child.stdin.take().unwrap());
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert_eq!(output.stdout, b"");
}
#[test]
fn test_tac_single_line() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"hello\n").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "hello\n");
}
#[test]
fn test_tac_no_trailing_newline() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"a\nb").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("b") && stdout.contains("a"));
}
#[test]
fn test_tac_file() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("test.txt");
std::fs::write(&file, "1\n2\n3\n").unwrap();
let output = cmd().arg(file.to_str().unwrap()).output().unwrap();
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stdout), "3\n2\n1\n");
}
#[test]
fn test_tac_multiple_files() {
let dir = tempfile::tempdir().unwrap();
let f1 = dir.path().join("a.txt");
let f2 = dir.path().join("b.txt");
std::fs::write(&f1, "1\n2\n").unwrap();
std::fs::write(&f2, "3\n4\n").unwrap();
let output = cmd()
.args([f1.to_str().unwrap(), f2.to_str().unwrap()])
.output()
.unwrap();
assert!(output.status.success());
}
#[test]
fn test_tac_nonexistent_file() {
let output = cmd().arg("/nonexistent_xyz_tac").output().unwrap();
assert!(!output.status.success());
}
#[test]
fn test_tac_custom_separator() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.args(["-s", ":"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"a:b:c:").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("c"));
}
#[test]
fn test_tac_before_flag() {
use std::io::Write;
use std::process::Stdio;
let mut child = cmd()
.args(["-b", "-s", ":"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(b"a:b:c").unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
}
#[test]
fn test_tac_many_lines() {
use std::io::Write;
use std::process::Stdio;
let input: String = (1..=100).map(|i| format!("{}\n", i)).collect();
let mut child = cmd()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(input.as_bytes())
.unwrap();
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines[0], "100");
assert_eq!(lines[99], "1");
}
}