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>