1use std::io::{self, Write};
2use std::path::{Path, PathBuf};
3use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
4
5use grep_matcher::Matcher;
6use grep_regex::RegexMatcher;
7use grep_searcher::{Searcher, Sink, SinkMatch};
8use rayon::prelude::*;
9
10use crate::Candidate;
11use crate::search::emit::format::{ANSI_PATH, ANSI_RESET};
12use crate::search::emit::result::{ChunkOutput, FileResult};
13use crate::search::emit::stats::TextStatsCounters;
14use crate::search::output::SearchOutput;
15use crate::search::output::mode::{OutputEmission, SearchMode, ZeroCountMode};
16use crate::search::output::style::FilenameMode;
17use crate::search::query::SearchQuery;
18use crate::search::request::SearchCollection;
19
20#[derive(Clone, Copy)]
21pub struct FileSummary {
22 pub matched: bool,
23 pub count: usize,
24}
25
26impl FileSummary {
27 #[must_use]
28 pub fn tally(&self, mode: SearchMode) -> usize {
29 match mode {
30 SearchMode::Count | SearchMode::CountMatches => self.count,
31 SearchMode::FilesWithMatches => usize::from(self.matched),
32 SearchMode::FilesWithoutMatch => usize::from(!self.matched),
33 SearchMode::Standard | SearchMode::OnlyMatching => 0,
34 }
35 }
36
37 #[must_use]
38 pub const fn is_success(&self, mode: SearchMode) -> bool {
39 match mode {
40 SearchMode::Count | SearchMode::CountMatches => self.count > 0,
41 SearchMode::FilesWithMatches | SearchMode::Standard | SearchMode::OnlyMatching => {
42 self.matched
43 }
44 SearchMode::FilesWithoutMatch => !self.matched,
45 }
46 }
47
48 #[must_use]
49 pub const fn had_positive_hit(&self, mode: SearchMode) -> bool {
50 match mode {
51 SearchMode::Count | SearchMode::CountMatches => self.count > 0,
52 SearchMode::FilesWithMatches => self.matched,
53 SearchMode::FilesWithoutMatch | SearchMode::Standard | SearchMode::OnlyMatching => {
54 false
55 }
56 }
57 }
58}
59
60pub struct SummarySink {
61 mode: SearchMode,
62 matcher: Option<RegexMatcher>,
63 matched: bool,
64 count: usize,
65}
66
67impl SummarySink {
68 #[must_use]
69 pub const fn new(mode: SearchMode, matcher: Option<RegexMatcher>) -> Self {
70 Self {
71 mode,
72 matcher,
73 matched: false,
74 count: 0,
75 }
76 }
77
78 #[must_use]
79 pub fn finish(self) -> FileSummary {
80 FileSummary {
81 matched: self.matched,
82 count: self.count,
83 }
84 }
85}
86
87impl Sink for SummarySink {
88 type Error = io::Error;
89
90 fn matched(&mut self, searcher: &Searcher, mat: &SinkMatch<'_>) -> Result<bool, Self::Error> {
91 std::hint::black_box(searcher);
92 self.matched = true;
93 if self.mode == SearchMode::CountMatches {
94 if let Some(ref matcher) = self.matcher {
95 let line = mat.bytes();
96 let mut n = 0;
97 let _ = matcher.find_iter(line, |_| {
98 n += 1;
99 true
100 });
101 self.count += n;
102 }
103 } else {
104 self.count += 1;
105 }
106 Ok(matches!(
107 self.mode,
108 SearchMode::Count | SearchMode::CountMatches
109 ))
110 }
111}
112
113fn summary_search_file(
114 searcher: &mut Searcher,
115 matcher: &RegexMatcher,
116 mode: SearchMode,
117 path: &Path,
118) -> FileSummary {
119 let sink_matcher = if mode == SearchMode::CountMatches {
120 Some(matcher.clone())
121 } else {
122 None
123 };
124 let mut sink = SummarySink::new(mode, sink_matcher);
125 let _ = searcher.search_path(matcher, path, &mut sink);
126 sink.finish()
127}
128
129fn write_summary_record(
130 out: &mut Vec<u8>,
131 output: SearchOutput,
132 display_path: &str,
133 result: FileSummary,
134) -> io::Result<()> {
135 if output.emission == OutputEmission::Quiet {
136 return Ok(());
137 }
138 let records = output.records;
139 match output.mode {
140 SearchMode::Count | SearchMode::CountMatches => {
141 if result.count == 0 && matches!(output.include_zero, ZeroCountMode::Omit) {
142 return Ok(());
143 }
144 let print_filename = output.lines.filename_mode != FilenameMode::Never;
145 if print_filename {
146 if records.should_color() {
147 out.extend_from_slice(ANSI_PATH);
148 }
149 write!(out, "{display_path}")?;
150 if records.should_color() {
151 out.extend_from_slice(ANSI_RESET);
152 }
153 writeln!(out, ":{}", result.count)?;
154 } else {
155 writeln!(out, "{}", result.count)?;
156 }
157 Ok(())
158 }
159 SearchMode::FilesWithMatches => {
160 if result.matched {
161 if records.should_color() {
162 out.extend_from_slice(ANSI_PATH);
163 }
164 write!(out, "{display_path}")?;
165 if records.should_color() {
166 out.extend_from_slice(ANSI_RESET);
167 }
168 records.terminator.write_to(out);
169 }
170 Ok(())
171 }
172 SearchMode::FilesWithoutMatch => {
173 if result.matched {
174 return Ok(());
175 }
176 if records.should_color() {
177 out.extend_from_slice(ANSI_PATH);
178 }
179 write!(out, "{display_path}")?;
180 if records.should_color() {
181 out.extend_from_slice(ANSI_RESET);
182 }
183 records.terminator.write_to(out);
184 Ok(())
185 }
186 SearchMode::Standard | SearchMode::OnlyMatching => unreachable!(),
187 }
188}
189
190struct SummaryWorker<'a> {
191 matcher: &'a RegexMatcher,
192 searcher: Searcher,
193 output: SearchOutput,
194 summary_counter: Option<&'a AtomicUsize>,
195 files_with_matches: Option<&'a AtomicUsize>,
196 collect_hits: bool,
197}
198
199impl<'a> SummaryWorker<'a> {
200 fn new(scan: &'a SummaryScan<'a>, collect: SearchCollection) -> Self {
201 Self {
202 searcher: scan
203 .search
204 .build_searcher(false, scan.search.opts().max_results, false),
205 matcher: scan.matcher,
206 output: scan.output,
207 summary_counter: scan.counters.primary(),
208 files_with_matches: scan.counters.files_with_matches(),
209 collect_hits: collect.hits,
210 }
211 }
212
213 fn search_candidate(&mut self, candidate: &Candidate, stop: &AtomicBool) -> FileResult {
214 if stop.load(Ordering::SeqCst) {
215 return FileResult {
216 output: ChunkOutput::empty(),
217 json_stats: None,
218 hit: None,
219 };
220 }
221
222 let result = summary_search_file(
223 &mut self.searcher,
224 self.matcher,
225 self.output.mode,
226 candidate.abs_path(),
227 );
228 if let Some(c) = self.summary_counter {
229 c.fetch_add(result.tally(self.output.mode), Ordering::Relaxed);
230 }
231 if let Some(c) = self.files_with_matches
232 && result.had_positive_hit(self.output.mode)
233 {
234 c.fetch_add(1, Ordering::Relaxed);
235 }
236 let matched = result.is_success(self.output.mode);
237 let mut bytes = Vec::new();
238 let display = candidate.display_path(
239 self.output.lines.path_display,
240 self.output.records.path_separator,
241 );
242 let _ = write_summary_record(&mut bytes, self.output, &display, result);
243 if self.output.emission == OutputEmission::Quiet && result.is_success(self.output.mode) {
244 stop.store(true, Ordering::SeqCst);
245 }
246
247 FileResult {
248 output: ChunkOutput {
249 bytes,
250 matched,
251 heading: false,
252 },
253 json_stats: None,
254 hit: (self.collect_hits && result.matched).then(|| candidate.rel_path().to_path_buf()),
255 }
256 }
257}
258
259pub struct SummaryScan<'a> {
260 search: &'a SearchQuery,
261 matcher: &'a RegexMatcher,
262 output: SearchOutput,
263 counters: &'a TextStatsCounters,
264}
265
266impl<'a> SummaryScan<'a> {
267 pub const fn new(
268 search: &'a SearchQuery,
269 matcher: &'a RegexMatcher,
270 output: SearchOutput,
271 counters: &'a TextStatsCounters,
272 ) -> Self {
273 Self {
274 search,
275 matcher,
276 output,
277 counters,
278 }
279 }
280
281 pub fn run(
285 &self,
286 candidates: &[Candidate],
287 collect: SearchCollection,
288 ) -> crate::Result<(bool, Vec<PathBuf>)> {
289 let stop = AtomicBool::new(false);
290 let n = candidates.len();
291 let mut files = Vec::with_capacity(n);
292 candidates
293 .par_iter()
294 .map_init(
295 || SummaryWorker::new(self, collect),
296 |worker: &mut SummaryWorker<'_>, candidate: &Candidate| {
297 worker.search_candidate(candidate, &stop)
298 },
299 )
300 .collect_into_vec(&mut files);
301 let mut hits = Vec::new();
302 let mut outputs = Vec::with_capacity(files.len());
303 let mut any_match = false;
304 for file in files {
305 if collect.hits
306 && let Some(hit) = file.hit
307 {
308 hits.push(hit);
309 }
310 any_match |= file.output.matched;
311 outputs.push(file.output);
312 }
313 ChunkOutput::flush_all(outputs, self.counters.bytes_printed())?;
314 Ok((any_match, hits))
315 }
316}