Skip to main content
File system utilities for working with files and directories. This crate provides efficient utilities for recursive file traversal with extension filtering and content reading.

Features

  • Recursive File Visiting: Traverse directory trees with extension filtering
  • Content Reading: Read file contents during traversal
  • Extension Filtering: Filter files by multiple extensions
  • Error Handling: Comprehensive error propagation
  • Callback-Based API: Process files with custom logic

Installation

Add bomboni_fs to your Cargo.toml:
[dependencies]
bomboni_fs = "*"

File Visiting

Recursively visit files in a directory with extension filtering:
use bomboni_fs::visit_files;
use std::error::Error;
use std::fs::DirEntry;
use std::path::PathBuf;

// Find all Rust files in a directory
let mut rust_files = Vec::new();
visit_files("src", &["rs"], &mut |entry: &DirEntry| {
    rust_files.push(entry.path().to_path_buf());
    Ok(())
})?;

println!("Found {} Rust files", rust_files.len());
for file in rust_files {
    println!("  - {}", file.display());
}

Multiple Extensions

Filter files by multiple extensions:
use bomboni_fs::visit_files;
use std::fs::DirEntry;

// Find all configuration files
let mut config_files = Vec::new();
visit_files(".", &["toml", "yaml", "yml", "json"], &mut |entry: &DirEntry| {
    config_files.push(entry.file_name().to_string_lossy().to_string());
    Ok(())
})?;

println!("Configuration files: {:?}", config_files);

Processing Files

Process files as they’re discovered:
use bomboni_fs::visit_files;
use std::fs::DirEntry;

// Count total lines in all Rust files
let mut total_lines = 0;
visit_files("src", &["rs"], &mut |entry: &DirEntry| {
    let content = std::fs::read_to_string(entry.path())?;
    total_lines += content.lines().count();
    Ok(())
})?;

println!("Total lines of Rust code: {}", total_lines);

Reading File Contents

Visit files and automatically read their contents:
use bomboni_fs::visit_files_contents;
use std::error::Error;
use std::fs::DirEntry;
use std::path::PathBuf;

// Read all Markdown files and their contents
let mut documents = Vec::new();
visit_files_contents("docs", &["md"], &mut |entry: &DirEntry, content: String| {
    documents.push((
        entry.path().to_path_buf(),
        content.lines().count()
    ));
    Ok(())
})?;

for (path, line_count) in documents {
    println!("{}: {} lines", path.display(), line_count);
}

Content Analysis

Analyze file contents during traversal:
use bomboni_fs::visit_files_contents;
use std::fs::DirEntry;

// Find all TODO comments in source files
let mut todos = Vec::new();
visit_files_contents("src", &["rs"], &mut |entry: &DirEntry, content: String| {
    for (line_num, line) in content.lines().enumerate() {
        if line.contains("TODO") {
            todos.push((
                entry.path().to_path_buf(),
                line_num + 1,
                line.trim().to_string(),
            ));
        }
    }
    Ok(())
})?;

println!("Found {} TODO comments:", todos.len());
for (file, line, text) in todos {
    println!("  {}:{} - {}", file.display(), line, text);
}

Content Modification

Read, process, and modify file contents:
use bomboni_fs::visit_files_contents;
use std::fs::{self, DirEntry};

// Remove trailing whitespace from all text files
let mut modified = 0;
visit_files_contents("docs", &["txt", "md"], &mut |entry: &DirEntry, content: String| {
    let cleaned: String = content
        .lines()
        .map(|line| line.trim_end())
        .collect::<Vec<_>>()
        .join("\n");
    
    if cleaned != content {
        fs::write(entry.path(), cleaned)?;
        modified += 1;
    }
    Ok(())
})?;

println!("Modified {} files", modified);

Error Handling

Both functions support custom error types that implement Error and From<io::Error>:
use bomboni_fs::visit_files;
use std::fs::DirEntry;
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("IO error: {0}")]
    Io(#[from] io::Error),
    
    #[error("Invalid file: {0}")]
    InvalidFile(String),
}

fn process_files() -> Result<(), MyError> {
    let mut valid_files = Vec::new();
    
    visit_files("src", &["rs"], &mut |entry: &DirEntry| {
        let filename = entry.file_name().to_string_lossy().to_string();
        
        // Custom validation
        if filename.starts_with(".") {
            return Err(MyError::InvalidFile(filename));
        }
        
        valid_files.push(filename);
        Ok(())
    })?;
    
    Ok(())
}

Practical Examples

File Statistics

Gather statistics about your codebase:
use bomboni_fs::visit_files_contents;
use std::collections::HashMap;
use std::fs::DirEntry;

#[derive(Default)]
struct CodeStats {
    files: usize,
    total_lines: usize,
    code_lines: usize,
    comment_lines: usize,
}

let mut stats = CodeStats::default();
let mut stats_by_ext = HashMap::new();

visit_files_contents("src", &["rs"], &mut |entry: &DirEntry, content: String| {
    let ext = entry.path()
        .extension()
        .and_then(|s| s.to_str())
        .unwrap_or("unknown")
        .to_string();
    
    let ext_stats = stats_by_ext.entry(ext).or_insert(CodeStats::default());
    ext_stats.files += 1;
    
    for line in content.lines() {
        let trimmed = line.trim();
        ext_stats.total_lines += 1;
        
        if trimmed.starts_with("//") || trimmed.starts_with("/*") {
            ext_stats.comment_lines += 1;
        } else if !trimmed.is_empty() {
            ext_stats.code_lines += 1;
        }
    }
    
    Ok::<(), std::io::Error>(())
})?;

for (ext, stats) in stats_by_ext {
    println!("{}:", ext);
    println!("  Files: {}", stats.files);
    println!("  Total lines: {}", stats.total_lines);
    println!("  Code lines: {}", stats.code_lines);
    println!("  Comments: {}", stats.comment_lines);
}

Finding Files

Search for files matching specific criteria:
use bomboni_fs::visit_files_contents;
use std::fs::DirEntry;
use std::path::PathBuf;

fn find_files_containing(dir: &str, pattern: &str) -> std::io::Result<Vec<PathBuf>> {
    let mut matches = Vec::new();
    
    visit_files_contents(dir, &["rs", "toml", "md"], &mut |entry: &DirEntry, content: String| {
        if content.contains(pattern) {
            matches.push(entry.path().to_path_buf());
        }
        Ok(())
    })?;
    
    Ok(matches)
}

let files = find_files_containing("src", "TODO");
println!("Files containing 'TODO': {:?}", files);

Build Script Integration

Use in build scripts to process files:
// build.rs
use bomboni_fs::visit_files;
use std::fs::DirEntry;
use std::path::PathBuf;

fn main() {
    // Tell Cargo to rerun if any .proto file changes
    let mut proto_files = Vec::new();
    visit_files("proto", &["proto"], &mut |entry: &DirEntry| {
        let path = entry.path();
        println!("cargo:rerun-if-changed={}", path.display());
        proto_files.push(path.to_path_buf());
        Ok::<(), std::io::Error>(())
    })
    .expect("Failed to visit proto files");
    
    // Compile protobuf files
    // protobuf_codegen::compile_protos(&proto_files, &["proto"], "src/proto");
}

Testing Utilities

Create test helpers for file operations:
use bomboni_fs::visit_files;
use std::fs::{self, DirEntry};
use std::path::Path;

fn collect_test_files(dir: &Path) -> std::io::Result<Vec<String>> {
    let mut files = Vec::new();
    
    visit_files(dir, &["rs"], &mut |entry: &DirEntry| {
        let path = entry.path();
        if let Some(name) = path.file_stem() {
            files.push(name.to_string_lossy().to_string());
        }
        Ok(())
    })?;
    
    files.sort();
    Ok(files)
}

#[test]
fn test_all_source_files_have_tests() {
    let src_files = collect_test_files(Path::new("src")).unwrap();
    let test_files = collect_test_files(Path::new("tests")).unwrap();
    
    for src_file in src_files {
        assert!(
            test_files.contains(&format!("{}_test", src_file)),
            "Missing test file for {}",
            src_file
        );
    }
}

Performance Considerations

Buffered Processing

For large directories, consider processing files in batches:
use bomboni_fs::visit_files;
use std::fs::DirEntry;

const BATCH_SIZE: usize = 100;
let mut batch = Vec::with_capacity(BATCH_SIZE);

visit_files("large_dir", &["txt"], &mut |entry: &DirEntry| {
    batch.push(entry.path().to_path_buf());
    
    if batch.len() >= BATCH_SIZE {
        process_batch(&batch)?;
        batch.clear();
    }
    
    Ok::<(), std::io::Error>(())
})?;

// Process remaining files
if !batch.is_empty() {
    process_batch(&batch)?;
}

Memory Efficiency

When using visit_files_contents, the content is passed directly to your callback, avoiding unnecessary allocations:
use bomboni_fs::visit_files_contents;
use std::fs::DirEntry;

// Process content without storing it
let mut line_count = 0;
visit_files_contents("src", &["rs"], &mut |_entry: &DirEntry, content: String| {
    line_count += content.lines().count();
    // Content is dropped after callback returns
    Ok::<(), std::io::Error>(())
})?;

API Reference

visit_files
function
Recursively visits files in a directory with specified extensions.Parameters:
  • dir: impl AsRef<Path> - The directory to search
  • extensions: &[&str] - File extensions to match (without dots)
  • cb: &mut dyn FnMut(&DirEntry) -> Result<(), E> - Callback for each matching file
Returns: Result<(), E> where E: Error + From<io::Error>Errors: Returns error if directory reading fails or callback returns error
visit_files_contents
function
Recursively visits files and reads their contents.Parameters:
  • dir: impl AsRef<Path> - The directory to search
  • extensions: &[&str] - File extensions to match (without dots)
  • cb: &mut dyn FnMut(&DirEntry, String) -> Result<(), E> - Callback with file entry and contents
Returns: Result<(), E> where E: Error + From<io::Error>Errors: Returns error if directory reading, file reading, or callback fails

bomboni_common

Common utilities and types

bomboni_core

Core utilities and abstractions

Build docs developers (and LLMs) love