Skip to content

Ignore Crate lib.rs: Code Companion

Reference code for the ignore crate lib.rs lecture. Sections correspond to the lecture document.


Section 1: Crate Overview

/*!
The ignore crate provides a fast recursive directory iterator that respects
various filters such as globs, file types and `.gitignore` files. The precise
matching rules and precedence is explained in the documentation for
`WalkBuilder`.
*/

#![deny(missing_docs)]

use std::path::{Path, PathBuf};

// Re-exports from walk module — the main public API
pub use crate::walk::{
    DirEntry, ParallelVisitor, ParallelVisitorBuilder, Walk, WalkBuilder,
    WalkParallel, WalkState,
};

// Module declarations
mod default_types;  // Built-in file type definitions
mod dir;            // Per-directory ignore state
pub mod gitignore;  // Gitignore parsing (public)
pub mod overrides;  // CLI glob overrides (public)
mod pathutil;       // Path utilities (private)
pub mod types;      // File type matching (public)
mod walk;           // Directory traversal (items re-exported above)

Simple usage:

use ignore::Walk;

for result in Walk::new("./") {
    match result {
        Ok(entry) => println!("{}", entry.path().display()),
        Err(err) => println!("ERROR: {}", err),
    }
}

Advanced usage:

use ignore::WalkBuilder;

for result in WalkBuilder::new("./")
    .hidden(false)      // Include hidden files
    .git_ignore(true)   // Respect .gitignore
    .build() 
{
    println!("{:?}", result);
}

Section 2: The Error Enum — Variant Overview

/// Represents an error that can occur when parsing a gitignore file.
#[derive(Debug)]
pub enum Error {
    /// A collection of "soft" errors. These occur when adding an ignore
    /// file partially succeeded.
    Partial(Vec<Error>),

    /// An error associated with a specific line number.
    WithLineNumber {
        line: u64,
        err: Box<Error>,
    },

    /// An error associated with a particular file path.
    WithPath {
        path: PathBuf,
        err: Box<Error>,
    },

    /// An error associated with a particular directory depth.
    WithDepth {
        depth: usize,
        err: Box<Error>,
    },

    /// File system loop detected when following symbolic links.
    Loop {
        ancestor: PathBuf,
        child: PathBuf,
    },

    /// An I/O error.
    Io(std::io::Error),

    /// Glob parsing error.
    Glob {
        glob: Option<String>,
        err: String,
    },

    /// A type selection for a file type that is not defined.
    UnrecognizedFileType(String),

    /// A user specified file type definition could not be parsed.
    InvalidDefinition,
}

Error variant purposes:

Variant When Used
Partial Some operations succeed, others fail
WithLineNumber Error in a specific line of a file
WithPath Error associated with a file path
WithDepth Error at a specific directory depth
Loop Symlink cycle detected
Io Standard I/O operations
Glob Invalid glob pattern syntax
UnrecognizedFileType Unknown -t argument
InvalidDefinition Bad --type-add format

Section 3: Error Context Wrapping

// Example of nested error context:
// Error structure for a bad glob on line 5 of .gitignore:

Error::WithPath {
    path: PathBuf::from(".gitignore"),
    err: Box::new(Error::WithLineNumber {
        line: 5,
        err: Box::new(Error::Glob {
            glob: Some("**[bad".to_string()),
            err: "unclosed character class".to_string(),
        }),
    }),
}

// When displayed: ".gitignore: line 5: error parsing glob '**[bad': unclosed character class"

Why Box:

// Without Box, this would be infinite size:
enum Error {
    WithPath { path: PathBuf, err: Error },  // Error contains Error contains Error...
}

// With Box, the size is known:
enum Error {
    WithPath { path: PathBuf, err: Box<Error> },  // Box is pointer-sized
}

Section 4: Partial Errors

impl Error {
    /// Returns true if this is a partial error.
    ///
    /// A partial error occurs when only some operations failed while others
    /// may have succeeded.
    pub fn is_partial(&self) -> bool {
        match *self {
            Error::Partial(_) => true,
            Error::WithLineNumber { ref err, .. } => err.is_partial(),
            Error::WithPath { ref err, .. } => err.is_partial(),
            Error::WithDepth { ref err, .. } => err.is_partial(),
            _ => false,
        }
    }
}

Partial error scenario:

# .gitignore file:
*.log           # Line 1: valid
*.tmp           # Line 2: valid
**[broken       # Line 3: invalid - unclosed bracket
*.bak           # Line 4: valid
// Result: Partial error containing the line 3 error
// But lines 1, 2, 4 are still applied!

Error::Partial(vec![
    Error::WithLineNumber {
        line: 3,
        err: Box::new(Error::Glob { ... }),
    }
])

Section 5: I/O Error Handling

impl Error {
    /// Returns true if this error is exclusively an I/O error.
    pub fn is_io(&self) -> bool {
        match *self {
            Error::Partial(ref errs) => errs.len() == 1 && errs[0].is_io(),
            Error::WithLineNumber { ref err, .. } => err.is_io(),
            Error::WithPath { ref err, .. } => err.is_io(),
            Error::WithDepth { ref err, .. } => err.is_io(),
            Error::Loop { .. } => false,
            Error::Io(_) => true,
            Error::Glob { .. } => false,
            Error::UnrecognizedFileType(_) => false,
            Error::InvalidDefinition => false,
        }
    }

    /// Inspect the original std::io::Error if there is one.
    pub fn io_error(&self) -> Option<&std::io::Error> {
        match *self {
            Error::Partial(ref errs) => {
                if errs.len() == 1 {
                    errs[0].io_error()
                } else {
                    None
                }
            }
            Error::WithLineNumber { ref err, .. } => err.io_error(),
            Error::WithPath { ref err, .. } => err.io_error(),
            Error::WithDepth { ref err, .. } => err.is_io(),
            Error::Loop { .. } => None,
            Error::Io(ref err) => Some(err),
            Error::Glob { .. } => None,
            Error::UnrecognizedFileType(_) => None,
            Error::InvalidDefinition => None,
        }
    }
}

Clone implementation for io::Error:

impl Clone for Error {
    fn clone(&self) -> Error {
        match *self {
            // ... other variants ...

            // std::io::Error isn't Clone, but we can reconstruct it
            Error::Io(ref err) => match err.raw_os_error() {
                Some(e) => Error::Io(std::io::Error::from_raw_os_error(e)),
                None => {
                    Error::Io(std::io::Error::new(err.kind(), err.to_string()))
                }
            },

            // ... other variants ...
        }
    }
}

Section 6: The PartialErrorBuilder

#[derive(Debug, Default)]
struct PartialErrorBuilder(Vec<Error>);

impl PartialErrorBuilder {
    /// Push an error unconditionally.
    fn push(&mut self, err: Error) {
        self.0.push(err);
    }

    /// Push an error only if it's not an I/O error.
    fn push_ignore_io(&mut self, err: Error) {
        if !err.is_io() {
            self.push(err);
        }
    }

    /// Push an error if Some, ignore if None.
    fn maybe_push(&mut self, err: Option<Error>) {
        if let Some(err) = err {
            self.push(err);
        }
    }

    /// Push non-IO error if Some.
    fn maybe_push_ignore_io(&mut self, err: Option<Error>) {
        if let Some(err) = err {
            self.push_ignore_io(err);
        }
    }

    /// Convert accumulated errors to Option<Error>.
    /// None if empty, single error if one, Partial if multiple.
    fn into_error_option(mut self) -> Option<Error> {
        if self.0.is_empty() {
            None
        } else if self.0.len() == 1 {
            Some(self.0.pop().unwrap())
        } else {
            Some(Error::Partial(self.0))
        }
    }
}

Usage pattern:

fn process_lines(lines: &[String]) -> Option<Error> {
    let mut errs = PartialErrorBuilder::default();

    for (i, line) in lines.iter().enumerate() {
        if let Err(e) = parse_glob(line) {
            errs.push(Error::WithLineNumber {
                line: i as u64 + 1,
                err: Box::new(e),
            });
        }
    }

    errs.into_error_option()
}

Section 7: The Match Enum — The Filtering Decision

/// The result of a glob match.
///
/// The type parameter `T` typically refers to a type that provides more
/// information about a particular match.
#[derive(Clone, Debug)]
pub enum Match<T> {
    /// The path didn't match any glob.
    None,
    /// The highest precedent glob matched indicates the path should be
    /// ignored.
    Ignore(T),
    /// The highest precedent glob matched indicates the path should be
    /// whitelisted.
    Whitelist(T),
}

What T typically contains:

// For gitignore matching:
Match<gitignore::Glob>  // Which glob pattern matched

// For type matching:
Match<types::FileTypeDef>  // Which type definition matched

// For overrides:
Match<overrides::Glob>  // Which override glob matched

Section 8: Match Semantics — Ignore vs Whitelist

impl<T> Match<T> {
    /// Returns true if the match result didn't match any globs.
    pub fn is_none(&self) -> bool {
        match *self {
            Match::None => true,
            Match::Ignore(_) | Match::Whitelist(_) => false,
        }
    }

    /// Returns true if the match result implies the path should be ignored.
    pub fn is_ignore(&self) -> bool {
        match *self {
            Match::Ignore(_) => true,
            Match::None | Match::Whitelist(_) => false,
        }
    }

    /// Returns true if the match result implies the path should be whitelisted.
    pub fn is_whitelist(&self) -> bool {
        match *self {
            Match::Whitelist(_) => true,
            Match::None | Match::Ignore(_) => false,
        }
    }
}

Gitignore example:

# .gitignore
*.log           # Produces Ignore for foo.log
!important.log  # Produces Whitelist for important.log
// Matching "debug.log":
// First *.log matches → Ignore
// !important.log doesn't match
// Result: Ignore("*.log")

// Matching "important.log":
// First *.log matches → Ignore
// Then !important.log matches → Whitelist (takes precedence!)
// Result: Whitelist("!important.log")

Section 9: Match Combinators

impl<T> Match<T> {
    /// Inverts the match: Ignore ↔ Whitelist, None stays None.
    pub fn invert(self) -> Match<T> {
        match self {
            Match::None => Match::None,
            Match::Ignore(t) => Match::Whitelist(t),
            Match::Whitelist(t) => Match::Ignore(t),
        }
    }

    /// Return the value inside this match if it exists.
    pub fn inner(&self) -> Option<&T> {
        match *self {
            Match::None => None,
            Match::Ignore(ref t) => Some(t),
            Match::Whitelist(ref t) => Some(t),
        }
    }

    /// Apply a function to the inner value.
    pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Match<U> {
        match self {
            Match::None => Match::None,
            Match::Ignore(t) => Match::Ignore(f(t)),
            Match::Whitelist(t) => Match::Whitelist(f(t)),
        }
    }

    /// Return self if not None, otherwise return other.
    pub fn or(self, other: Self) -> Self {
        if self.is_none() { other } else { self }
    }
}

Invert usage (for -T flag):

// -t rust: include only rust files
let match_result = types.matched(path);  // Whitelist for .rs files

// -T rust: exclude rust files
let match_result = types.matched(path).invert();  // Now Ignore for .rs files

Or usage (combining matchers):

// Check override first, fall back to gitignore
let result = override_match.or(gitignore_match);

Section 10: How Match Flows Through the Crate

// Conceptual flow for deciding whether to yield a file:

fn should_yield(entry: &DirEntry) -> bool {
    let path = entry.path();

    // 1. Check command-line overrides (highest precedence)
    let override_match = overrides.matched(path, entry.is_dir());
    if override_match.is_whitelist() {
        return true;  // Explicit include
    }
    if override_match.is_ignore() {
        return false;  // Explicit exclude
    }

    // 2. Check file types (if any selected)
    if types_selected {
        let type_match = types.matched(path, entry.is_dir());
        if type_match.is_ignore() {
            return false;  // Doesn't match selected types
        }
    }

    // 3. Check gitignore rules
    let ignore_match = gitignore.matched(path, entry.is_dir());
    if ignore_match.is_ignore() {
        return false;
    }

    // 4. Default: yield the file
    true
}

Precedence summary:

Override Whitelist  →  Include (highest)
Override Ignore     →  Exclude
Type Whitelist      →  Include
Type Ignore         →  Exclude  
Gitignore Whitelist →  Include
Gitignore Ignore    →  Exclude
No match (None)     →  Include (default)

Quick Reference: Error and Match

// Error checking
err.is_partial()     // Contains multiple sub-errors?
err.is_io()          // Is this an I/O error?
err.io_error()       // Get &std::io::Error if present

// Match checking
m.is_none()          // No rule matched?
m.is_ignore()        // Should skip this file?
m.is_whitelist()     // Explicitly included?

// Match transformation
m.invert()           // Flip Ignore ↔ Whitelist
m.inner()            // Get &T if present
m.map(f)             // Transform inner value
m.or(other)          // Fallback if None

Type Aliases You'll See

// Common patterns in the crate:

type Result<T> = std::result::Result<T, Error>;

// Match with specific metadata:
type GitignoreMatch = Match<gitignore::Glob>;
type TypeMatch = Match<types::FileTypeDef>;
type OverrideMatch = Match<overrides::Glob>;