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_or(0, |i| i + 1);
260 out.push(String::from_utf8_lossy(&buf[..end]).into_owned());
261 }
262 Ok((out, line_truncated, byte_truncated))
263}
264
265pub fn entry(args: FilterArgs) -> i32 {
268 match run_entry(args) {
269 Ok(code) => code,
270 Err(msg) => {
271 eprintln!("rgx filter: {msg}");
272 EXIT_ERROR
273 }
274 }
275}
276
277fn run_entry(args: FilterArgs) -> Result<i32, String> {
278 let (lines, line_truncated, byte_truncated) =
279 read_input(args.file.as_deref(), io::stdin(), args.max_lines)
280 .map_err(|e| format!("reading input: {e}"))?;
281 if byte_truncated {
282 eprintln!(
283 "rgx filter: one or more lines exceeded {MAX_LINE_BYTES} bytes and were truncated"
284 );
285 }
286 if line_truncated {
287 eprintln!(
288 "rgx filter: input truncated at {} lines (use --max-lines to override)",
289 args.max_lines
290 );
291 }
292
293 let options = FilterOptions {
294 invert: args.invert,
295 case_insensitive: args.case_insensitive,
296 };
297
298 let has_pattern = args.pattern.as_deref().is_some_and(|p| !p.is_empty());
301 let stdout_is_tty = io::stdout().is_terminal();
302 let non_interactive = args.count || args.line_number || (has_pattern && !stdout_is_tty);
303
304 let json_extracted = if let Some(path_expr) = args.json.as_deref() {
308 Some(extract_strings(&lines, path_expr).map_err(|e| format!("--json: {e}"))?)
309 } else {
310 None
311 };
312
313 if non_interactive {
314 let pattern = args.pattern.unwrap_or_default();
315 let matched = match &json_extracted {
316 Some(extracted) => filter_lines_with_extracted(extracted, &pattern, options)
317 .map_err(|e| format!("pattern: {e}"))?,
318 None => filter_lines(&lines, &pattern, options).map_err(|e| format!("pattern: {e}"))?,
319 };
320
321 let mut stdout = io::stdout().lock();
322 if args.count {
323 emit_count(&mut stdout, matched.len()).map_err(|e| format!("writing output: {e}"))?;
324 } else {
325 emit_matches(&mut stdout, &lines, &matched, args.line_number)
328 .map_err(|e| format!("writing output: {e}"))?;
329 }
330 return Ok(if matched.is_empty() {
331 EXIT_NO_MATCH
332 } else {
333 EXIT_MATCH
334 });
335 }
336
337 let initial_pattern = args.pattern.unwrap_or_default();
339 let app = match json_extracted {
340 Some(extracted) => {
341 FilterApp::with_json_extracted(lines, extracted, &initial_pattern, options)
342 .map_err(|e| format!("--json: {e}"))?
343 }
344 None => FilterApp::new(lines, &initial_pattern, options),
345 };
346 let (final_app, outcome) = run::run_tui(app).map_err(|e| format!("tui: {e}"))?;
347
348 match outcome {
349 Outcome::Emit => {
350 let mut stdout = io::stdout().lock();
351 emit_matches(&mut stdout, &final_app.lines, &final_app.matched, false)
352 .map_err(|e| format!("writing output: {e}"))?;
353 Ok(if final_app.matched.is_empty() {
354 EXIT_NO_MATCH
355 } else {
356 EXIT_MATCH
357 })
358 }
359 Outcome::Discard => Ok(EXIT_NO_MATCH),
360 Outcome::Pending => Ok(EXIT_ERROR),
361 }
362}