use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write};
use std::path::Path;
use crate::config::cli::FilterArgs;
use crate::engine::{self, CompiledRegex, EngineFlags, EngineKind};
pub mod app;
pub mod json_path;
pub mod run;
pub mod ui;
pub use app::{FilterApp, Outcome};
#[derive(Debug, Clone, Copy, Default)]
pub struct FilterOptions {
pub invert: bool,
pub case_insensitive: bool,
}
impl FilterOptions {
fn flags(&self) -> EngineFlags {
EngineFlags {
case_insensitive: self.case_insensitive,
..EngineFlags::default()
}
}
}
pub fn match_haystack(
compiled: &dyn CompiledRegex,
haystack: &str,
invert: bool,
) -> Option<Vec<std::ops::Range<usize>>> {
let found = compiled.find_matches(haystack).unwrap_or_default();
let hit = !found.is_empty();
if hit == invert {
return None;
}
Some(if invert {
Vec::new()
} else {
found.into_iter().map(|m| m.start..m.end).collect()
})
}
pub fn filter_lines(
lines: &[String],
pattern: &str,
options: FilterOptions,
) -> Result<Vec<usize>, String> {
if pattern.is_empty() {
return Ok(if options.invert {
Vec::new()
} else {
(0..lines.len()).collect()
});
}
let engine = engine::create_engine(EngineKind::RustRegex);
let compiled = engine
.compile(pattern, &options.flags())
.map_err(|e| e.to_string())?;
let mut indices = Vec::with_capacity(lines.len());
for (idx, line) in lines.iter().enumerate() {
if match_haystack(&*compiled, line, options.invert).is_some() {
indices.push(idx);
}
}
Ok(indices)
}
pub fn filter_lines_with_extracted(
extracted: &[Option<String>],
pattern: &str,
options: FilterOptions,
) -> Result<Vec<usize>, String> {
if pattern.is_empty() {
if options.invert {
return Ok(Vec::new());
}
return Ok(extracted
.iter()
.enumerate()
.filter_map(|(idx, v)| v.as_ref().map(|_| idx))
.collect());
}
let engine = engine::create_engine(EngineKind::RustRegex);
let compiled = engine
.compile(pattern, &options.flags())
.map_err(|e| e.to_string())?;
let mut indices = Vec::with_capacity(extracted.len());
for (idx, slot) in extracted.iter().enumerate() {
let Some(s) = slot else {
continue;
};
if match_haystack(&*compiled, s, options.invert).is_some() {
indices.push(idx);
}
}
Ok(indices)
}
pub fn extract_strings(lines: &[String], path_expr: &str) -> Result<Vec<Option<String>>, String> {
let path = json_path::parse_path(path_expr)?;
let mut out = Vec::with_capacity(lines.len());
for line in lines {
let extracted = serde_json::from_str::<serde_json::Value>(line)
.ok()
.and_then(|v| {
json_path::extract(&v, &path).and_then(|v| v.as_str().map(str::to_string))
});
out.push(extracted);
}
Ok(out)
}
pub const EXIT_MATCH: i32 = 0;
pub const EXIT_NO_MATCH: i32 = 1;
pub const EXIT_ERROR: i32 = 2;
pub const MAX_LINE_BYTES: usize = 10 * 1024 * 1024;
pub fn emit_matches(
writer: &mut dyn Write,
lines: &[String],
matched: &[usize],
line_number: bool,
) -> io::Result<()> {
for &idx in matched {
if line_number {
writeln!(writer, "{}:{}", idx + 1, lines[idx])?;
} else {
writeln!(writer, "{}", lines[idx])?;
}
}
Ok(())
}
pub fn emit_count(writer: &mut dyn Write, matched_count: usize) -> io::Result<()> {
writeln!(writer, "{matched_count}")
}
pub fn read_input(
file: Option<&Path>,
fallback: impl Read,
max_lines: usize,
) -> io::Result<(Vec<String>, bool)> {
let mut reader: Box<dyn BufRead> = match file {
Some(path) => Box::new(BufReader::new(std::fs::File::open(path)?)),
None => Box::new(BufReader::new(fallback)),
};
let mut out = Vec::new();
let mut buf = Vec::new();
let mut truncated = false;
let line_limit = MAX_LINE_BYTES as u64 + 1;
loop {
if max_lines != 0 && out.len() >= max_lines {
let mut one = [0u8; 1];
if reader.read(&mut one)? > 0 {
truncated = true;
}
break;
}
buf.clear();
let n = (&mut reader).take(line_limit).read_until(b'\n', &mut buf)?;
if n == 0 {
break;
}
let line_overflowed = buf.last() != Some(&b'\n') && n as u64 == line_limit;
if line_overflowed {
truncated = true;
buf.truncate(MAX_LINE_BYTES);
let mut discard = Vec::new();
reader.read_until(b'\n', &mut discard)?;
}
let end = buf
.iter()
.rposition(|b| *b != b'\n' && *b != b'\r')
.map(|i| i + 1)
.unwrap_or(0);
out.push(String::from_utf8_lossy(&buf[..end]).into_owned());
}
Ok((out, truncated))
}
pub fn entry(args: FilterArgs) -> i32 {
match run_entry(args) {
Ok(code) => code,
Err(msg) => {
eprintln!("rgx filter: {msg}");
EXIT_ERROR
}
}
}
fn run_entry(args: FilterArgs) -> Result<i32, String> {
let (lines, truncated) = read_input(args.file.as_deref(), io::stdin(), args.max_lines)
.map_err(|e| format!("reading input: {e}"))?;
if truncated {
eprintln!(
"rgx filter: input truncated at {} lines (use --max-lines to override)",
args.max_lines
);
}
let options = FilterOptions {
invert: args.invert,
case_insensitive: args.case_insensitive,
};
let has_pattern = args.pattern.as_deref().is_some_and(|p| !p.is_empty());
let stdout_is_tty = io::stdout().is_terminal();
let non_interactive = args.count || args.line_number || (has_pattern && !stdout_is_tty);
let json_extracted = if let Some(path_expr) = args.json.as_deref() {
Some(extract_strings(&lines, path_expr).map_err(|e| format!("--json: {e}"))?)
} else {
None
};
if non_interactive {
let pattern = args.pattern.unwrap_or_default();
let matched = match &json_extracted {
Some(extracted) => filter_lines_with_extracted(extracted, &pattern, options)
.map_err(|e| format!("pattern: {e}"))?,
None => filter_lines(&lines, &pattern, options).map_err(|e| format!("pattern: {e}"))?,
};
let mut stdout = io::stdout().lock();
if args.count {
emit_count(&mut stdout, matched.len()).map_err(|e| format!("writing output: {e}"))?;
} else {
emit_matches(&mut stdout, &lines, &matched, args.line_number)
.map_err(|e| format!("writing output: {e}"))?;
}
return Ok(if matched.is_empty() {
EXIT_NO_MATCH
} else {
EXIT_MATCH
});
}
let initial_pattern = args.pattern.unwrap_or_default();
let app = match json_extracted {
Some(extracted) => {
FilterApp::with_json_extracted(lines, extracted, &initial_pattern, options)
.map_err(|e| format!("--json: {e}"))?
}
None => FilterApp::new(lines, &initial_pattern, options),
};
let (final_app, outcome) = run::run_tui(app).map_err(|e| format!("tui: {e}"))?;
match outcome {
Outcome::Emit => {
let mut stdout = io::stdout().lock();
emit_matches(&mut stdout, &final_app.lines, &final_app.matched, false)
.map_err(|e| format!("writing output: {e}"))?;
Ok(if final_app.matched.is_empty() {
EXIT_NO_MATCH
} else {
EXIT_MATCH
})
}
Outcome::Discard => Ok(EXIT_NO_MATCH),
Outcome::Pending => Ok(EXIT_ERROR),
}
}