grep_printer/summary.rs
1use std::{
2 cell::RefCell,
3 io::{self, Write},
4 path::Path,
5 sync::Arc,
6 time::Instant,
7};
8
9use {
10 grep_matcher::Matcher,
11 grep_searcher::{Searcher, Sink, SinkError, SinkFinish, SinkMatch},
12 termcolor::{ColorSpec, NoColor, WriteColor},
13};
14
15use crate::{
16 color::ColorSpecs,
17 counter::CounterWriter,
18 hyperlink::{self, HyperlinkConfig},
19 stats::Stats,
20 util::{PrinterPath, find_iter_at_in_context},
21};
22
23/// The configuration for the summary printer.
24///
25/// This is manipulated by the SummaryBuilder and then referenced by the actual
26/// implementation. Once a printer is build, the configuration is frozen and
27/// cannot changed.
28#[derive(Debug, Clone)]
29struct Config {
30 kind: SummaryKind,
31 colors: ColorSpecs,
32 hyperlink: HyperlinkConfig,
33 stats: bool,
34 path: bool,
35 exclude_zero: bool,
36 separator_field: Arc<Vec<u8>>,
37 separator_path: Option<u8>,
38 path_terminator: Option<u8>,
39}
40
41impl Default for Config {
42 fn default() -> Config {
43 Config {
44 kind: SummaryKind::Count,
45 colors: ColorSpecs::default(),
46 hyperlink: HyperlinkConfig::default(),
47 stats: false,
48 path: true,
49 exclude_zero: true,
50 separator_field: Arc::new(b":".to_vec()),
51 separator_path: None,
52 path_terminator: None,
53 }
54 }
55}
56
57/// The type of summary output (if any) to print.
58#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum SummaryKind {
60 /// Show only a count of the total number of matches (counting each line
61 /// at most once) found.
62 ///
63 /// If the `path` setting is enabled, then the count is prefixed by the
64 /// corresponding file path.
65 Count,
66 /// Show only a count of the total number of matches (counting possibly
67 /// many matches on each line) found.
68 ///
69 /// If the `path` setting is enabled, then the count is prefixed by the
70 /// corresponding file path.
71 CountMatches,
72 /// Show only the file path if and only if a match was found.
73 ///
74 /// This ignores the `path` setting and always shows the file path. If no
75 /// file path is provided, then searching will immediately stop and return
76 /// an error.
77 PathWithMatch,
78 /// Show only the file path if and only if a match was found.
79 ///
80 /// This ignores the `path` setting and always shows the file path. If no
81 /// file path is provided, then searching will immediately stop and return
82 /// an error.
83 PathWithoutMatch,
84 /// Don't show any output and the stop the search once a match is found.
85 ///
86 /// Note that if `stats` is enabled, then searching continues in order to
87 /// compute statistics.
88 QuietWithMatch,
89 /// Don't show any output and the stop the search once a non-matching file
90 /// is found.
91 ///
92 /// Note that if `stats` is enabled, then searching continues in order to
93 /// compute statistics.
94 QuietWithoutMatch,
95}
96
97impl SummaryKind {
98 /// Returns true if and only if this output mode requires a file path.
99 ///
100 /// When an output mode requires a file path, then the summary printer
101 /// will report an error at the start of every search that lacks a file
102 /// path.
103 fn requires_path(&self) -> bool {
104 use self::SummaryKind::*;
105
106 match *self {
107 PathWithMatch | PathWithoutMatch => true,
108 Count | CountMatches | QuietWithMatch | QuietWithoutMatch => false,
109 }
110 }
111
112 /// Returns true if and only if this output mode requires computing
113 /// statistics, regardless of whether they have been enabled or not.
114 fn requires_stats(&self) -> bool {
115 use self::SummaryKind::*;
116
117 match *self {
118 CountMatches => true,
119 Count | PathWithMatch | PathWithoutMatch | QuietWithMatch
120 | QuietWithoutMatch => false,
121 }
122 }
123
124 /// Returns true if and only if a printer using this output mode can
125 /// quit after seeing the first match.
126 fn quit_early(&self) -> bool {
127 use self::SummaryKind::*;
128
129 match *self {
130 PathWithMatch | QuietWithMatch => true,
131 Count | CountMatches | PathWithoutMatch | QuietWithoutMatch => {
132 false
133 }
134 }
135 }
136}
137
138/// A builder for summary printer.
139///
140/// The builder permits configuring how the printer behaves. The summary
141/// printer has fewer configuration options than the standard printer because
142/// it aims to produce aggregate output about a single search (typically just
143/// one line) instead of output for each match.
144///
145/// Once a `Summary` printer is built, its configuration cannot be changed.
146#[derive(Clone, Debug)]
147pub struct SummaryBuilder {
148 config: Config,
149}
150
151impl SummaryBuilder {
152 /// Return a new builder for configuring the summary printer.
153 pub fn new() -> SummaryBuilder {
154 SummaryBuilder { config: Config::default() }
155 }
156
157 /// Build a printer using any implementation of `termcolor::WriteColor`.
158 ///
159 /// The implementation of `WriteColor` used here controls whether colors
160 /// are used or not when colors have been configured using the
161 /// `color_specs` method.
162 ///
163 /// For maximum portability, callers should generally use either
164 /// `termcolor::StandardStream` or `termcolor::BufferedStandardStream`
165 /// where appropriate, which will automatically enable colors on Windows
166 /// when possible.
167 ///
168 /// However, callers may also provide an arbitrary writer using the
169 /// `termcolor::Ansi` or `termcolor::NoColor` wrappers, which always enable
170 /// colors via ANSI escapes or always disable colors, respectively.
171 ///
172 /// As a convenience, callers may use `build_no_color` to automatically
173 /// select the `termcolor::NoColor` wrapper to avoid needing to import
174 /// from `termcolor` explicitly.
175 pub fn build<W: WriteColor>(&self, wtr: W) -> Summary<W> {
176 Summary {
177 config: self.config.clone(),
178 wtr: RefCell::new(CounterWriter::new(wtr)),
179 }
180 }
181
182 /// Build a printer from any implementation of `io::Write` and never emit
183 /// any colors, regardless of the user color specification settings.
184 ///
185 /// This is a convenience routine for
186 /// `SummaryBuilder::build(termcolor::NoColor::new(wtr))`.
187 pub fn build_no_color<W: io::Write>(&self, wtr: W) -> Summary<NoColor<W>> {
188 self.build(NoColor::new(wtr))
189 }
190
191 /// Set the output mode for this printer.
192 ///
193 /// The output mode controls how aggregate results of a search are printed.
194 ///
195 /// By default, this printer uses the `Count` mode.
196 pub fn kind(&mut self, kind: SummaryKind) -> &mut SummaryBuilder {
197 self.config.kind = kind;
198 self
199 }
200
201 /// Set the user color specifications to use for coloring in this printer.
202 ///
203 /// A [`UserColorSpec`](crate::UserColorSpec) can be constructed from
204 /// a string in accordance with the color specification format. See
205 /// the `UserColorSpec` type documentation for more details on the
206 /// format. A [`ColorSpecs`] can then be generated from zero or more
207 /// `UserColorSpec`s.
208 ///
209 /// Regardless of the color specifications provided here, whether color
210 /// is actually used or not is determined by the implementation of
211 /// `WriteColor` provided to `build`. For example, if `termcolor::NoColor`
212 /// is provided to `build`, then no color will ever be printed regardless
213 /// of the color specifications provided here.
214 ///
215 /// This completely overrides any previous color specifications. This does
216 /// not add to any previously provided color specifications on this
217 /// builder.
218 ///
219 /// The default color specifications provide no styling.
220 pub fn color_specs(&mut self, specs: ColorSpecs) -> &mut SummaryBuilder {
221 self.config.colors = specs;
222 self
223 }
224
225 /// Set the configuration to use for hyperlinks output by this printer.
226 ///
227 /// Regardless of the hyperlink format provided here, whether hyperlinks
228 /// are actually used or not is determined by the implementation of
229 /// `WriteColor` provided to `build`. For example, if `termcolor::NoColor`
230 /// is provided to `build`, then no hyperlinks will ever be printed
231 /// regardless of the format provided here.
232 ///
233 /// This completely overrides any previous hyperlink format.
234 ///
235 /// The default configuration results in not emitting any hyperlinks.
236 pub fn hyperlink(
237 &mut self,
238 config: HyperlinkConfig,
239 ) -> &mut SummaryBuilder {
240 self.config.hyperlink = config;
241 self
242 }
243
244 /// Enable the gathering of various aggregate statistics.
245 ///
246 /// When this is enabled (it's disabled by default), statistics will be
247 /// gathered for all uses of `Summary` printer returned by `build`,
248 /// including but not limited to, the total number of matches, the total
249 /// number of bytes searched and the total number of bytes printed.
250 ///
251 /// Aggregate statistics can be accessed via the sink's
252 /// [`SummarySink::stats`] method.
253 ///
254 /// When this is enabled, this printer may need to do extra work in order
255 /// to compute certain statistics, which could cause the search to take
256 /// longer. For example, in `QuietWithMatch` mode, a search can quit after
257 /// finding the first match, but if `stats` is enabled, then the search
258 /// will continue after the first match in order to compute statistics.
259 ///
260 /// For a complete description of available statistics, see [`Stats`].
261 ///
262 /// Note that some output modes, such as `CountMatches`, automatically
263 /// enable this option even if it has been explicitly disabled.
264 pub fn stats(&mut self, yes: bool) -> &mut SummaryBuilder {
265 self.config.stats = yes;
266 self
267 }
268
269 /// When enabled, if a path was given to the printer, then it is shown in
270 /// the output (either as a heading or as a prefix to each matching line).
271 /// When disabled, then no paths are ever included in the output even when
272 /// a path is provided to the printer.
273 ///
274 /// This setting has no effect in `PathWithMatch` and `PathWithoutMatch`
275 /// modes.
276 ///
277 /// This is enabled by default.
278 pub fn path(&mut self, yes: bool) -> &mut SummaryBuilder {
279 self.config.path = yes;
280 self
281 }
282
283 /// Exclude count-related summary results with no matches.
284 ///
285 /// When enabled and the mode is either `Count` or `CountMatches`, then
286 /// results are not printed if no matches were found. Otherwise, every
287 /// search prints a result with a possibly `0` number of matches.
288 ///
289 /// This is enabled by default.
290 pub fn exclude_zero(&mut self, yes: bool) -> &mut SummaryBuilder {
291 self.config.exclude_zero = yes;
292 self
293 }
294
295 /// Set the separator used between fields for the `Count` and
296 /// `CountMatches` modes.
297 ///
298 /// By default, this is set to `:`.
299 pub fn separator_field(&mut self, sep: Vec<u8>) -> &mut SummaryBuilder {
300 self.config.separator_field = Arc::new(sep);
301 self
302 }
303
304 /// Set the path separator used when printing file paths.
305 ///
306 /// Typically, printing is done by emitting the file path as is. However,
307 /// this setting provides the ability to use a different path separator
308 /// from what the current environment has configured.
309 ///
310 /// A typical use for this option is to permit cygwin users on Windows to
311 /// set the path separator to `/` instead of using the system default of
312 /// `\`.
313 ///
314 /// This is disabled by default.
315 pub fn separator_path(&mut self, sep: Option<u8>) -> &mut SummaryBuilder {
316 self.config.separator_path = sep;
317 self
318 }
319
320 /// Set the path terminator used.
321 ///
322 /// The path terminator is a byte that is printed after every file path
323 /// emitted by this printer.
324 ///
325 /// If no path terminator is set (the default), then paths are terminated
326 /// by either new lines or the configured field separator.
327 pub fn path_terminator(
328 &mut self,
329 terminator: Option<u8>,
330 ) -> &mut SummaryBuilder {
331 self.config.path_terminator = terminator;
332 self
333 }
334}
335
336/// The summary printer, which emits aggregate results from a search.
337///
338/// Aggregate results generally correspond to file paths and/or the number of
339/// matches found.
340///
341/// A default printer can be created with either of the `Summary::new` or
342/// `Summary::new_no_color` constructors. However, there are a number of
343/// options that configure this printer's output. Those options can be
344/// configured using [`SummaryBuilder`].
345///
346/// This type is generic over `W`, which represents any implementation of
347/// the `termcolor::WriteColor` trait.
348#[derive(Clone, Debug)]
349pub struct Summary<W> {
350 config: Config,
351 wtr: RefCell<CounterWriter<W>>,
352}
353
354impl<W: WriteColor> Summary<W> {
355 /// Return a summary printer with a default configuration that writes
356 /// matches to the given writer.
357 ///
358 /// The writer should be an implementation of `termcolor::WriteColor`
359 /// and not just a bare implementation of `io::Write`. To use a normal
360 /// `io::Write` implementation (simultaneously sacrificing colors), use
361 /// the `new_no_color` constructor.
362 ///
363 /// The default configuration uses the `Count` summary mode.
364 pub fn new(wtr: W) -> Summary<W> {
365 SummaryBuilder::new().build(wtr)
366 }
367}
368
369impl<W: io::Write> Summary<NoColor<W>> {
370 /// Return a summary printer with a default configuration that writes
371 /// matches to the given writer.
372 ///
373 /// The writer can be any implementation of `io::Write`. With this
374 /// constructor, the printer will never emit colors.
375 ///
376 /// The default configuration uses the `Count` summary mode.
377 pub fn new_no_color(wtr: W) -> Summary<NoColor<W>> {
378 SummaryBuilder::new().build_no_color(wtr)
379 }
380}
381
382impl<W: WriteColor> Summary<W> {
383 /// Return an implementation of `Sink` for the summary printer.
384 ///
385 /// This does not associate the printer with a file path, which means this
386 /// implementation will never print a file path. If the output mode of
387 /// this summary printer does not make sense without a file path (such as
388 /// `PathWithMatch` or `PathWithoutMatch`), then any searches executed
389 /// using this sink will immediately quit with an error.
390 pub fn sink<'s, M: Matcher>(
391 &'s mut self,
392 matcher: M,
393 ) -> SummarySink<'static, 's, M, W> {
394 let interpolator =
395 hyperlink::Interpolator::new(&self.config.hyperlink);
396 let stats = if self.config.stats || self.config.kind.requires_stats() {
397 Some(Stats::new())
398 } else {
399 None
400 };
401 SummarySink {
402 matcher,
403 summary: self,
404 interpolator,
405 path: None,
406 start_time: Instant::now(),
407 match_count: 0,
408 binary_byte_offset: None,
409 stats,
410 }
411 }
412
413 /// Return an implementation of `Sink` associated with a file path.
414 ///
415 /// When the printer is associated with a path, then it may, depending on
416 /// its configuration, print the path.
417 pub fn sink_with_path<'p, 's, M, P>(
418 &'s mut self,
419 matcher: M,
420 path: &'p P,
421 ) -> SummarySink<'p, 's, M, W>
422 where
423 M: Matcher,
424 P: ?Sized + AsRef<Path>,
425 {
426 if !self.config.path && !self.config.kind.requires_path() {
427 return self.sink(matcher);
428 }
429 let interpolator =
430 hyperlink::Interpolator::new(&self.config.hyperlink);
431 let stats = if self.config.stats || self.config.kind.requires_stats() {
432 Some(Stats::new())
433 } else {
434 None
435 };
436 let ppath = PrinterPath::new(path.as_ref())
437 .with_separator(self.config.separator_path);
438 SummarySink {
439 matcher,
440 summary: self,
441 interpolator,
442 path: Some(ppath),
443 start_time: Instant::now(),
444 match_count: 0,
445 binary_byte_offset: None,
446 stats,
447 }
448 }
449}
450
451impl<W> Summary<W> {
452 /// Returns true if and only if this printer has written at least one byte
453 /// to the underlying writer during any of the previous searches.
454 pub fn has_written(&self) -> bool {
455 self.wtr.borrow().total_count() > 0
456 }
457
458 /// Return a mutable reference to the underlying writer.
459 pub fn get_mut(&mut self) -> &mut W {
460 self.wtr.get_mut().get_mut()
461 }
462
463 /// Consume this printer and return back ownership of the underlying
464 /// writer.
465 pub fn into_inner(self) -> W {
466 self.wtr.into_inner().into_inner()
467 }
468}
469
470/// An implementation of `Sink` associated with a matcher and an optional file
471/// path for the summary printer.
472///
473/// This type is generic over a few type parameters:
474///
475/// * `'p` refers to the lifetime of the file path, if one is provided. When
476/// no file path is given, then this is `'static`.
477/// * `'s` refers to the lifetime of the [`Summary`] printer that this type
478/// borrows.
479/// * `M` refers to the type of matcher used by
480/// `grep_searcher::Searcher` that is reporting results to this sink.
481/// * `W` refers to the underlying writer that this printer is writing its
482/// output to.
483#[derive(Debug)]
484pub struct SummarySink<'p, 's, M: Matcher, W> {
485 matcher: M,
486 summary: &'s mut Summary<W>,
487 interpolator: hyperlink::Interpolator,
488 path: Option<PrinterPath<'p>>,
489 start_time: Instant,
490 match_count: u64,
491 binary_byte_offset: Option<u64>,
492 stats: Option<Stats>,
493}
494
495impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
496 /// Returns true if and only if this printer received a match in the
497 /// previous search.
498 ///
499 /// This is unaffected by the result of searches before the previous
500 /// search.
501 pub fn has_match(&self) -> bool {
502 match self.summary.config.kind {
503 SummaryKind::PathWithoutMatch | SummaryKind::QuietWithoutMatch => {
504 self.match_count == 0
505 }
506 _ => self.match_count > 0,
507 }
508 }
509
510 /// If binary data was found in the previous search, this returns the
511 /// offset at which the binary data was first detected.
512 ///
513 /// The offset returned is an absolute offset relative to the entire
514 /// set of bytes searched.
515 ///
516 /// This is unaffected by the result of searches before the previous
517 /// search. e.g., If the search prior to the previous search found binary
518 /// data but the previous search found no binary data, then this will
519 /// return `None`.
520 pub fn binary_byte_offset(&self) -> Option<u64> {
521 self.binary_byte_offset
522 }
523
524 /// Return a reference to the stats produced by the printer for all
525 /// searches executed on this sink.
526 ///
527 /// This only returns stats if they were requested via the
528 /// [`SummaryBuilder`] configuration.
529 pub fn stats(&self) -> Option<&Stats> {
530 self.stats.as_ref()
531 }
532
533 /// Returns true if and only if the searcher may report matches over
534 /// multiple lines.
535 ///
536 /// Note that this doesn't just return whether the searcher is in multi
537 /// line mode, but also checks if the matter can match over multiple lines.
538 /// If it can't, then we don't need multi line handling, even if the
539 /// searcher has multi line mode enabled.
540 fn multi_line(&self, searcher: &Searcher) -> bool {
541 searcher.multi_line_with_matcher(&self.matcher)
542 }
543
544 /// If this printer has a file path associated with it, then this will
545 /// write that path to the underlying writer followed by a line terminator.
546 /// (If a path terminator is set, then that is used instead of the line
547 /// terminator.)
548 fn write_path_line(&mut self, searcher: &Searcher) -> io::Result<()> {
549 if self.path.is_some() {
550 self.write_path()?;
551 if let Some(term) = self.summary.config.path_terminator {
552 self.write(&[term])?;
553 } else {
554 self.write_line_term(searcher)?;
555 }
556 }
557 Ok(())
558 }
559
560 /// If this printer has a file path associated with it, then this will
561 /// write that path to the underlying writer followed by the field
562 /// separator. (If a path terminator is set, then that is used instead of
563 /// the field separator.)
564 fn write_path_field(&mut self) -> io::Result<()> {
565 if self.path.is_some() {
566 self.write_path()?;
567 if let Some(term) = self.summary.config.path_terminator {
568 self.write(&[term])?;
569 } else {
570 self.write(&self.summary.config.separator_field)?;
571 }
572 }
573 Ok(())
574 }
575
576 /// If this printer has a file path associated with it, then this will
577 /// write that path to the underlying writer in the appropriate style
578 /// (color and hyperlink).
579 fn write_path(&mut self) -> io::Result<()> {
580 if self.path.is_some() {
581 let status = self.start_hyperlink()?;
582 self.write_spec(
583 self.summary.config.colors.path(),
584 self.path.as_ref().unwrap().as_bytes(),
585 )?;
586 self.end_hyperlink(status)?;
587 }
588 Ok(())
589 }
590
591 /// Starts a hyperlink span when applicable.
592 fn start_hyperlink(
593 &mut self,
594 ) -> io::Result<hyperlink::InterpolatorStatus> {
595 let Some(hyperpath) =
596 self.path.as_ref().and_then(|p| p.as_hyperlink())
597 else {
598 return Ok(hyperlink::InterpolatorStatus::inactive());
599 };
600 let values = hyperlink::Values::new(hyperpath);
601 self.interpolator.begin(&values, &mut *self.summary.wtr.borrow_mut())
602 }
603
604 fn end_hyperlink(
605 &self,
606 status: hyperlink::InterpolatorStatus,
607 ) -> io::Result<()> {
608 self.interpolator.finish(status, &mut *self.summary.wtr.borrow_mut())
609 }
610
611 /// Write the line terminator configured on the given searcher.
612 fn write_line_term(&self, searcher: &Searcher) -> io::Result<()> {
613 self.write(searcher.line_terminator().as_bytes())
614 }
615
616 /// Write the given bytes using the give style.
617 fn write_spec(&self, spec: &ColorSpec, buf: &[u8]) -> io::Result<()> {
618 self.summary.wtr.borrow_mut().set_color(spec)?;
619 self.write(buf)?;
620 self.summary.wtr.borrow_mut().reset()?;
621 Ok(())
622 }
623
624 /// Write all of the given bytes.
625 fn write(&self, buf: &[u8]) -> io::Result<()> {
626 self.summary.wtr.borrow_mut().write_all(buf)
627 }
628}
629
630impl<'p, 's, M: Matcher, W: WriteColor> Sink for SummarySink<'p, 's, M, W> {
631 type Error = io::Error;
632
633 fn matched(
634 &mut self,
635 searcher: &Searcher,
636 mat: &SinkMatch<'_>,
637 ) -> Result<bool, io::Error> {
638 let is_multi_line = self.multi_line(searcher);
639 let sink_match_count = if self.stats.is_none() && !is_multi_line {
640 1
641 } else {
642 // This gives us as many bytes as the searcher can offer. This
643 // isn't guaranteed to hold the necessary context to get match
644 // detection correct (because of look-around), but it does in
645 // practice.
646 let buf = mat.buffer();
647 let range = mat.bytes_range_in_buffer();
648 let mut count = 0;
649 find_iter_at_in_context(
650 searcher,
651 &self.matcher,
652 buf,
653 range,
654 |_| {
655 count += 1;
656 true
657 },
658 )?;
659 // Because of `find_iter_at_in_context` being a giant
660 // kludge internally, it's possible that it won't find
661 // *any* matches even though we clearly know that there is
662 // at least one. So make sure we record at least one here.
663 count.max(1)
664 };
665 if is_multi_line {
666 self.match_count += sink_match_count;
667 } else {
668 self.match_count += 1;
669 }
670 if let Some(ref mut stats) = self.stats {
671 stats.add_matches(sink_match_count);
672 stats.add_matched_lines(mat.lines().count() as u64);
673 } else if self.summary.config.kind.quit_early() {
674 return Ok(false);
675 }
676 Ok(true)
677 }
678
679 fn binary_data(
680 &mut self,
681 searcher: &Searcher,
682 binary_byte_offset: u64,
683 ) -> Result<bool, io::Error> {
684 if searcher.binary_detection().quit_byte().is_some() {
685 if let Some(ref path) = self.path {
686 log::debug!(
687 "ignoring {path}: found binary data at \
688 offset {binary_byte_offset}",
689 path = path.as_path().display(),
690 );
691 }
692 }
693 Ok(true)
694 }
695
696 fn begin(&mut self, _searcher: &Searcher) -> Result<bool, io::Error> {
697 if self.path.is_none() && self.summary.config.kind.requires_path() {
698 return Err(io::Error::error_message(format!(
699 "output kind {:?} requires a file path",
700 self.summary.config.kind,
701 )));
702 }
703 self.summary.wtr.borrow_mut().reset_count();
704 self.start_time = Instant::now();
705 self.match_count = 0;
706 self.binary_byte_offset = None;
707 Ok(true)
708 }
709
710 fn finish(
711 &mut self,
712 searcher: &Searcher,
713 finish: &SinkFinish,
714 ) -> Result<(), io::Error> {
715 self.binary_byte_offset = finish.binary_byte_offset();
716 if let Some(ref mut stats) = self.stats {
717 stats.add_elapsed(self.start_time.elapsed());
718 stats.add_searches(1);
719 if self.match_count > 0 {
720 stats.add_searches_with_match(1);
721 }
722 stats.add_bytes_searched(finish.byte_count());
723 stats.add_bytes_printed(self.summary.wtr.borrow().count());
724 }
725 // If our binary detection method says to quit after seeing binary
726 // data, then we shouldn't print any results at all, even if we've
727 // found a match before detecting binary data. The intent here is to
728 // keep BinaryDetection::quit as a form of filter. Otherwise, we can
729 // present a matching file with a smaller number of matches than
730 // there might be, which can be quite misleading.
731 //
732 // If our binary detection method is to convert binary data, then we
733 // don't quit and therefore search the entire contents of the file.
734 //
735 // There is an unfortunate inconsistency here. Namely, when using
736 // QuietWithMatch or PathWithMatch, then the printer can quit after the
737 // first match seen, which could be long before seeing binary data.
738 // This means that using PathWithMatch can print a path where as using
739 // Count might not print it at all because of binary data.
740 //
741 // It's not possible to fix this without also potentially significantly
742 // impacting the performance of QuietWithMatch or PathWithMatch, so we
743 // accept the bug.
744 if self.binary_byte_offset.is_some()
745 && searcher.binary_detection().quit_byte().is_some()
746 {
747 // Squash the match count. The statistics reported will still
748 // contain the match count, but the "official" match count should
749 // be zero.
750 self.match_count = 0;
751 return Ok(());
752 }
753
754 let show_count =
755 !self.summary.config.exclude_zero || self.match_count > 0;
756 match self.summary.config.kind {
757 SummaryKind::Count => {
758 if show_count {
759 self.write_path_field()?;
760 self.write(self.match_count.to_string().as_bytes())?;
761 self.write_line_term(searcher)?;
762 }
763 }
764 SummaryKind::CountMatches => {
765 if show_count {
766 self.write_path_field()?;
767 let stats = self
768 .stats
769 .as_ref()
770 .expect("CountMatches should enable stats tracking");
771 self.write(stats.matches().to_string().as_bytes())?;
772 self.write_line_term(searcher)?;
773 }
774 }
775 SummaryKind::PathWithMatch => {
776 if self.match_count > 0 {
777 self.write_path_line(searcher)?;
778 }
779 }
780 SummaryKind::PathWithoutMatch => {
781 if self.match_count == 0 {
782 self.write_path_line(searcher)?;
783 }
784 }
785 SummaryKind::QuietWithMatch | SummaryKind::QuietWithoutMatch => {}
786 }
787 Ok(())
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use grep_regex::RegexMatcher;
794 use grep_searcher::SearcherBuilder;
795 use termcolor::NoColor;
796
797 use super::{Summary, SummaryBuilder, SummaryKind};
798
799 const SHERLOCK: &'static [u8] = b"\
800For the Doctor Watsons of this world, as opposed to the Sherlock
801Holmeses, success in the province of detective work must always
802be, to a very large extent, the result of luck. Sherlock Holmes
803can extract a clew from a wisp of straw or a flake of cigar ash;
804but Doctor Watson has to have it taken out for him and dusted,
805and exhibited clearly, with a label attached.
806";
807
808 fn printer_contents(printer: &mut Summary<NoColor<Vec<u8>>>) -> String {
809 String::from_utf8(printer.get_mut().get_ref().to_owned()).unwrap()
810 }
811
812 #[test]
813 fn path_with_match_error() {
814 let matcher = RegexMatcher::new(r"Watson").unwrap();
815 let mut printer = SummaryBuilder::new()
816 .kind(SummaryKind::PathWithMatch)
817 .build_no_color(vec![]);
818 let res = SearcherBuilder::new().build().search_reader(
819 &matcher,
820 SHERLOCK,
821 printer.sink(&matcher),
822 );
823 assert!(res.is_err());
824 }
825
826 #[test]
827 fn path_without_match_error() {
828 let matcher = RegexMatcher::new(r"Watson").unwrap();
829 let mut printer = SummaryBuilder::new()
830 .kind(SummaryKind::PathWithoutMatch)
831 .build_no_color(vec![]);
832 let res = SearcherBuilder::new().build().search_reader(
833 &matcher,
834 SHERLOCK,
835 printer.sink(&matcher),
836 );
837 assert!(res.is_err());
838 }
839
840 #[test]
841 fn count_no_path() {
842 let matcher = RegexMatcher::new(r"Watson").unwrap();
843 let mut printer = SummaryBuilder::new()
844 .kind(SummaryKind::Count)
845 .build_no_color(vec![]);
846 SearcherBuilder::new()
847 .build()
848 .search_reader(&matcher, SHERLOCK, printer.sink(&matcher))
849 .unwrap();
850
851 let got = printer_contents(&mut printer);
852 assert_eq_printed!("2\n", got);
853 }
854
855 #[test]
856 fn count_no_path_even_with_path() {
857 let matcher = RegexMatcher::new(r"Watson").unwrap();
858 let mut printer = SummaryBuilder::new()
859 .kind(SummaryKind::Count)
860 .path(false)
861 .build_no_color(vec![]);
862 SearcherBuilder::new()
863 .build()
864 .search_reader(
865 &matcher,
866 SHERLOCK,
867 printer.sink_with_path(&matcher, "sherlock"),
868 )
869 .unwrap();
870
871 let got = printer_contents(&mut printer);
872 assert_eq_printed!("2\n", got);
873 }
874
875 #[test]
876 fn count_path() {
877 let matcher = RegexMatcher::new(r"Watson").unwrap();
878 let mut printer = SummaryBuilder::new()
879 .kind(SummaryKind::Count)
880 .build_no_color(vec![]);
881 SearcherBuilder::new()
882 .build()
883 .search_reader(
884 &matcher,
885 SHERLOCK,
886 printer.sink_with_path(&matcher, "sherlock"),
887 )
888 .unwrap();
889
890 let got = printer_contents(&mut printer);
891 assert_eq_printed!("sherlock:2\n", got);
892 }
893
894 #[test]
895 fn count_path_with_zero() {
896 let matcher = RegexMatcher::new(r"NO MATCH").unwrap();
897 let mut printer = SummaryBuilder::new()
898 .kind(SummaryKind::Count)
899 .exclude_zero(false)
900 .build_no_color(vec![]);
901 SearcherBuilder::new()
902 .build()
903 .search_reader(
904 &matcher,
905 SHERLOCK,
906 printer.sink_with_path(&matcher, "sherlock"),
907 )
908 .unwrap();
909
910 let got = printer_contents(&mut printer);
911 assert_eq_printed!("sherlock:0\n", got);
912 }
913
914 #[test]
915 fn count_path_without_zero() {
916 let matcher = RegexMatcher::new(r"NO MATCH").unwrap();
917 let mut printer = SummaryBuilder::new()
918 .kind(SummaryKind::Count)
919 .exclude_zero(true)
920 .build_no_color(vec![]);
921 SearcherBuilder::new()
922 .build()
923 .search_reader(
924 &matcher,
925 SHERLOCK,
926 printer.sink_with_path(&matcher, "sherlock"),
927 )
928 .unwrap();
929
930 let got = printer_contents(&mut printer);
931 assert_eq_printed!("", got);
932 }
933
934 #[test]
935 fn count_path_field_separator() {
936 let matcher = RegexMatcher::new(r"Watson").unwrap();
937 let mut printer = SummaryBuilder::new()
938 .kind(SummaryKind::Count)
939 .separator_field(b"ZZ".to_vec())
940 .build_no_color(vec![]);
941 SearcherBuilder::new()
942 .build()
943 .search_reader(
944 &matcher,
945 SHERLOCK,
946 printer.sink_with_path(&matcher, "sherlock"),
947 )
948 .unwrap();
949
950 let got = printer_contents(&mut printer);
951 assert_eq_printed!("sherlockZZ2\n", got);
952 }
953
954 #[test]
955 fn count_path_terminator() {
956 let matcher = RegexMatcher::new(r"Watson").unwrap();
957 let mut printer = SummaryBuilder::new()
958 .kind(SummaryKind::Count)
959 .path_terminator(Some(b'\x00'))
960 .build_no_color(vec![]);
961 SearcherBuilder::new()
962 .build()
963 .search_reader(
964 &matcher,
965 SHERLOCK,
966 printer.sink_with_path(&matcher, "sherlock"),
967 )
968 .unwrap();
969
970 let got = printer_contents(&mut printer);
971 assert_eq_printed!("sherlock\x002\n", got);
972 }
973
974 #[test]
975 fn count_path_separator() {
976 let matcher = RegexMatcher::new(r"Watson").unwrap();
977 let mut printer = SummaryBuilder::new()
978 .kind(SummaryKind::Count)
979 .separator_path(Some(b'\\'))
980 .build_no_color(vec![]);
981 SearcherBuilder::new()
982 .build()
983 .search_reader(
984 &matcher,
985 SHERLOCK,
986 printer.sink_with_path(&matcher, "/home/andrew/sherlock"),
987 )
988 .unwrap();
989
990 let got = printer_contents(&mut printer);
991 assert_eq_printed!("\\home\\andrew\\sherlock:2\n", got);
992 }
993
994 #[test]
995 fn count_max_matches() {
996 let matcher = RegexMatcher::new(r"Watson").unwrap();
997 let mut printer = SummaryBuilder::new()
998 .kind(SummaryKind::Count)
999 .build_no_color(vec![]);
1000 SearcherBuilder::new()
1001 .max_matches(Some(1))
1002 .build()
1003 .search_reader(&matcher, SHERLOCK, printer.sink(&matcher))
1004 .unwrap();
1005
1006 let got = printer_contents(&mut printer);
1007 assert_eq_printed!("1\n", got);
1008 }
1009
1010 #[test]
1011 fn count_matches() {
1012 let matcher = RegexMatcher::new(r"Watson|Sherlock").unwrap();
1013 let mut printer = SummaryBuilder::new()
1014 .kind(SummaryKind::CountMatches)
1015 .build_no_color(vec![]);
1016 SearcherBuilder::new()
1017 .build()
1018 .search_reader(
1019 &matcher,
1020 SHERLOCK,
1021 printer.sink_with_path(&matcher, "sherlock"),
1022 )
1023 .unwrap();
1024
1025 let got = printer_contents(&mut printer);
1026 assert_eq_printed!("sherlock:4\n", got);
1027 }
1028
1029 #[test]
1030 fn path_with_match_found() {
1031 let matcher = RegexMatcher::new(r"Watson").unwrap();
1032 let mut printer = SummaryBuilder::new()
1033 .kind(SummaryKind::PathWithMatch)
1034 .build_no_color(vec![]);
1035 SearcherBuilder::new()
1036 .build()
1037 .search_reader(
1038 &matcher,
1039 SHERLOCK,
1040 printer.sink_with_path(&matcher, "sherlock"),
1041 )
1042 .unwrap();
1043
1044 let got = printer_contents(&mut printer);
1045 assert_eq_printed!("sherlock\n", got);
1046 }
1047
1048 #[test]
1049 fn path_with_match_not_found() {
1050 let matcher = RegexMatcher::new(r"ZZZZZZZZ").unwrap();
1051 let mut printer = SummaryBuilder::new()
1052 .kind(SummaryKind::PathWithMatch)
1053 .build_no_color(vec![]);
1054 SearcherBuilder::new()
1055 .build()
1056 .search_reader(
1057 &matcher,
1058 SHERLOCK,
1059 printer.sink_with_path(&matcher, "sherlock"),
1060 )
1061 .unwrap();
1062
1063 let got = printer_contents(&mut printer);
1064 assert_eq_printed!("", got);
1065 }
1066
1067 #[test]
1068 fn path_without_match_found() {
1069 let matcher = RegexMatcher::new(r"ZZZZZZZZZ").unwrap();
1070 let mut printer = SummaryBuilder::new()
1071 .kind(SummaryKind::PathWithoutMatch)
1072 .build_no_color(vec![]);
1073 SearcherBuilder::new()
1074 .build()
1075 .search_reader(
1076 &matcher,
1077 SHERLOCK,
1078 printer.sink_with_path(&matcher, "sherlock"),
1079 )
1080 .unwrap();
1081
1082 let got = printer_contents(&mut printer);
1083 assert_eq_printed!("sherlock\n", got);
1084 }
1085
1086 #[test]
1087 fn path_without_match_not_found() {
1088 let matcher = RegexMatcher::new(r"Watson").unwrap();
1089 let mut printer = SummaryBuilder::new()
1090 .kind(SummaryKind::PathWithoutMatch)
1091 .build_no_color(vec![]);
1092 SearcherBuilder::new()
1093 .build()
1094 .search_reader(
1095 &matcher,
1096 SHERLOCK,
1097 printer.sink_with_path(&matcher, "sherlock"),
1098 )
1099 .unwrap();
1100
1101 let got = printer_contents(&mut printer);
1102 assert_eq_printed!("", got);
1103 }
1104
1105 #[test]
1106 fn quiet() {
1107 let matcher = RegexMatcher::new(r"Watson|Sherlock").unwrap();
1108 let mut printer = SummaryBuilder::new()
1109 .kind(SummaryKind::QuietWithMatch)
1110 .build_no_color(vec![]);
1111 let match_count = {
1112 let mut sink = printer.sink_with_path(&matcher, "sherlock");
1113 SearcherBuilder::new()
1114 .build()
1115 .search_reader(&matcher, SHERLOCK, &mut sink)
1116 .unwrap();
1117 sink.match_count
1118 };
1119
1120 let got = printer_contents(&mut printer);
1121 assert_eq_printed!("", got);
1122 // There is actually more than one match, but Quiet should quit after
1123 // finding the first one.
1124 assert_eq!(1, match_count);
1125 }
1126
1127 #[test]
1128 fn quiet_with_stats() {
1129 let matcher = RegexMatcher::new(r"Watson|Sherlock").unwrap();
1130 let mut printer = SummaryBuilder::new()
1131 .kind(SummaryKind::QuietWithMatch)
1132 .stats(true)
1133 .build_no_color(vec![]);
1134 let match_count = {
1135 let mut sink = printer.sink_with_path(&matcher, "sherlock");
1136 SearcherBuilder::new()
1137 .build()
1138 .search_reader(&matcher, SHERLOCK, &mut sink)
1139 .unwrap();
1140 sink.match_count
1141 };
1142
1143 let got = printer_contents(&mut printer);
1144 assert_eq_printed!("", got);
1145 // There is actually more than one match, and Quiet will usually quit
1146 // after finding the first one, but since we request stats, it will
1147 // mush on to find all matches.
1148 assert_eq!(3, match_count);
1149 }
1150}