Ripgrep messages.rs: Code Companion¶
Reference code for the messages.rs lecture. Sections correspond to the lecture document.
Section 1: The Problem Being Solved¶
// Without synchronization, concurrent writes interleave:
// Thread 1: eprintln!("rg: file1.txt: permission denied");
// Thread 2: eprintln!("rg: file2.txt: not found");
// Output: "rg: file1.trg:x t:f ilpee2r.mtixsts:i opne rdmenisesdi oenn ideed"
// Even locking per-call doesn't help:
// Thread 1: lock stderr, write "rg: ", unlock
// Thread 2: lock stderr, write "rg: ", unlock <- interleaves here
// Thread 1: lock stderr, write "file1.txt...", unlock
The solution: Hold a single lock across the entire message output.
Section 2: The Global State¶
use std::sync::atomic::{AtomicBool, Ordering};
/// When false, "messages" will not be printed.
static MESSAGES: AtomicBool = AtomicBool::new(false);
/// When false, "messages" related to ignore rules will not be printed.
static IGNORE_MESSAGES: AtomicBool = AtomicBool::new(false);
/// Flipped to true when an error message is printed.
static ERRORED: AtomicBool = AtomicBool::new(false);
Flag purposes:
| Flag | Default | Controlled By | Purpose |
|---|---|---|---|
MESSAGES |
false | --no-messages |
Master switch for all messages |
IGNORE_MESSAGES |
false | --no-ignore-messages |
Ignore file parse errors |
ERRORED |
false | err_message! macro |
Track if any error occurred |
Lifecycle:
Program start → All flags false
Argument parsing → MESSAGES, IGNORE_MESSAGES configured
Parallel search → ERRORED may flip to true
Program exit → ERRORED checked for exit status
Section 3: Memory Ordering¶
// All operations use Relaxed ordering
MESSAGES.load(Ordering::Relaxed)
MESSAGES.store(yes, Ordering::Relaxed)
Why Relaxed is safe here:
Timeline:
────────────────────────────────────────────────────────────
│ Single-threaded init │ Parallel search phase │ Exit │
────────────────────────────────────────────────────────────
↑ ↑ ↑
Flags set here Flags read here ERRORED read
No concurrent writes during reads → Relaxed is sufficient
Memory ordering comparison:
| Ordering | Guarantees | Use Case |
|---|---|---|
Relaxed |
Atomicity only | Flags set before threads spawn |
Acquire/Release |
Synchronizes memory | Producer/consumer patterns |
SeqCst |
Total ordering | When order matters across threads |
Section 4: The eprintln_locked Macro¶
/// Like eprintln, but locks stdout to prevent interleaving lines.
///
/// This locks stdout, not stderr, even though this prints to stderr. This
/// avoids the appearance of interleaving output when stdout and stderr both
/// correspond to a tty.
#[macro_export]
macro_rules! eprintln_locked {
($($tt:tt)*) => {{
{
use std::io::Write;
// This is a bit of an abstraction violation because we explicitly
// lock stdout before printing to stderr. This avoids interleaving
// lines within ripgrep because `search_parallel` uses `termcolor`,
// which accesses the same stdout lock when writing lines.
let stdout = std::io::stdout().lock();
let mut stderr = std::io::stderr().lock();
// Error handling for write operations...
if let Err(err) = write!(stderr, "rg: ") {
// ... handle error
}
if let Err(err) = writeln!(stderr, $($tt)*) {
// ... handle error
}
drop(stdout); // Explicit drop for clarity
}
}}
}
Why lock stdout for stderr writes:
Search thread (via termcolor): Error thread:
───────────────────────────── ─────────────
lock(stdout)
write match to stdout
lock(stdout) ← blocks here
unlock(stdout)
lock(stderr)
write error to stderr
unlock(stderr)
unlock(stdout)
Result: Match output completes before error appears
The macro pattern $($tt:tt)*:
// $tt:tt matches any single token tree
// $($tt:tt)* matches zero or more token trees
// This captures arbitrary format arguments:
eprintln_locked!("simple message");
eprintln_locked!("{}: {}", path, err);
eprintln_locked!("{:#}", err); // Pretty-print with error chain
Section 5: Broken Pipe Handling¶
// Within eprintln_locked:
if let Err(err) = write!(stderr, "rg: ") {
if err.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0); // Broken pipe = success
} else {
std::process::exit(2); // Other error = failure
}
}
if let Err(err) = writeln!(stderr, $($tt)*) {
if err.kind() == std::io::ErrorKind::BrokenPipe {
std::process::exit(0);
} else {
std::process::exit(2);
}
}
Broken pipe scenario:
$ rg pattern 2>&1 | head -5
# head exits after 5 lines
# ripgrep's next write fails with BrokenPipe
# Exit code 0 (success) because head got what it needed
Exit codes:
exit(0) // Success (matches found, or broken pipe)
exit(1) // No matches found
exit(2) // Error occurred
Section 6: The message Macro¶
/// Emit a non-fatal error message, unless messages were disabled.
#[macro_export]
macro_rules! message {
($($tt:tt)*) => {
if crate::messages::messages() {
eprintln_locked!($($tt)*);
}
}
}
Conditional output:
// With --no-messages flag:
set_messages(false);
message!("{}: {}", path, err); // No output, but expression evaluated
// Default behavior:
set_messages(true);
message!("{}: {}", path, err); // Prints: "rg: path: err"
Note on macro hygiene:
// #[macro_export] makes the macro available at crate root
// Usage elsewhere: crate::message! or just message! with #[macro_use]
Section 7: The err_message Macro¶
/// Like message, but sets ripgrep's "errored" flag, which controls the exit
/// status.
#[macro_export]
macro_rules! err_message {
($($tt:tt)*) => {
crate::messages::set_errored(); // Always set, even if messages disabled
message!($($tt)*); // Conditionally print
}
}
Exit status logic in main.rs:
// Simplified from actual code:
Ok(if matched && !messages::errored() {
ExitCode::from(0) // Matches found, no errors
} else if messages::errored() {
ExitCode::from(2) // Error occurred
} else {
ExitCode::from(1) // No matches found
})
Why separate set_errored from printing:
// Error tracking happens even with --no-messages
err_message!("{}: {}", path, err);
// ERRORED = true (always)
// Message printed only if MESSAGES = true
Section 8: The ignore_message Macro¶
/// Emit a non-fatal ignore-related error message (like a parse error), unless
/// ignore-messages were disabled.
#[macro_export]
macro_rules! ignore_message {
($($tt:tt)*) => {
if crate::messages::messages() && crate::messages::ignore_messages() {
eprintln_locked!($($tt)*);
}
}
}
Two-level gating:
MESSAGES = true, IGNORE_MESSAGES = true → prints
MESSAGES = true, IGNORE_MESSAGES = false → silent
MESSAGES = false, IGNORE_MESSAGES = true → silent
MESSAGES = false, IGNORE_MESSAGES = false → silent
Typical ignore message:
ignore_message!("{}: {}", gitignore_path, parse_error);
// Output: "rg: .gitignore: invalid glob pattern on line 5"
Section 9: The Accessor Functions¶
/// Returns true if and only if messages should be shown.
pub(crate) fn messages() -> bool {
MESSAGES.load(Ordering::Relaxed)
}
/// Set whether messages should be shown or not.
///
/// By default, they are not shown.
pub(crate) fn set_messages(yes: bool) {
MESSAGES.store(yes, Ordering::Relaxed)
}
/// Returns true if and only if "ignore" related messages should be shown.
pub(crate) fn ignore_messages() -> bool {
IGNORE_MESSAGES.load(Ordering::Relaxed)
}
/// Set whether "ignore" related messages should be shown or not.
///
/// By default, they are not shown.
///
/// Note that this is overridden if `messages` is disabled.
pub(crate) fn set_ignore_messages(yes: bool) {
IGNORE_MESSAGES.store(yes, Ordering::Relaxed)
}
/// Returns true if and only if ripgrep came across a non-fatal error.
pub(crate) fn errored() -> bool {
ERRORED.load(Ordering::Relaxed)
}
/// Indicate that ripgrep has come across a non-fatal error.
///
/// Callers should not use this directly. Instead, it is called automatically
/// via the `err_message` macro.
pub(crate) fn set_errored() {
ERRORED.store(true, Ordering::Relaxed);
}
Visibility:
Section 10: Usage Patterns in main.rs¶
Initialization during argument parsing:
// In hiargs.rs or similar:
messages::set_messages(!low.no_messages);
messages::set_ignore_messages(!low.no_ignore_messages);
Error handling in search loop:
// From main.rs search function:
let search_result = match searcher.search(&haystack) {
Ok(search_result) => search_result,
Err(err) if err.kind() == std::io::ErrorKind::BrokenPipe => break,
Err(err) => {
err_message!("{}: {}", haystack.path().display(), err);
continue; // Non-fatal: keep searching other files
}
};
Exit status determination:
// From main.rs run function:
Ok(if matched && (args.quiet() || !messages::errored()) {
ExitCode::from(0)
} else if messages::errored() {
ExitCode::from(2)
} else {
ExitCode::from(1)
})
Quick Reference: Macro Invocations¶
// Thread-safe stderr with "rg: " prefix, always outputs
eprintln_locked!("message");
eprintln_locked!("{}: {}", path, err);
eprintln_locked!("{:#}", anyhow_err); // Full error chain
// Conditional on MESSAGES flag
message!("warning: {}", msg);
// Sets ERRORED flag, conditional output on MESSAGES
err_message!("{}: {}", path, err);
// Conditional on both MESSAGES and IGNORE_MESSAGES
ignore_message!("{}: {}", ignore_path, parse_err);
Data Flow Summary¶
CLI Arguments
│
▼
┌─────────────────────────────────┐
│ Argument Parsing (single-thread)│
│ set_messages(true/false) │
│ set_ignore_messages(true/false) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Parallel Search Phase │
│ │
│ Thread 1: err_message!(...) ───┼──► ERRORED = true
│ Thread 2: message!(...) │ (if MESSAGES) → stderr
│ Thread 3: ignore_message!(...) │ (if both flags) → stderr
│ ↓ │
│ lock(stdout) │
│ lock(stderr) │
│ write "rg: ..." │
│ unlock(stderr) │
│ unlock(stdout) │
└─────────────────────────────────┘
│
▼
┌─────────────────────────────────┐
│ Exit Status Determination │
│ if errored() → exit(2) │
│ elif matched → exit(0) │
│ else → exit(1) │
└─────────────────────────────────┘
Comparison with Standard Macros¶
// Standard library (not thread-safe for multi-line):
eprintln!("rg: {}", msg);
// This module (thread-safe):
eprintln_locked!("{}", msg); // Adds "rg: " prefix automatically
// Key differences:
// 1. Locks stdout AND stderr
// 2. Prefixes all output with "rg: "
// 3. Handles broken pipe gracefully
// 4. Exits on write errors