Skip to main content
The bomboni_fs crate provides utilities for recursively visiting files in a directory tree with support for filtering by file extensions.

Functions

visit_files

Recursively visits files in a directory with specified extensions and executes a callback for each matching file.
visit_files
fn
pub fn visit_files<P, E>(
    dir: P,
    extensions: &[&str],
    cb: &mut dyn FnMut(&DirEntry) -> Result<(), E>,
) -> Result<(), E>
where
    P: AsRef<Path>,
    E: Error + From<io::Error>
Recursively traverses a directory tree and calls the provided callback for each file that matches one of the specified extensions.Parameters:
  • dir - The directory path to start traversing from
  • extensions - A slice of file extensions to match (without the dot, e.g., "rs", "txt")
  • cb - A mutable callback function that receives a reference to each matching DirEntry
Returns: Result<(), E> where E is your custom error typeErrors:Returns an error if:
  • Directory reading fails
  • The callback returns an error
Example:
use bomboni_fs::visit_files;
use std::fs::DirEntry;

fn process_rust_files() -> std::io::Result<()> {
    let mut file_count = 0;
    
    visit_files(
        "./src",
        &["rs"],
        &mut |entry: &DirEntry| {
            println!("Found Rust file: {}", entry.path().display());
            file_count += 1;
            Ok(())
        }
    )?;
    
    println!("Total Rust files: {}", file_count);
    Ok(())
}

visit_files_contents

Recursively visits files in a directory and reads their contents, executing a callback with both the file entry and its contents.
visit_files_contents
fn
pub fn visit_files_contents<P, E>(
    dir: P,
    extensions: &[&str],
    cb: &mut dyn FnMut(&DirEntry, String) -> Result<(), E>,
) -> Result<(), E>
where
    P: AsRef<Path>,
    E: Error + From<io::Error>
Recursively traverses a directory tree and calls the provided callback for each file that matches one of the specified extensions. The callback receives both the file entry and its contents as a string.Parameters:
  • dir - The directory path to start traversing from
  • extensions - A slice of file extensions to match (without the dot, e.g., "rs", "txt")
  • cb - A mutable callback function that receives a reference to each matching DirEntry and the file’s contents as a String
Returns: Result<(), E> where E is your custom error typeErrors:Returns an error if:
  • Directory reading fails
  • File reading fails
  • The callback returns an error
Example:
use bomboni_fs::visit_files_contents;
use std::fs::DirEntry;

fn count_lines() -> std::io::Result<()> {
    let mut total_lines = 0;
    
    visit_files_contents(
        "./src",
        &["rs"],
        &mut |entry: &DirEntry, content: String| {
            let lines = content.lines().count();
            println!("{}: {} lines", entry.path().display(), lines);
            total_lines += lines;
            Ok(())
        }
    )?;
    
    println!("Total lines: {}", total_lines);
    Ok(())
}

Usage Examples

Collecting File Paths

use bomboni_fs::visit_files;
use std::path::PathBuf;

fn find_all_rust_files(dir: &str) -> std::io::Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    
    visit_files(
        dir,
        &["rs"],
        &mut |entry| {
            files.push(entry.path());
            Ok(())
        }
    )?;
    
    Ok(files)
}

let rust_files = find_all_rust_files("./src")?;
for file in rust_files {
    println!("Found: {}", file.display());
}

Processing Multiple File Types

use bomboni_fs::visit_files;
use std::collections::HashMap;

fn count_files_by_extension(dir: &str) -> std::io::Result<HashMap<String, usize>> {
    let mut counts = HashMap::new();
    
    visit_files(
        dir,
        &["rs", "toml", "md"],
        &mut |entry| {
            if let Some(ext) = entry.path().extension() {
                let ext_str = ext.to_string_lossy().to_string();
                *counts.entry(ext_str).or_insert(0) += 1;
            }
            Ok(())
        }
    )?;
    
    Ok(counts)
}

let counts = count_files_by_extension("./")?;
for (ext, count) in counts {
    println!(".{}: {} files", ext, count);
}

Analyzing File Contents

use bomboni_fs::visit_files_contents;

#[derive(Default)]
struct CodeStats {
    total_files: usize,
    total_lines: usize,
    total_chars: usize,
}

fn analyze_rust_code(dir: &str) -> std::io::Result<CodeStats> {
    let mut stats = CodeStats::default();
    
    visit_files_contents(
        dir,
        &["rs"],
        &mut |_entry, content| {
            stats.total_files += 1;
            stats.total_lines += content.lines().count();
            stats.total_chars += content.len();
            Ok(())
        }
    )?;
    
    Ok(stats)
}

let stats = analyze_rust_code("./src")?;
println!("Files: {}", stats.total_files);
println!("Lines: {}", stats.total_lines);
println!("Characters: {}", stats.total_chars);

Searching for Patterns

use bomboni_fs::visit_files_contents;
use std::path::PathBuf;

fn find_pattern(dir: &str, pattern: &str) -> std::io::Result<Vec<(PathBuf, Vec<usize>)>> {
    let mut matches = Vec::new();
    
    visit_files_contents(
        dir,
        &["rs", "txt"],
        &mut |entry, content| {
            let mut line_numbers = Vec::new();
            
            for (line_num, line) in content.lines().enumerate() {
                if line.contains(pattern) {
                    line_numbers.push(line_num + 1);
                }
            }
            
            if !line_numbers.is_empty() {
                matches.push((entry.path(), line_numbers));
            }
            
            Ok(())
        }
    )?;
    
    Ok(matches)
}

let results = find_pattern("./src", "TODO")?;
for (path, lines) in results {
    println!("{}:", path.display());
    for line_num in lines {
        println!("  Line {}", line_num);
    }
}

Error Handling with Custom Error Types

use bomboni_fs::visit_files_contents;
use std::fmt;
use std::io;

#[derive(Debug)]
enum ProcessError {
    Io(io::Error),
    InvalidContent(String),
}

impl fmt::Display for ProcessError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ProcessError::Io(e) => write!(f, "IO error: {}", e),
            ProcessError::InvalidContent(msg) => write!(f, "Invalid content: {}", msg),
        }
    }
}

impl std::error::Error for ProcessError {}

impl From<io::Error> for ProcessError {
    fn from(err: io::Error) -> Self {
        ProcessError::Io(err)
    }
}

fn validate_config_files(dir: &str) -> Result<(), ProcessError> {
    visit_files_contents(
        dir,
        &["toml"],
        &mut |entry, content| {
            // Validate TOML content
            if content.is_empty() {
                return Err(ProcessError::InvalidContent(
                    format!("Empty config file: {}", entry.path().display())
                ));
            }
            // Additional validation...
            Ok(())
        }
    )
}

Building File Trees

use bomboni_fs::visit_files;
use std::collections::BTreeMap;
use std::path::PathBuf;

fn build_file_tree(dir: &str) -> std::io::Result<BTreeMap<String, Vec<PathBuf>>> {
    let mut tree = BTreeMap::new();
    
    visit_files(
        dir,
        &["rs", "toml", "md", "txt"],
        &mut |entry| {
            let path = entry.path();
            if let Some(ext) = path.extension() {
                let ext_str = ext.to_string_lossy().to_string();
                tree.entry(ext_str)
                    .or_insert_with(Vec::new)
                    .push(path);
            }
            Ok(())
        }
    )?;
    
    Ok(tree)
}

let tree = build_file_tree("./")?;
for (ext, files) in tree {
    println!("\n.{} files:", ext);
    for file in files {
        println!("  - {}", file.display());
    }
}

Implementation Notes

  • Both functions use recursive directory traversal
  • Symbolic links are followed during traversal
  • File extensions are matched case-sensitively
  • The extension check excludes the leading dot (use "rs", not ".rs")
  • visit_files_contents uses visit_files internally and adds file reading
  • Files are opened with read-only permissions
  • The entire file content is read into memory as a UTF-8 string

Best Practices

  1. Extension Matching: Provide extensions without the dot prefix:
    // Correct
    visit_files("./", &["rs", "toml"], &mut callback)?;
    
    // Incorrect
    visit_files("./", &[".rs", ".toml"], &mut callback)?;
    
  2. Error Handling: Implement proper error types with From<io::Error>:
    #[derive(Debug)]
    enum MyError {
        Io(io::Error),
        // other variants...
    }
    
    impl From<io::Error> for MyError {
        fn from(err: io::Error) -> Self {
            MyError::Io(err)
        }
    }
    
  3. Memory Considerations: Be cautious when using visit_files_contents with large files as it reads entire files into memory
  4. Performance: For better performance with many files, consider using visit_files and reading files selectively

Build docs developers (and LLMs) love