Skip to content

ripgrep crates/grep/examples/simplegrep.rs: Code Companion

Reference code for the Minimal Working Example lecture. Sections correspond to the lecture document.


Section 1: The Error Handling Entry Point

use std::{env, error::Error, ffi::OsString, io::IsTerminal, process};

fn main() {
    // Delegate to try_main and handle errors uniformly
    if let Err(err) = try_main() {
        eprintln!("{}", err);      // Print error to stderr
        process::exit(1);           // Exit with failure code
    }
}

fn try_main() -> Result<(), Box<dyn Error>> {
    // ... actual logic here, can use ? operator freely
}

The Box<dyn Error> return type accepts any error type implementing the Error trait. This is called a trait object—the dyn keyword indicates dynamic dispatch at runtime.


Section 2: Argument Parsing and Platform Safety

fn try_main() -> Result<(), Box<dyn Error>> {
    // args_os() returns OsString, not String—handles non-UTF-8 filenames
    let mut args: Vec<OsString> = env::args_os().collect();

    // Require at least: program name + pattern
    if args.len() < 2 {
        return Err("Usage: simplegrep <pattern> [<path> ...]".into());
    }

    // Default to current directory if no paths provided
    if args.len() == 2 {
        args.push(OsString::from("./"));
    }

    // args[0] is program name, args[1] is pattern, args[2..] are paths
    search(cli::pattern_from_os(&args[1])?, &args[2..])
}

The .into() call on the string literal converts &str into Box<dyn Error> through Rust's From trait implementations. The ? operator on pattern_from_os propagates any conversion error.


Section 3: Pattern Conversion with cli::pattern_from_os

use grep::cli;

// In try_main:
search(cli::pattern_from_os(&args[1])?, &args[2..])

// cli::pattern_from_os signature (from grep::cli module):
// pub fn pattern_from_os(pattern: &OsStr) -> Result<&str, Error>

This function attempts to interpret the OsString as UTF-8. If the pattern contains invalid UTF-8 bytes, it returns an error rather than panicking. The ? propagates this error to try_main's caller.


Section 4: The Matcher — Your First Builder Pattern

fn search(pattern: &str, paths: &[OsString]) -> Result<(), Box<dyn Error>> {
    // Create a matcher optimized for line-oriented searching
    // Pattern never matches across line boundaries
    let matcher = RegexMatcher::new_line_matcher(&pattern)?;

    // ... rest of search function
}

RegexMatcher::new_line_matcher compiles the pattern and configures it to respect line boundaries. The ? handles regex syntax errors—an invalid pattern like [unclosed would return an error here.


Section 5: The Searcher Builder — Configuration Through Method Chaining

use grep::searcher::{BinaryDetection, SearcherBuilder};

// Builder pattern: new() → configure → build()
let mut searcher = SearcherBuilder::new()
    // Stop searching if we hit a null byte (indicates binary file)
    .binary_detection(BinaryDetection::quit(b'\x00'))
    // Don't track line numbers (simpler output, slight perf gain)
    .line_number(false)
    // Consume the builder, produce the Searcher
    .build();

The mut on searcher is required because search_path() modifies internal buffers. Each builder method returns &mut Self, enabling the fluent chaining syntax.


Section 6: The Printer Builder — Terminal-Aware Output

use std::io::IsTerminal;
use grep::{cli, printer::{ColorSpecs, StandardBuilder}};
use termcolor::ColorChoice;

let mut printer = StandardBuilder::new()
    // Set up default colors for matches, filenames, etc.
    .color_specs(ColorSpecs::default_with_color())
    // build() takes a WriteColor implementation
    .build(cli::stdout(if std::io::stdout().is_terminal() {
        ColorChoice::Auto    // Terminal: let printer decide on colors
    } else {
        ColorChoice::Never   // Pipe/file: no ANSI escape codes
    }));

is_terminal() (Rust 1.70+) detects whether stdout connects to an interactive terminal. The cli::stdout() function wraps the standard output handle with buffering and color support.


Section 7: Directory Walking with WalkDir

use walkdir::WalkDir;

for path in paths {
    // WalkDir recursively yields directory entries
    for result in WalkDir::new(path) {
        let dent = match result {
            Ok(dent) => dent,
            Err(err) => {
                // Permission denied, broken symlink, etc.
                eprintln!("{}", err);
                continue;  // Skip this entry, keep searching
            }
        };

        // Skip directories, symlinks, etc.—only search regular files
        if !dent.file_type().is_file() {
            continue;
        }

        // dent.path() returns &Path for use in search
    }
}

WalkDir handles recursive traversal including symlinks, permissions, and cross-platform path handling. Each result is a Result<DirEntry, walkdir::Error>.


Section 8: The Search Execution — Bringing It Together

// Inside the directory walking loop:
let result = searcher.search_path(
    &matcher,                                    // Pattern to match
    dent.path(),                                 // File to search
    printer.sink_with_path(&matcher, dent.path()), // Where results go
);

// Handle search errors (I/O errors, encoding issues, etc.)
if let Err(err) = result {
    eprintln!("{}: {}", dent.path().display(), err);
}

The Sink pattern decouples searching from output. sink_with_path creates a sink that knows the filename and matcher—it handles formatting each match and writing to the printer's output.


Quick Reference

Component Responsibilities

Component Type Purpose
Matcher RegexMatcher Compiles and executes regex pattern
Searcher Searcher Reads files, finds matches, manages buffers
Printer Standard<W> Formats and outputs search results
Sink StandardSink<...> Bridges searcher to printer

Builder Pattern Summary

// General pattern:
let thing = ThingBuilder::new()
    .option_a(value)
    .option_b(value)
    .build();  // Consumes builder, returns Thing

Key Type Signatures

// Matcher construction
RegexMatcher::new_line_matcher(pattern: &str) -> Result<RegexMatcher, Error>

// Searcher execution
Searcher::search_path<M, P, S>(
    &mut self,
    matcher: M,
    path: P,
    sink: S
) -> Result<(), S::Error>
where
    M: Matcher,
    P: AsRef<Path>,
    S: Sink

// Printer sink creation
Standard<W>::sink_with_path<M, P>(
    &self,
    matcher: M,
    path: P
) -> StandardSink<'_, M, W>

Import Summary

use std::{env, error::Error, ffi::OsString, io::IsTerminal, process};
use grep::{cli, printer::*, regex::RegexMatcher, searcher::*};
use termcolor::ColorChoice;
use walkdir::WalkDir;