Skip to content

ripgrep crates/printer/src/summary.rs: Code Companion

Reference code for the Summary Output lecture. Sections correspond to the lecture document.


Section 1: The Six Summary Modes

/// The type of summary output (if any) to print.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SummaryKind {
    /// Show only a count of the total number of matches (counting each line
    /// at most once) found.
    Count,
    /// Show only a count of the total number of matches (counting possibly
    /// many matches on each line) found.
    CountMatches,
    /// Show only the file path if and only if a match was found.
    PathWithMatch,
    /// Show only the file path if and only if no match was found.
    PathWithoutMatch,
    /// Don't show any output and stop the search once a match is found.
    QuietWithMatch,
    /// Don't show any output and stop the search once a non-matching file
    /// is found.
    QuietWithoutMatch,
}

impl SummaryKind {
    /// Returns true if this mode requires a file path to make sense.
    fn requires_path(&self) -> bool {
        use self::SummaryKind::*;
        match *self {
            PathWithMatch | PathWithoutMatch => true,
            Count | CountMatches | QuietWithMatch | QuietWithoutMatch => false,
        }
    }

    /// Returns true if this mode needs statistics regardless of user config.
    fn requires_stats(&self) -> bool {
        use self::SummaryKind::*;
        match *self {
            CountMatches => true,  // Must count individual matches, not just lines
            Count | PathWithMatch | PathWithoutMatch | QuietWithMatch
            | QuietWithoutMatch => false,
        }
    }

    /// Returns true if the search can stop after the first match.
    fn quit_early(&self) -> bool {
        use self::SummaryKind::*;
        match *self {
            PathWithMatch | QuietWithMatch => true,  // Only need to know "any match exists"
            Count | CountMatches | PathWithoutMatch | QuietWithoutMatch => false,
        }
    }
}

These three methods form a behavior matrix that the rest of the code consults. CountMatches forces statistics on because it must count each match individually, not just matching lines.


Section 2: Configuration Through the Builder Pattern

/// Configuration frozen after printer is built.
#[derive(Debug, Clone)]
struct Config {
    kind: SummaryKind,
    colors: ColorSpecs,
    hyperlink: HyperlinkConfig,
    stats: bool,
    path: bool,
    exclude_zero: bool,
    separator_field: Arc<Vec<u8>>,    // What goes between path and count
    separator_path: Option<u8>,        // Replace path separators (for Windows/Cygwin)
    path_terminator: Option<u8>,       // Custom terminator after paths
}

impl Default for Config {
    fn default() -> Config {
        Config {
            kind: SummaryKind::Count,
            colors: ColorSpecs::default(),
            hyperlink: HyperlinkConfig::default(),
            stats: false,
            path: true,
            exclude_zero: true,                      // Don't show "file:0" by default
            separator_field: Arc::new(b":".to_vec()), // Classic grep format
            separator_path: None,
            path_terminator: None,
        }
    }
}

/// Builder with consistent method chaining pattern.
#[derive(Clone, Debug)]
pub struct SummaryBuilder {
    config: Config,
}

impl SummaryBuilder {
    pub fn new() -> SummaryBuilder {
        SummaryBuilder { config: Config::default() }
    }

    /// Each setter follows the same pattern: mutate config, return &mut self.
    pub fn kind(&mut self, kind: SummaryKind) -> &mut SummaryBuilder {
        self.config.kind = kind;
        self
    }

    pub fn separator_field(&mut self, sep: Vec<u8>) -> &mut SummaryBuilder {
        self.config.separator_field = Arc::new(sep);
        self
    }

    /// Build clones the config into the final printer.
    pub fn build<W: WriteColor>(&self, wtr: W) -> Summary<W> {
        Summary {
            config: self.config.clone(),
            wtr: RefCell::new(CounterWriter::new(wtr)),
        }
    }

    /// Convenience method that wraps writer in NoColor.
    pub fn build_no_color<W: io::Write>(&self, wtr: W) -> Summary<NoColor<W>> {
        self.build(NoColor::new(wtr))
    }
}

The Arc<Vec<u8>> for separator_field allows cheap cloning of the config while sharing the separator bytes across all uses.


Section 3: The Summary Printer and RefCell

/// The summary printer wraps a writer with interior mutability.
#[derive(Clone, Debug)]
pub struct Summary<W> {
    config: Config,
    wtr: RefCell<CounterWriter<W>>,  // Interior mutability for borrowing flexibility
}

impl<W: WriteColor> Summary<W> {
    /// Create a sink without a file path association.
    pub fn sink<'s, M: Matcher>(
        &'s mut self,
        matcher: M,
    ) -> SummarySink<'static, 's, M, W> {  // 'static because no path borrowed
        let interpolator = hyperlink::Interpolator::new(&self.config.hyperlink);
        // Enable stats if user requested OR if mode requires them
        let stats = if self.config.stats || self.config.kind.requires_stats() {
            Some(Stats::new())
        } else {
            None
        };
        SummarySink {
            matcher,
            summary: self,
            interpolator,
            path: None,  // No path for this sink
            start_time: Instant::now(),
            match_count: 0,
            binary_byte_offset: None,
            stats,
        }
    }

    /// Create a sink with a file path for output.
    pub fn sink_with_path<'p, 's, M, P>(
        &'s mut self,
        matcher: M,
        path: &'p P,
    ) -> SummarySink<'p, 's, M, W>
    where
        M: Matcher,
        P: ?Sized + AsRef<Path>,
    {
        // If path display is disabled AND mode doesn't require path, skip path processing
        if !self.config.path && !self.config.kind.requires_path() {
            return self.sink(matcher);
        }
        // ... (similar setup with path normalization)
        let ppath = PrinterPath::new(path.as_ref())
            .with_separator(self.config.separator_path);
        // ...
    }

    /// Query whether any output has been produced.
    pub fn has_written(&self) -> bool {
        self.wtr.borrow().total_count() > 0
    }
}

The CounterWriter tracks bytes written, enabling has_written() to report output status without examining content.


Section 4: The SummarySink State Machine

/// Sink implementation that receives search callbacks.
#[derive(Debug)]
pub struct SummarySink<'p, 's, M: Matcher, W> {
    matcher: M,                              // The pattern matcher
    summary: &'s mut Summary<W>,             // Borrowed printer
    interpolator: hyperlink::Interpolator,   // For clickable terminal links
    path: Option<PrinterPath<'p>>,           // Borrowed or 'static if none
    start_time: Instant,                     // For performance stats
    match_count: u64,                        // Accumulated matches
    binary_byte_offset: Option<u64>,         // If binary detected, where
    stats: Option<Stats>,                    // Detailed stats when enabled
}

impl<'p, 's, M: Matcher, W: WriteColor> SummarySink<'p, 's, M, W> {
    /// Interprets match_count based on the output mode.
    pub fn has_match(&self) -> bool {
        match self.summary.config.kind {
            // For "without match" modes, success means zero matches
            SummaryKind::PathWithoutMatch | SummaryKind::QuietWithoutMatch => {
                self.match_count == 0
            }
            // For all other modes, success means at least one match
            _ => self.match_count > 0,
        }
    }

    /// Check if the matcher can produce multi-line matches.
    fn multi_line(&self, searcher: &Searcher) -> bool {
        searcher.multi_line_with_matcher(&self.matcher)
    }
}

The lifetime 'p is 'static when no path is provided, allowing the sink to work uniformly with or without paths.


Section 5: Match Counting Subtleties

impl<'p, 's, M: Matcher, W: WriteColor> Sink for SummarySink<'p, 's, M, W> {
    type Error = io::Error;

    fn matched(
        &mut self,
        searcher: &Searcher,
        mat: &SinkMatch<'_>,
    ) -> Result<bool, io::Error> {
        let is_multi_line = self.multi_line(searcher);

        // Simple case: single-line mode without stats, one callback = one match
        let sink_match_count = if self.stats.is_none() && !is_multi_line {
            1
        } else {
            // Complex case: re-scan to count individual matches in the region
            let buf = mat.buffer();
            let range = mat.bytes_range_in_buffer();
            let mut count = 0;
            find_iter_at_in_context(
                searcher,
                &self.matcher,
                buf,
                range,
                |_| {
                    count += 1;
                    true  // Continue iterating
                },
            )?;
            // Guard against edge cases where find_iter misses matches
            count.max(1)
        };

        // Update counts based on search mode
        if is_multi_line {
            self.match_count += sink_match_count;
        } else {
            self.match_count += 1;  // In single-line, count lines not matches
        }

        // Update statistics if enabled
        if let Some(ref mut stats) = self.stats {
            stats.add_matches(sink_match_count);
            stats.add_matched_lines(mat.lines().count() as u64);
        } else if self.summary.config.kind.quit_early() {
            // Early termination: signal searcher to stop
            return Ok(false);
        }
        Ok(true)  // Continue searching
    }
}

Returning Ok(false) signals the searcher to stop—critical for PathWithMatch mode where one match suffices.


Quick Reference

Mode Output Needs Path Needs Stats Quits Early
Count file:N No No No
CountMatches file:N No Yes No
PathWithMatch file Yes No Yes
PathWithoutMatch file Yes No No
QuietWithMatch (none) No No Yes
QuietWithoutMatch (none) No No No

Key Type Signatures

// The Sink trait callback return value
fn matched(&mut self, ...) -> Result<bool, io::Error>
//                                   ^^^^
//                                   false = stop searching

// Lifetime parameters on SummarySink
SummarySink<'p, 's, M, W>
//          'p = path lifetime ('static if no path)
//          's = summary printer lifetime
//          M  = Matcher implementation
//          W  = WriteColor implementation

Builder Method Pattern

pub fn option_name(&mut self, value: Type) -> &mut SummaryBuilder {
    self.config.field = value;
    self
}