use trex::files::{Edit, LineIndex, Source, is_binary, read_source};
use trex::records::RecordUnit;
use trex::window::{Asked, Select};
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Windowing {
pub(crate) select: Option<Select>,
pub(crate) follow: bool,
}
impl Windowing {
pub(crate) fn take(&mut self, args: &[String], i: &mut usize) -> Option<Result<(), String>> {
let arg = args[*i].as_str();
let (flag, attached) = match arg.split_once('=') {
Some((f, v)) if matches!(f, "--head" | "--tail" | "--lines") => (f, Some(v.to_string())),
_ => (arg, None),
};
if flag == "--follow" {
self.follow = true;
return Some(Ok(()));
}
if !matches!(flag, "--head" | "--tail" | "--lines") {
return None;
}
let value = match attached {
Some(v) => v,
None => {
*i += 1;
match args.get(*i) {
Some(v) => v.clone(),
None => {
let wants = if flag == "--lines" { "a range, as in 100..200, 100.. or ..200" } else { "a number" };
return Some(Err(format!("{flag} needs {wants}")));
}
}
}
};
Some(self.set(flag, &value))
}
fn set(&mut self, flag: &str, value: &str) -> Result<(), String> {
let select = match flag {
"--head" => Select::Head(count(flag, value)?),
"--tail" => Select::Tail(count(flag, value)?),
_ => Select::parse_range(value).map_err(|e| format!("--lines {e}"))?,
};
if self.select.is_some() {
return Err(format!("{flag} names a second window; --head, --tail and --lines name one between them"));
}
self.select = Some(select);
Ok(())
}
pub(crate) fn follow_refusal(&self) -> Option<&'static str> {
match self.select {
Some(s) if self.follow && !s.runs_to_end() => Some(
"--follow reads on as a file grows, past the end of the window, and --head and --lines A..B end at a record they name; follow a --tail N, a --lines A.. or the whole file",
),
_ => None,
}
}
}
fn count(flag: &str, value: &str) -> Result<usize, String> {
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
return Err(format!("{flag} needs a number, not {value:?}"));
}
value.parse::<usize>().map_err(|e| format!("{flag} {value}: {e}"))
}
pub(crate) struct Part {
pub(crate) text: Vec<u8>,
pub(crate) byte_base: Option<usize>,
pub(crate) line_base: Option<usize>,
pub(crate) binary: bool,
pub(crate) input_len: usize,
pub(crate) encoding: trex::encoding::Encoding,
}
impl Part {
pub(crate) fn index(&self) -> LineIndex {
LineIndex::new(&self.text).within(self.byte_base, self.line_base)
}
pub(crate) fn base(&self) -> usize {
self.byte_base.expect("a part is placed wherever its command continues or prints its offsets")
}
pub(crate) fn of_text(text: Vec<u8>, select: Option<Select>, unit: &RecordUnit) -> Part {
let input_len = text.len();
match select {
None => Part {
text,
byte_base: Some(0),
line_base: Some(0),
binary: false,
input_len,
encoding: trex::encoding::Encoding::Utf8,
},
Some(s) => Part::from(trex::window::window_of(&text, s, unit)),
}
}
}
impl From<trex::window::Window> for Part {
fn from(w: trex::window::Window) -> Part {
Part {
text: w.bytes,
byte_base: w.byte_base,
line_base: w.line_base,
binary: w.binary,
input_len: w.input_len,
encoding: w.encoding,
}
}
}
pub(crate) fn read_part(src: &Source, select: Option<Select>, unit: &RecordUnit, asked: Asked) -> std::io::Result<Part> {
let Some(select) = select else {
let raw = read_source(src)?;
let binary = is_binary(&raw);
let input_len = raw.len();
let encoding = trex::encoding::Encoding::declared(&raw).0;
let text = if binary && !asked.binary { Vec::new() } else { trex::encoding::decode(raw) };
return Ok(Part { text, byte_base: Some(0), line_base: Some(0), binary, input_len, encoding });
};
let window = match src {
Source::File(path) => trex::window::read_file(path, select, unit, asked)?,
Source::Stdin => trex::window::read_stream(std::io::stdin().lock(), select, unit, asked)?,
};
Ok(Part::from(window))
}
#[derive(Clone, Copy)]
pub(crate) struct Restrict<'a> {
pub(crate) select: Select,
pub(crate) unit: &'a RecordUnit,
}
impl<'a> Restrict<'a> {
pub(crate) fn of(windowing: &Windowing, unit: &'a RecordUnit) -> Option<Restrict<'a>> {
windowing.select.map(|select| Restrict { select, unit })
}
}
#[derive(Clone, Copy)]
pub(crate) struct Piece<'a> {
pub(crate) text: &'a [u8],
pub(crate) byte_base: usize,
pub(crate) line_base: usize,
}
impl Piece<'_> {
pub(crate) fn index(&self) -> LineIndex {
LineIndex::new(self.text).within(Some(self.byte_base), Some(self.line_base))
}
}
pub(crate) fn edits_within(
input: &[u8],
restrict: Option<Restrict<'_>>,
edits: impl FnOnce(Piece<'_>) -> Vec<Edit>,
) -> Vec<Edit> {
let Some(r) = restrict else {
return edits(Piece { text: input, byte_base: 0, line_base: 0 });
};
let range = trex::window::range_of(input, r.select, r.unit);
let line_base = trex::byte_simd::count_byte(&input[..range.start], b'\n');
let mut made = edits(Piece { text: &input[range.clone()], byte_base: range.start, line_base });
for e in &mut made {
e.start += range.start;
e.end += range.start;
}
made
}
#[cfg(test)]
mod tests {
use super::*;
fn args(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn the_window_flags_take_their_values_either_way_and_name_one_window() {
let a = args(&["--head", "3", "--follow"]);
let mut w = Windowing::default();
let mut i = 0;
assert!(matches!(w.take(&a, &mut i), Some(Ok(()))));
assert_eq!((i, w.select), (1, Some(Select::Head(3))));
i += 1;
assert!(matches!(w.take(&a, &mut i), Some(Ok(()))));
assert!(w.follow);
assert!(w.follow_refusal().is_some(), "a head ends before the file does");
let a = args(&["--lines=5..", "--tail", "2", "--other"]);
let mut w = Windowing::default();
let mut i = 0;
assert!(matches!(w.take(&a, &mut i), Some(Ok(()))));
assert_eq!(w.select, Some(Select::Range { from: Some(5), to: None }));
i += 1;
assert!(matches!(w.take(&a, &mut i), Some(Err(e)) if e.contains("second window")));
let mut i = 3;
assert!(w.take(&a, &mut i).is_none());
let mut i = 0;
assert!(matches!(Windowing::default().take(&args(&["--tail", "x"]), &mut i), Some(Err(e)) if e.contains("number")));
}
#[test]
fn an_edit_confined_to_a_window_changes_only_that_part() {
let input = b"a 1\nb 2\nc 3\nd 4\n";
let unit = RecordUnit::Line;
let restrict = Some(Restrict { select: Select::Tail(2), unit: &unit });
let edits = edits_within(input, restrict, |piece| {
assert_eq!((piece.text, piece.byte_base, piece.line_base), (&b"c 3\nd 4\n"[..], 8, 2));
vec![Edit { start: 2, end: 3, replacement: b"X".to_vec() }]
});
assert_eq!(edits, vec![Edit { start: 10, end: 11, replacement: b"X".to_vec() }]);
}
}