mod error;
use clap::{crate_version, Arg, ArgAction, Command};
use memchr::memmem;
use memmap2::Mmap;
use std::io::{stdin, stdout, BufWriter, Read, Write};
use std::{
fs::{read, File},
path::Path,
};
use uucore::display::Quotable;
use uucore::error::UError;
use uucore::error::UResult;
use uucore::{format_usage, help_about, help_usage, show};
use crate::error::TacError;
static USAGE: &str = help_usage!("tac.md");
static ABOUT: &str = help_about!("tac.md");
mod options {
pub static BEFORE: &str = "before";
pub static REGEX: &str = "regex";
pub static SEPARATOR: &str = "separator";
pub static FILE: &str = "file";
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let args = args.collect_lossy();
let matches = uu_app().try_get_matches_from(args)?;
let before = matches.get_flag(options::BEFORE);
let regex = matches.get_flag(options::REGEX);
let raw_separator = matches
.get_one::<String>(options::SEPARATOR)
.map(|s| s.as_str())
.unwrap_or("\n");
let separator = if raw_separator.is_empty() {
"\0"
} else {
raw_separator
};
let files: Vec<&str> = match matches.get_many::<String>(options::FILE) {
Some(v) => v.map(|s| s.as_str()).collect(),
None => vec!["-"],
};
tac(&files, before, regex, separator)
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.override_usage(format_usage(USAGE))
.about(ABOUT)
.infer_long_args(true)
.arg(
Arg::new(options::BEFORE)
.short('b')
.long(options::BEFORE)
.help("attach the separator before instead of after")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::REGEX)
.short('r')
.long(options::REGEX)
.help("interpret the sequence as a regular expression")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SEPARATOR)
.short('s')
.long(options::SEPARATOR)
.help("use STRING as the separator instead of newline")
.value_name("STRING"),
)
.arg(
Arg::new(options::FILE)
.hide(true)
.action(ArgAction::Append)
.value_hint(clap::ValueHint::FilePath),
)
}
fn buffer_tac_regex(
data: &[u8],
pattern: ®ex::bytes::Regex,
before: bool,
) -> std::io::Result<()> {
let out = stdout();
let mut out = BufWriter::new(out.lock());
let mut this_line_end = data.len();
let mut following_line_start = data.len();
for i in (0..data.len()).rev() {
if let Some(match_) = pattern.find_at(&data[..this_line_end], i) {
this_line_end = i;
let slen = match_.end() - match_.start();
if before {
out.write_all(&data[i..following_line_start])?;
following_line_start = i;
} else {
out.write_all(&data[i + slen..following_line_start])?;
following_line_start = i + slen;
}
}
}
out.write_all(&data[0..following_line_start])?;
Ok(())
}
fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> {
let out = stdout();
let mut out = BufWriter::new(out.lock());
let slen = separator.as_bytes().len();
let mut following_line_start = data.len();
for i in memmem::rfind_iter(data, separator) {
if before {
out.write_all(&data[i..following_line_start])?;
following_line_start = i;
} else {
out.write_all(&data[i + slen..following_line_start])?;
following_line_start = i + slen;
}
}
out.write_all(&data[0..following_line_start])?;
Ok(())
}
#[allow(clippy::cognitive_complexity)]
fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResult<()> {
let maybe_pattern = if regex {
match regex::bytes::Regex::new(separator) {
Ok(p) => Some(p),
Err(e) => return Err(TacError::InvalidRegex(e).into()),
}
} else {
None
};
for &filename in filenames {
let mmap;
let buf;
let data: &[u8] = if filename == "-" {
if let Some(mmap1) = try_mmap_stdin() {
mmap = mmap1;
&mmap
} else {
let mut buf1 = Vec::new();
if let Err(e) = stdin().read_to_end(&mut buf1) {
let e: Box<dyn UError> = TacError::ReadError("stdin".to_string(), e).into();
show!(e);
continue;
}
buf = buf1;
&buf
}
} else {
let path = Path::new(filename);
if path.is_dir() {
let e: Box<dyn UError> = TacError::InvalidArgument(String::from(filename)).into();
show!(e);
continue;
}
if path.metadata().is_err() {
let e: Box<dyn UError> = TacError::FileNotFound(String::from(filename)).into();
show!(e);
continue;
}
if let Some(mmap1) = try_mmap_path(path) {
mmap = mmap1;
&mmap
} else {
match read(path) {
Ok(buf1) => {
buf = buf1;
&buf
}
Err(e) => {
let s = format!("{}", filename.quote());
let e: Box<dyn UError> = TacError::ReadError(s.to_string(), e).into();
show!(e);
continue;
}
}
}
};
let result = match maybe_pattern {
Some(ref pattern) => buffer_tac_regex(data, pattern, before),
None => buffer_tac(data, before, separator),
};
if let Err(e) = result {
return Err(TacError::WriteError(e).into());
}
}
Ok(())
}
fn try_mmap_stdin() -> Option<Mmap> {
unsafe { Mmap::map(&stdin()).ok() }
}
fn try_mmap_path(path: &Path) -> Option<Mmap> {
let file = File::open(path).ok()?;
let mmap = unsafe { Mmap::map(&file).ok()? };
Some(mmap)
}