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):
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