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