1use std::io::{self, BufRead, BufReader, IsTerminal, Read, Write};
4use std::path::Path;
5
6use crate::config::cli::FilterArgs;
7use crate::engine::{self, CompiledRegex, EngineFlags};
8
9pub mod app;
10pub mod json_path;
11pub mod run;
12pub mod ui;
13pub use app::{FilterApp, Outcome};
14
15#[derive(Debug, Clone, Copy, Default)]
16pub struct FilterOptions {
17 pub invert: bool,
18 pub case_insensitive: bool,
19}
20
21impl FilterOptions {
22 fn flags(&self) -> EngineFlags {
23 EngineFlags {
24 case_insensitive: self.case_insensitive,
25 ..EngineFlags::default()
26 }
27 }
28}
29
30pub fn match_haystack(
37 compiled: &dyn CompiledRegex,
38 haystack: &str,
39 invert: bool,
40) -> Option<Vec<std::ops::Range<usize>>> {
41 let found = compiled.find_matches(haystack).unwrap_or_default();
42 let hit = !found.is_empty();
43 if hit == invert {
44 return None;
45 }
46 Some(if invert {
47 Vec::new()
48 } else {
49 found.into_iter().map(|m| m.start..m.end).collect()
50 })
51}
52
53pub fn filter_lines(
60 lines: &[String],
61 pattern: &str,
62 options: FilterOptions,
63) -> Result<Vec<usize>, String> {
64 if pattern.is_empty() {
65 return Ok(if options.invert {
67 Vec::new()
68 } else {
69 (0..lines.len()).collect()
70 });
71 }
72
73 let engine = engine::create_engine(engine::detect_minimum_engine(pattern));
74 let compiled = engine
75 .compile(pattern, &options.flags())
76 .map_err(|e| e.to_string())?;
77
78 let mut indices = Vec::with_capacity(lines.len());
79 for (idx, line) in lines.iter().enumerate() {
80 if match_haystack(&*compiled, line, options.invert).is_some() {
81 indices.push(idx);
82 }
83 }
84 Ok(indices)
85}
86
87pub fn filter_lines_with_extracted(
95 extracted: &[Option<String>],
96 pattern: &str,
97 options: FilterOptions,
98) -> Result<Vec<usize>, String> {
99 if pattern.is_empty() {
100 if options.invert {
104 return Ok(Vec::new());
105 }
106 return Ok(extracted
107 .iter()
108 .enumerate()
109 .filter_map(|(idx, v)| v.as_ref().map(|_| idx))
110 .collect());
111 }
112
113 let engine = engine::create_engine(engine::detect_minimum_engine(pattern));
114 let compiled = engine
115 .compile(pattern, &options.flags())
116 .map_err(|e| e.to_string())?;
117
118 let mut indices = Vec::with_capacity(extracted.len());
119 for (idx, slot) in extracted.iter().enumerate() {
120 let Some(s) = slot else {
121 continue;
123 };
124 if match_haystack(&*compiled, s, options.invert).is_some() {
125 indices.push(idx);
126 }
127 }
128 Ok(indices)
129}
130
131pub fn extract_strings(lines: &[String], path_expr: &str) -> Result<Vec<Option<String>>, String> {
136 let path = json_path::parse_path(path_expr)?;
137 let mut out = Vec::with_capacity(lines.len());
138 for line in lines {
139 let extracted = serde_json::from_str::<serde_json::Value>(line)
140 .ok()
141 .and_then(|v| {
142 json_path::extract(&v, &path).and_then(|v| v.as_str().map(str::to_string))
143 });
144 out.push(extracted);
145 }
146 Ok(out)
147}
148
149pub const EXIT_MATCH: i32 = 0;
151pub const EXIT_NO_MATCH: i32 = 1;
152pub const EXIT_ERROR: i32 = 2;
153
154pub const MAX_LINE_BYTES: usize = 10 * 1024 * 1024;
159
160pub fn emit_matches(
163 writer: &mut dyn Write,
164 lines: &[String],
165 matched: &[usize],
166 line_number: bool,
167) -> io::Result<()> {
168 for &idx in matched {
169 if line_number {
170 writeln!(writer, "{}:{}", idx + 1, lines[idx])?;
171 } else {
172 writeln!(writer, "{}", lines[idx])?;
173 }
174 }
175 Ok(())
176}
177
178pub fn emit_count(writer: &mut dyn Write, matched_count: usize) -> io::Result<()> {
180 writeln!(writer, "{matched_count}")
181}
182
183pub fn read_input(
201 file: Option<&Path>,
202 fallback: impl Read,
203 max_lines: usize,
204) -> io::Result<(Vec<String>, bool, bool)> {
205 let mut reader: Box<dyn BufRead> = match file {
206 Some(path) => Box::new(BufReader::new(std::fs::File::open(path)?)),
207 None => Box::new(BufReader::new(fallback)),
208 };
209 let mut out = Vec::new();
210 let mut buf = Vec::new();
211 let mut line_truncated = false;
212 let mut byte_truncated = false;
213 let line_limit = MAX_LINE_BYTES as u64 + 1;
216 loop {
217 if max_lines != 0 && out.len() >= max_lines {
218 let mut one = [0u8; 1];
224 if reader.read(&mut one)? > 0 {
225 line_truncated = true;
226 }
227 break;
228 }
229 buf.clear();
230 let n = (&mut reader).take(line_limit).read_until(b'\n', &mut buf)?;
231 if n == 0 {
232 break;
233 }
234 let line_overflowed = buf.last() != Some(&b'\n') && n as u64 == line_limit;
241 if line_overflowed {
242 byte_truncated = true;
243 buf.truncate(MAX_LINE_BYTES);
244 let mut discard = Vec::with_capacity(65_536);
247 loop {
248 discard.clear();
249 (&mut reader).take(65_536).read_until(b'\n', &mut discard)?;
250 if discard.is_empty() || discard.last() == Some(&b'\n') {
251 break;
252 }
253 }
254 }
255 let end = buf
257 .iter()
258 .rposition(|b| *b != b'\n' && *b != b'\r')
259 .map(|i| i + 1)
260 .unwrap_or(0);
261 out.push(String::from_utf8_lossy(&buf[..end]).into_owned());
262 }
263 Ok((out, line_truncated, byte_truncated))
264}
265
266pub fn entry(args: FilterArgs) -> i32 {
269 match run_entry(args) {
270 Ok(code) => code,
271 Err(msg) => {
272 eprintln!("rgx filter: {msg}");
273 EXIT_ERROR
274 }
275 }
276}
277
278fn run_entry(args: FilterArgs) -> Result<i32, String> {
279 let (lines, line_truncated, byte_truncated) =
280 read_input(args.file.as_deref(), io::stdin(), args.max_lines)
281 .map_err(|e| format!("reading input: {e}"))?;
282 if byte_truncated {
283 eprintln!(
284 "rgx filter: one or more lines exceeded {} bytes and were truncated",
285 MAX_LINE_BYTES
286 );
287 }
288 if line_truncated {
289 eprintln!(
290 "rgx filter: input truncated at {} lines (use --max-lines to override)",
291 args.max_lines
292 );
293 }
294
295 let options = FilterOptions {
296 invert: args.invert,
297 case_insensitive: args.case_insensitive,
298 };
299
300 let has_pattern = args.pattern.as_deref().is_some_and(|p| !p.is_empty());
303 let stdout_is_tty = io::stdout().is_terminal();
304 let non_interactive = args.count || args.line_number || (has_pattern && !stdout_is_tty);
305
306 let json_extracted = if let Some(path_expr) = args.json.as_deref() {
310 Some(extract_strings(&lines, path_expr).map_err(|e| format!("--json: {e}"))?)
311 } else {
312 None
313 };
314
315 if non_interactive {
316 let pattern = args.pattern.unwrap_or_default();
317 let matched = match &json_extracted {
318 Some(extracted) => filter_lines_with_extracted(extracted, &pattern, options)
319 .map_err(|e| format!("pattern: {e}"))?,
320 None => filter_lines(&lines, &pattern, options).map_err(|e| format!("pattern: {e}"))?,
321 };
322
323 let mut stdout = io::stdout().lock();
324 if args.count {
325 emit_count(&mut stdout, matched.len()).map_err(|e| format!("writing output: {e}"))?;
326 } else {
327 emit_matches(&mut stdout, &lines, &matched, args.line_number)
330 .map_err(|e| format!("writing output: {e}"))?;
331 }
332 return Ok(if matched.is_empty() {
333 EXIT_NO_MATCH
334 } else {
335 EXIT_MATCH
336 });
337 }
338
339 let initial_pattern = args.pattern.unwrap_or_default();
341 let app = match json_extracted {
342 Some(extracted) => {
343 FilterApp::with_json_extracted(lines, extracted, &initial_pattern, options)
344 .map_err(|e| format!("--json: {e}"))?
345 }
346 None => FilterApp::new(lines, &initial_pattern, options),
347 };
348 let (final_app, outcome) = run::run_tui(app).map_err(|e| format!("tui: {e}"))?;
349
350 match outcome {
351 Outcome::Emit => {
352 let mut stdout = io::stdout().lock();
353 emit_matches(&mut stdout, &final_app.lines, &final_app.matched, false)
354 .map_err(|e| format!("writing output: {e}"))?;
355 Ok(if final_app.matched.is_empty() {
356 EXIT_NO_MATCH
357 } else {
358 EXIT_MATCH
359 })
360 }
361 Outcome::Discard => Ok(EXIT_NO_MATCH),
362 Outcome::Pending => Ok(EXIT_ERROR),
363 }
364}