What are the trade-offs between glob::glob_with and glob for pattern matching options?

glob::glob_with accepts a MatchOptions struct that controls pattern matching behavior, while glob uses default matching options that prioritize simplicity but may not suit all use cases. The key difference is configurability: glob_with enables case-insensitive matching on Unix systems, control over symlink following, and toggling backslash escaping—behaviors that glob hardcodes to specific defaults. The trade-off is between the simpler API of glob (single function call with sensible defaults) and the flexibility of glob_with (explicit configuration for cross-platform consistency or specialized matching requirements). Use glob for quick scripts with standard patterns; use glob_with when you need predictable cross-platform behavior or specific matching semantics.

Basic glob vs glob_with

use glob::{glob, glob_with, MatchOptions};
 
fn main() {
    // glob: simple function with default options
    for entry in glob("src/**/*.rs").unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(e) => println!("Error: {:?}", e),
        }
    }
    
    // glob_with: same pattern with explicit options
    let options = MatchOptions::new();
    for entry in glob_with("src/**/*.rs", options).unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(e) => println!("Error: {:?}", e),
        }
    }
}

glob uses default MatchOptions; glob_with accepts explicit configuration.

MatchOptions Configuration

use glob::{glob_with, MatchOptions};
 
fn main() {
    let options = MatchOptions {
        case_sensitive: true,        // Match case exactly
        require_literal_separator: false,  // * and ? can match /
        require_literal_leading_dot: false, // * and ? can match leading dot
    };
    
    // Each field controls a specific matching behavior
    for entry in glob_with("src/**/*.rs", options).unwrap() {
        match entry {
            Ok(path) => println!("Path: {:?}", path),
            Err(e) => println!("Error: {:?}", e),
        }
    }
}

MatchOptions has three boolean fields that control pattern matching behavior.

Case Sensitivity Control

use glob::{glob, glob_with, MatchOptions};
 
fn main() {
    // Default behavior differs by platform:
    // - Unix: case-sensitive (file.txt != FILE.TXT)
    // - Windows: case-insensitive (file.txt == FILE.TXT)
    
    // On Unix, this won't match "FILE.TXT":
    for entry in glob("*.txt").unwrap() {
        if let Ok(path) = entry {
            println!("glob found: {:?}", path);
        }
    }
    
    // Force case-insensitive matching on all platforms
    let case_insensitive = MatchOptions {
        case_sensitive: false,
        require_literal_separator: false,
        require_literal_leading_dot: false,
    };
    
    for entry in glob_with("*.txt", case_insensitive).unwrap() {
        if let Ok(path) = entry {
            println!("glob_with (insensitive) found: {:?}", path);
        }
    }
    
    // Force case-sensitive matching on all platforms
    let case_sensitive = MatchOptions {
        case_sensitive: true,
        require_literal_separator: false,
        require_literal_leading_dot: false,
    };
    
    for entry in glob_with("*.txt", case_sensitive).unwrap() {
        if let Ok(path) = entry {
            println!("glob_with (sensitive) found: {:?}", path);
        }
    }
}

case_sensitive: false provides consistent cross-platform behavior.

Literal Separator Requirement

use glob::{glob_with, MatchOptions};
 
fn main() {
    // require_literal_separator controls whether * and ? can match /
    
    // Default: false, * can match /
    // Pattern "src/*.rs" matches "src/foo.rs" AND "src/subdir/foo.rs"
    let wildcard_path = MatchOptions {
        case_sensitive: true,
        require_literal_separator: false,
        require_literal_leading_dot: false,
    };
    
    // require_literal_separator: true
    // Pattern "src/*.rs" matches "src/foo.rs" but NOT "src/subdir/foo.rs"
    let literal_sep = MatchOptions {
        case_sensitive: true,
        require_literal_separator: true,
        require_literal_leading_dot: false,
    };
    
    // Use require_literal_separator: true when you want
    // * to NOT cross directory boundaries
    for entry in glob_with("test_data/*.txt", literal_sep).unwrap() {
        if let Ok(path) = entry {
            println!("Literal sep: {:?}", path);
        }
    }
}

require_literal_separator: true prevents wildcards from matching path separators.

Literal Leading Dot Requirement

use glob::{glob_with, MatchOptions};
 
fn main() {
    // require_literal_leading_dot controls whether * and ? can match leading dots
    
    // Default: false, * can match leading dot
    // Pattern "*.txt" matches "file.txt" AND ".hidden.txt"
    let wildcard_dot = MatchOptions {
        case_sensitive: true,
        require_literal_separator: false,
        require_literal_leading_dot: false,
    };
    
    // require_literal_leading_dot: true
    // Pattern "*.txt" matches "file.txt" but NOT ".hidden.txt"
    // To match hidden files, pattern must start with literal dot: ".*.txt"
    let literal_dot = MatchOptions {
        case_sensitive: true,
        require_literal_separator: false,
        require_literal_leading_dot: true,
    };
    
    // This mimics shell behavior where * doesn't match leading dot
    // Shell: ls *.txt won't show .hidden.txt
    for entry in glob_with("test_data/*.txt", literal_dot).unwrap() {
        if let Ok(path) = entry {
            println!("Literal dot: {:?}", path);
        }
    }
}

require_literal_leading_dot: true mimics shell behavior for hidden files.

Default MatchOptions Values

use glob::MatchOptions;
 
fn main() {
    // MatchOptions::new() provides defaults that match glob()'s behavior
    let default_options = MatchOptions::new();
    
    // These are the defaults:
    // case_sensitive: true on Unix, false on Windows
    // require_literal_separator: false
    // require_literal_leading_dot: false
    
    // Platform-specific case sensitivity:
    // - Unix: case_sensitive defaults to true
    // - Windows: case_sensitive defaults to false
    
    println!("case_sensitive: {}", default_options.case_sensitive);
    println!("require_literal_separator: {}", default_options.require_literal_separator);
    println!("require_literal_leading_dot: {}", default_options.require_literal_leading_dot);
}

MatchOptions::new() provides platform-appropriate defaults matching glob() behavior.

Cross-Platform Consistency

use glob::{glob_with, MatchOptions};
 
fn find_files_cross_platform(pattern: &str) -> Vec<std::path::PathBuf> {
    // Force consistent behavior across all platforms
    let consistent_options = MatchOptions {
        case_sensitive: false,        // Case-insensitive everywhere
        require_literal_separator: true, // * cannot match /
        require_literal_leading_dot: true, // * cannot match leading dot
    };
    
    let mut results = Vec::new();
    for entry in glob_with(pattern, consistent_options).unwrap() {
        if let Ok(path) = entry {
            results.push(path);
        }
    }
    results
}
 
fn main() {
    // Same behavior on Linux, macOS, and Windows
    let files = find_files_cross_platform("src/**/*.rs");
    for file in &files {
        println!("Found: {:?}", file);
    }
}

glob_with with explicit options ensures consistent behavior across platforms.

Shell-like Behavior

use glob::{glob_with, MatchOptions};
 
fn glob_shell_like(pattern: &str) -> Vec<std::path::PathBuf> {
    // Match typical shell behavior:
    // - Case-sensitive on Unix
    // - * and ? don't match leading dot (hidden files)
    // - * and ? can match path separators (unless using **)
    
    let shell_options = MatchOptions {
        case_sensitive: cfg!(not(windows)), // Unix: true, Windows: false
        require_literal_separator: false,
        require_literal_leading_dot: true,   // Shell behavior for hidden files
    };
    
    let mut results = Vec::new();
    for entry in glob_with(pattern, shell_options).unwrap() {
        if let Ok(path) = entry {
            results.push(path);
        }
    }
    results
}
 
fn main() {
    let files = glob_shell_like("*.txt");
    // Won't match .hidden.txt without explicit .* pattern
    for file in &files {
        println!("Shell-like found: {:?}", file);
    }
}

Configure MatchOptions to match specific shell behaviors.

Hidden Files Handling

use glob::{glob, glob_with, MatchOptions};
 
fn main() {
    // Create test files for demonstration
    // Assume we have: file.txt, .hidden.txt, another.txt
    
    // Default glob with *.txt - matches hidden files depending on options
    let include_hidden = MatchOptions {
        case_sensitive: true,
        require_literal_separator: false,
        require_literal_leading_dot: false, // * can match leading dot
    };
    
    // This matches both "file.txt" and ".hidden.txt"
    println!("Including hidden files:");
    for entry in glob_with("*.txt", include_hidden).unwrap() {
        if let Ok(path) = entry {
            println!("  {:?}", path);
        }
    }
    
    // Shell-like: exclude hidden files unless explicitly matched
    let exclude_hidden = MatchOptions {
        case_sensitive: true,
        require_literal_separator: false,
        require_literal_leading_dot: true, // * cannot match leading dot
    };
    
    // This matches "file.txt" but NOT ".hidden.txt"
    println!("\nExcluding hidden files:");
    for entry in glob_with("*.txt", exclude_hidden).unwrap() {
        if let Ok(path) = entry {
            println!("  {:?}", path);
        }
    }
    
    // To match hidden files with require_literal_leading_dot: true
    // Use pattern ".*.txt" or ".*"
    println!("\nHidden files explicitly:");
    for entry in glob_with(".*", exclude_hidden).unwrap() {
        if let Ok(path) = entry {
            println!("  {:?}", path);
        }
    }
}

Control whether wildcards match leading dots for hidden file handling.

Directory Depth Control

use glob::{glob_with, MatchOptions};
 
fn main() {
    // Control how deep wildcards traverse
    
    // * in pattern: matches single directory level
    // ** in pattern: matches any depth
    
    // With require_literal_separator:
    // - false: * can match / (acts like **)
    // - true: * cannot match / (must use ** for recursive)
    
    let single_level = MatchOptions {
        case_sensitive: true,
        require_literal_separator: true,  // * stops at /
        require_literal_leading_dot: false,
    };
    
    // Pattern "src/*.rs" matches "src/main.rs" but NOT "src/bin/cli.rs"
    println!("Single level:");
    for entry in glob_with("src/*.rs", single_level).unwrap() {
        if let Ok(path) = entry {
            println!("  {:?}", path);
        }
    }
    
    // Pattern "src/**/*.rs" matches both "src/main.rs" AND "src/bin/cli.rs"
    let recursive = MatchOptions {
        case_sensitive: true,
        require_literal_separator: true,
        require_literal_leading_dot: false,
    };
    
    println!("\nRecursive:");
    for entry in glob_with("src/**/*.rs", recursive).unwrap() {
        if let Ok(path) = entry {
            println!("  {:?}", path);
        }
    }
}

require_literal_separator affects how * interacts with path separators.

Symlink Handling

use glob::{glob, glob_with, MatchOptions};
 
fn main() {
    // Both glob() and glob_with() follow symlinks by default
    // when traversing directories (but not when matching the final component)
    
    // This is controlled internally, not via MatchOptions
    // If you need symlink control, consider walkdir crate
    
    // Example: pattern "src/**/*.rs" will traverse into symlinked directories
    
    let options = MatchOptions::new();
    
    for entry in glob_with("src/**/*.rs", options).unwrap() {
        if let Ok(path) = entry {
            println!("Found: {:?}", path);
            // If path is through a symlink, it's included
        }
    }
}

Symlink following is not controlled by MatchOptions; both functions follow symlinks.

Error Handling Differences

use glob::{glob, glob_with, MatchOptions};
 
fn main() {
    // Both functions return GlobError for path access issues
    // Both return Result iterator that yields Result<PathBuf, GlobError>
    
    // Invalid pattern returns Result::Err immediately
    match glob("**[invalid") {
        Ok(_) => println!("Pattern valid"),
        Err(e) => println!("Invalid pattern: {}", e),
    }
    
    // Path access errors during iteration yield Err
    for entry in glob("/root/**/*").unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(e) => println!("Access error: {:?}", e),
        }
    }
    
    // glob_with has identical error handling
    let options = MatchOptions::new();
    for entry in glob_with("/root/**/*", options).unwrap() {
        match entry {
            Ok(path) => println!("Found: {:?}", path),
            Err(e) => println!("Access error: {:?}", e),
        }
    }
}

Error handling is identical; only matching behavior differs.

Performance Considerations

use glob::{glob, glob_with, MatchOptions};
use std::time::Instant;
 
fn main() {
    let pattern = "src/**/*.rs";
    
    // Both functions have similar performance characteristics
    // The difference is in matching semantics, not speed
    
    // Default glob
    let start = Instant::now();
    let count = glob(pattern).unwrap().count();
    println!("glob: {} files in {:?}", count, start.elapsed());
    
    // glob_with with same options
    let start = Instant::now();
    let options = MatchOptions::new();
    let count = glob_with(pattern, options).unwrap().count();
    println!("glob_with: {} files in {:?}", count, start.elapsed());
    
    // Performance differences come from matching complexity:
    // - case_sensitive: false may have slight overhead
    // - require_literal_separator: true may be faster (stops at /)
    // - require_literal_leading_dot: true may be faster (skips hidden)
}

Performance differences are negligible; choice should be based on semantics.

Complete Options Reference

use glob::MatchOptions;
 
fn main() {
    // All MatchOptions fields:
    
    let options = MatchOptions {
        // case_sensitive: Whether patterns match case-sensitively
        // - true: "file.txt" only matches "file.txt", not "FILE.TXT"
        // - false: "file.txt" matches "file.txt", "FILE.TXT", "File.Txt"
        case_sensitive: true,
        
        // require_literal_separator: Whether * and ? must match / literally
        // - true: "src/*.rs" matches "src/main.rs" but not "src/bin/cli.rs"
        // - false: "src/*.rs" matches both "src/main.rs" and "src/bin/cli.rs"
        require_literal_separator: false,
        
        // require_literal_leading_dot: Whether * and ? must match leading . literally
        // - true: "*.txt" matches "file.txt" but not ".hidden.txt"
        // - false: "*.txt" matches both "file.txt" and ".hidden.txt"
        require_literal_leading_dot: false,
    };
    
    // Builder-style construction is not available, use struct literal
}

Three boolean fields control all matching behavior variations.

Common Use Case Patterns

use glob::{glob_with, MatchOptions};
 
fn main() {
    // Use case 1: Cross-platform case-insensitive search
    let case_insensitive = MatchOptions {
        case_sensitive: false,
        ..MatchOptions::new()
    };
    
    // Use case 2: Shell-like behavior (hidden files excluded)
    let shell_like = MatchOptions {
        require_literal_leading_dot: true,
        ..MatchOptions::new()
    };
    
    // Use case 3: Non-recursive single directory
    let single_dir = MatchOptions {
        require_literal_separator: true,
        ..MatchOptions::new()
    };
    
    // Use case 4: Windows-style matching on all platforms
    let windows_style = MatchOptions {
        case_sensitive: false,
        require_literal_separator: false,
        require_literal_leading_dot: false,
    };
    
    // Use case 5: Strict Unix-style matching
    let unix_strict = MatchOptions {
        case_sensitive: true,
        require_literal_separator: true,
        require_literal_leading_dot: true,
    };
}

Common patterns emerge for specific matching requirements.

Real-World Example: Configuration File Finder

use glob::{glob_with, MatchOptions};
use std::path::PathBuf;
 
fn find_config_files(base_dir: &str) -> Vec<PathBuf> {
    // Find config files: case-insensitive, exclude hidden
    let options = MatchOptions {
        case_sensitive: false,         // Match "Config" and "config"
        require_literal_separator: true, // Don't recurse into subdirs with *
        require_literal_leading_dot: true, // Ignore hidden files
    };
    
    let mut configs = Vec::new();
    
    // Common config file patterns
    let patterns = [
        "*.toml",      // Cargo.toml, pyproject.toml
        "*.yaml",      // CI configs
        "*.yml",       // YAML configs
        "*.json",      // JSON configs
        "*.config",    // Generic config files
    ];
    
    for pattern in &patterns {
        let full_pattern = format!("{}/{}", base_dir, pattern);
        for entry in glob_with(&full_pattern, options).unwrap() {
            if let Ok(path) = entry {
                configs.push(path);
            }
        }
    }
    
    configs
}
 
fn main() {
    let configs = find_config_files(".");
    for config in &configs {
        println!("Config: {:?}", config);
    }
}

Use glob_with for predictable cross-platform configuration file discovery.

Real-World Example: Source Code Search

use glob::{glob_with, MatchOptions};
use std::path::PathBuf;
 
fn find_source_files(project_dir: &str, extensions: &[&str]) -> Vec<PathBuf> {
    // Find source files: case-sensitive, include hidden .dotfiles
    let options = MatchOptions {
        case_sensitive: true,           // Source files are case-sensitive
        require_literal_separator: false, // Allow **/ patterns
        require_literal_leading_dot: false, // Include .dotfiles
    };
    
    let mut sources = Vec::new();
    
    for ext in extensions {
        // Recursive search for all files with extension
        let pattern = format!("{}/**/*.{}", project_dir, ext);
        for entry in glob_with(&pattern, options).unwrap() {
            if let Ok(path) = entry {
                // Skip target/build directories
                if path.to_string_lossy().contains("/target/") ||
                   path.to_string_lossy().contains("/build/") {
                    continue;
                }
                sources.push(path);
            }
        }
    }
    
    sources
}
 
fn main() {
    let rust_files = find_source_files(".", &["rs"]);
    println!("Found {} Rust files", rust_files.len());
    
    let web_files = find_source_files(".", &["js", "ts", "tsx", "jsx"]);
    println!("Found {} web files", web_files.len());
}

Source code search typically needs recursive matching with case sensitivity.

Synthesis

MatchOptions fields:

Field true false
case_sensitive Exact case match Case-insensitive
require_literal_separator * cannot match / * can match /
require_literal_leading_dot * cannot match leading . * can match leading .

glob vs glob_with:

Aspect glob glob_with
API Single function call Function + options struct
Defaults Platform-specific Explicit control
Cross-platform Varies Consistent when options set
Use case Quick scripts Controlled behavior

Common configurations:

Need case_sensitive require_literal_separator require_literal_leading_dot
Shell-like platform default false true
Case-insensitive false false false
Strict Unix true true true
Windows-style false false false

Key insight: glob::glob_with with explicit MatchOptions provides control over three matching behaviors that glob hardcodes to platform defaults: case sensitivity, wildcard matching of path separators, and wildcard matching of leading dots. The default glob function uses case_sensitive: true on Unix and false on Windows, allowing wildcards to match separators and leading dots—behaviors that may not match shell semantics or cross-platform requirements. Use glob when platform-specific defaults are acceptable; use glob_with when you need case-insensitive matching on Unix, want to prevent wildcards from crossing directory boundaries, need to exclude hidden files from * patterns, or require consistent behavior across operating systems. The performance overhead is negligible—the choice should be based on matching semantics rather than speed. For production code that may run on multiple platforms, glob_with with explicit options ensures predictable behavior regardless of the host operating system.