Skip to content

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:

pub(crate)  // Visible within the ripgrep binary crate only
            // Not exported to library users

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