How do I match file paths with glob in Rust?
Walkthrough
The glob crate provides pattern matching for file paths using shell-style wildcard patterns (glob patterns). It allows you to find files matching patterns like *.rs, src/**/*.txt, or data/[0-9]*.csv. Glob patterns are commonly used for file discovery, build systems, and any task that needs to match multiple files by pattern. The crate returns an iterator over matching paths, making it easy to process results.
Key concepts:
- glob() — creates an iterator over paths matching a pattern
- glob_with() — glob with custom options (case sensitivity, etc.)
- Pattern syntax —
*(any sequence),?(single char),[](character class),**(recursive) - Paths — returned as
PathBufviaGlobResult - Errors — both IO errors and pattern parsing errors
Code Example
# Cargo.toml
[dependencies]
glob = "0.3"use glob::glob;
fn main() {
// Find all Rust files in current directory
for entry in glob("*.rs").expect("Failed to read glob pattern") {
match entry {
Ok(path) => println!("Found: {:?}", path.display()),
Err(e) => println!("Error: {}", e),
}
}
}Basic Pattern Matching
use glob::glob;
fn main() {
// Match all files with .txt extension
println!("=== *.txt files ===");
for entry in glob("*.txt").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Match all files starting with 'data'
println!("\n=== data* files ===");
for entry in glob("data*").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Match all files ending with a number
println!("\n=== * with numbers ===");
for entry in glob("*[0-9]*").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}Wildcard Patterns
use glob::glob;
fn main() {
// * matches any sequence of characters (except path separator)
println!("=== Single * pattern ===");
for entry in glob("src/*.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// ? matches exactly one character
println!("\n=== ? pattern (single character) ===");
for entry in glob("test?.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Multiple wildcards
println!("\n=== Multiple wildcards ===");
for entry in glob("src/*/mod.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}Recursive Matching with **
use glob::glob;
fn main() {
// ** matches any number of directories recursively
println!("=== All Rust files recursively ===");
for entry in glob("**/*.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Find all mod.rs files in any subdirectory
println!("\n=== All mod.rs files ===");
for entry in glob("**/mod.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Find files in specific nested structure
println!("\n=== Nested src files ===");
for entry in glob("src/**/*.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}Character Classes
use glob::glob;
fn main() {
// [abc] matches a, b, or c
println!("=== Files starting with a, b, or c ===");
for entry in glob("[abc]*").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// [!abc] matches anything except a, b, or c
println!("\n=== Files NOT starting with a, b, or c ===");
for entry in glob("[!abc]*").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// [0-9] matches digits
println!("\n=== Files starting with digit ===");
for entry in glob("[0-9]*").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// [a-z] matches lowercase letters
println!("\n=== Lowercase starting files ===");
for entry in glob("[a-z]*.rs").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Combined character class
println!("\n=== Image files ===");
for entry in glob("*.[jJ][pP][gG]").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}Glob Options
use glob::{glob_with, MatchOptions};
fn main() {
// Case-insensitive matching
let options = MatchOptions {
case_sensitive: false,
require_literal_separator: false,
require_literal_leading_dot: false,
};
println!("=== Case-insensitive *.RS ===");
for entry in glob_with("*.RS", options).unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Require literal separator (don't allow * to match /)
let literal_sep = MatchOptions {
case_sensitive: true,
require_literal_separator: true,
require_literal_leading_dot: false,
};
println!("\n=== Literal separator ===");
for entry in glob_with("*/*.rs", literal_sep).unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
// Require literal leading dot (don't allow * to match leading .)
let literal_dot = MatchOptions {
case_sensitive: true,
require_literal_separator: false,
require_literal_leading_dot: true,
};
println!("\n=== Literal leading dot (skip hidden) ===");
for entry in glob_with("*", literal_dot).unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}Handling Errors
use glob::glob;
fn main() {
// Invalid pattern
match glob("[invalid") {
Ok(_) => println!("Pattern is valid"),
Err(e) => println!("Invalid pattern: {}", e),
}
// Handle both pattern and IO errors
println!("\n=== Searching with error handling ===");
match glob("src/**/*.rs") {
Ok(paths) => {
for entry in paths {
match entry {
Ok(path) => println!("Found: {}", path.display()),
Err(e) => println!("Error accessing path: {}", e),
}
}
}
Err(e) => println!("Invalid glob pattern: {}", e),
}
}Collecting Results
use glob::glob;
use std::path::PathBuf;
fn main() {
// Collect into a Vec of successful paths
let files: Vec<PathBuf> = glob("*.rs")
.unwrap()
.filter_map(|e| e.ok())
.collect();
println!("Found {} Rust files", files.len());
for file in &files {
println!(" {}", file.display());
}
// Count matching files
let count = glob("src/**/*.rs")
.unwrap()
.filter_map(|e| e.ok())
.count();
println!("\nTotal Rust files in src: {}", count);
// Check if any match exists
let has_config = glob("*.toml")
.unwrap()
.any(|e| e.is_ok());
println!("Has TOML file: {}", has_config);
}Filtering and Processing Results
use glob::glob;
use std::fs;
fn main() {
// Find and process Rust files
for entry in glob("src/**/*.rs").unwrap() {
if let Ok(path) = entry {
// Get file metadata
if let Ok(metadata) = fs::metadata(&path) {
let size = metadata.len();
println!("{} ({} bytes)", path.display(), size);
}
}
}
// Find files by extension
let extensions = ["rs", "toml", "md"];
for ext in extensions {
println!("\n=== .{} files ===", ext);
let pattern = format!("**/*.{}", ext);
for entry in glob(&pattern).unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}
// Filter by file name pattern
println!("\n=== Test files ===");
for entry in glob("**/*.rs").unwrap() {
if let Ok(path) = entry {
if let Some(name) = path.file_name() {
if name.to_string_lossy().starts_with("test") {
println!("{}", path.display());
}
}
}
}
}Working with Directories
use glob::glob;
fn main() {
// Find all directories
println!("=== Directories ===");
for entry in glob("*/").unwrap() {
if let Ok(path) = entry {
println!("Directory: {}", path.display());
}
}
// Find empty directories (directories with no files)
println!("\n=== Empty directories ===");
for entry in glob("*/").unwrap() {
if let Ok(path) = entry {
let has_files = glob(&format!("{}/*", path.display()))
.unwrap()
.any(|e| e.is_ok());
if !has_files {
println!("Empty: {}", path.display());
}
}
}
// Find directories at specific depth
println!("\n=== Directories 2 levels deep ===");
for entry in glob("*/*/").unwrap() {
if let Ok(path) = entry {
println!("{}", path.display());
}
}
}Sorting Results
use glob::glob;
use std::path::PathBuf;
fn main() {
// Collect and sort by path
let mut files: Vec<PathBuf> = glob("**/*.rs")
.unwrap()
.filter_map(|e| e.ok())
.collect();
files.sort();
println!("=== Sorted by path ===");
for file in &files {
println!("{}", file.display());
}
// Sort by file name (not full path)
files.sort_by(|a, b| {
let a_name = a.file_name().unwrap_or_default();
let b_name = b.file_name().unwrap_or_default();
a_name.cmp(b_name)
});
println!("\n=== Sorted by filename ===");
for file in &files {
println!("{}", file.display());
}
// Sort by extension
files.sort_by(|a, b| {
let a_ext = a.extension().unwrap_or_default();
let b_ext = b.extension().unwrap_or_default();
a_ext.cmp(b_ext)
});
println!("\n=== Sorted by extension ===");
for file in &files {
println!("{}", file.display());
}
}Real-World Example: Find Source Files
use glob::glob;
use std::path::Path;
fn find_source_files(root: &Path) -> Vec<std::path::PathBuf> {
let patterns = [
"**/*.rs",
"**/*.c",
"**/*.cpp",
"**/*.h",
"**/*.hpp",
"**/*.py",
"**/*.js",
"**/*.ts",
];
let mut all_files = Vec::new();
for pattern in &patterns {
let full_pattern = root.join(pattern);
let matches: Vec<_> = glob(&full_pattern.to_string_lossy())
.unwrap()
.filter_map(|e| e.ok())
.collect();
all_files.extend(matches);
}
all_files.sort();
all_files.dedup();
all_files
}
fn main() {
let files = find_source_files(Path::new("."));
println!("Found {} source files:", files.len());
for file in files.iter().take(10) {
println!(" {}", file.display());
}
if files.len() > 10 {
println!(" ... and {} more", files.len() - 10);
}
}Real-World Example: Clean Build Artifacts
use glob::glob;
use std::fs;
use std::path::Path;
fn clean_build_artifacts() -> Result<usize, Box<dyn std::error::Error>> {
let patterns = [
"target/",
"**/*.o",
"**/*.obj",
"**/*.exe",
"**/*.dll",
"**/*.so",
"**/*.dylib",
"**/*.class",
"**/__pycache__/",
"**/node_modules/",
];
let mut removed = 0;
for pattern in &patterns {
for entry in glob(pattern)? {
if let Ok(path) = entry {
if path.exists() {
if path.is_dir() {
fs::remove_dir_all(&path)?;
} else {
fs::remove_file(&path)?;
}
println!("Removed: {}", path.display());
removed += 1;
}
}
}
}
Ok(removed)
}
fn main() {
match clean_build_artifacts() {
Ok(count) => println!("\nCleaned {} items", count),
Err(e) => println!("Error: {}", e),
}
}Real-World Example: Configuration File Search
use glob::glob;
use std::path::PathBuf;
#[derive(Debug)]
struct ConfigFiles {
project: Option<PathBuf>,
cargo: Option<PathBuf>,
docker: Option<PathBuf>,
env: Vec<PathBuf>,
}
fn find_config_files() -> ConfigFiles {
ConfigFiles {
project: glob("*.toml")
.unwrap()
.filter_map(|e| e.ok())
.find(|p| p.file_name() == Some(std::ffi::OsStr::new("Cargo.toml"))),
cargo: glob("**/Cargo.toml")
.unwrap()
.filter_map(|e| e.ok())
.next(),
docker: glob("Dockerfile*")
.unwrap()
.filter_map(|e| e.ok())
.next(),
env: glob(".env*")
.unwrap()
.filter_map(|e| e.ok())
.collect(),
}
}
fn main() {
let configs = find_config_files();
println!("Configuration files:");
if let Some(p) = configs.project {
println!(" Project: {}", p.display());
}
if let Some(p) = configs.cargo {
println!(" Cargo: {}", p.display());
}
if let Some(p) = configs.docker {
println!(" Docker: {}", p.display());
}
for env in &configs.env {
println!(" Env: {}", env.display());
}
}Real-World Example: Batch File Renaming
use glob::glob;
use std::fs;
use std::path::Path;
fn batch_rename(
pattern: &str,
new_extension: &str,
dry_run: bool,
) -> Result<usize, Box<dyn std::error::Error>> {
let mut renamed = 0;
for entry in glob(pattern)? {
let path = entry?;
if !path.is_file() {
continue;
}
let new_path = path.with_extension(new_extension);
if dry_run {
println!("Would rename: {} -> {}",
path.display(), new_path.display());
} else {
fs::rename(&path, &new_path)?;
println!("Renamed: {} -> {}",
path.display(), new_path.display());
}
renamed += 1;
}
Ok(renamed)
}
fn main() {
// Dry run first
println!("=== Dry run ===");
match batch_rename("*.txt", "md", true) {
Ok(count) => println!("Would rename {} files", count),
Err(e) => println!("Error: {}", e),
}
// Uncomment to actually rename:
// batch_rename("*.txt", "md", false).unwrap();
}Real-World Example: Directory Size Calculator
use glob::glob;
use std::fs;
use std::collections::HashMap;
fn calculate_extension_sizes(pattern: &str) -> HashMap<String, u64> {
let mut sizes: HashMap<String, u64> = HashMap::new();
for entry in glob(pattern).unwrap() {
if let Ok(path) = entry {
if let Ok(metadata) = fs::metadata(&path) {
if metadata.is_file() {
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("no_ext")
.to_string();
*sizes.entry(ext).or_insert(0) += metadata.len();
}
}
}
}
sizes
}
fn main() {
let sizes = calculate_extension_sizes("**/*");
// Sort by size descending
let mut sorted: Vec<_> = sizes.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1));
println!("File sizes by extension:");
for (ext, size) in sorted {
println!(" .{:>10}: {:>10} bytes", ext, size);
}
}Pattern Compilation
use glob::{Pattern, MatchOptions};
fn main() {
// Compile pattern once for repeated use
let pattern = Pattern::new("src/**/*.rs").unwrap();
// Test if paths match the pattern
let test_paths = [
"src/main.rs",
"src/lib/mod.rs",
"tests/test.rs",
"build.rs",
];
for path in test_paths {
if pattern.matches(path) {
println!("Matches: {}", path);
} else {
println!("No match: {}", path);
}
}
// Pattern with options
let options = MatchOptions {
case_sensitive: false,
require_literal_separator: false,
require_literal_leading_dot: false,
};
let case_insensitive = Pattern::new("*.RS").unwrap();
println!("\nCase-insensitive matching:");
for path in test_paths {
if case_insensitive.matches_with(path, options) {
println!("Matches: {}", path);
}
}
// Match path components
let pattern = Pattern::new("src/*/mod.rs").unwrap();
println!("\nComponent matching:");
println!(" matches: {}", pattern.matches("src/utils/mod.rs"));
println!(" matches: {}", pattern.matches("src/mod.rs"));
}Escaping Special Characters
use glob::glob;
fn main() {
// Escape special glob characters in literal strings
let filename = "report[2024].txt";
// Wrong: [ would be interpreted as character class
// let pattern = format!("*{}*", filename); // Dangerous!
// Correct: escape the filename
let escaped = glob::Pattern::escape(filename);
let pattern = format!("*{}*", escaped);
println!("Escaped pattern: {}", pattern);
// This would match files containing "report[2024].txt" literally
for entry in glob(&pattern).unwrap() {
if let Ok(path) = entry {
println!("Found: {}", path.display());
}
}
}Comparing Paths
use glob::glob;
use std::path::Path;
fn find_duplicates_by_name(dir: &Path) -> Vec<(&str, Vec<std::path::PathBuf>)> {
let mut file_map: std::collections::HashMap<String, Vec<std::path::PathBuf>> =
std::collections::HashMap::new();
for entry in glob(&format!("{}/**/*", dir.display())).unwrap() {
if let Ok(path) = entry {
if path.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
file_map
.entry(name.to_string())
.or_default()
.push(path);
}
}
}
}
file_map
.into_iter()
.filter(|(_, paths)| paths.len() > 1)
.collect()
}
fn main() {
let duplicates = find_duplicates_by_name(Path::new("."));
if duplicates.is_empty() {
println!("No duplicate filenames found");
} else {
println!("Duplicate filenames:");
for (name, paths) in duplicates {
println!("\n{}:", name);
for path in paths {
println!(" {}", path.display());
}
}
}
}Integration with Walk
use glob::glob;
use std::fs;
fn analyze_project() {
println!("=== Project Analysis ===
");
// Count files by type
let rust_files = glob("**/*.rs").unwrap().filter_map(|e| e.ok()).count();
let toml_files = glob("**/*.toml").unwrap().filter_map(|e| e.ok()).count();
let md_files = glob("**/*.md").unwrap().filter_map(|e| e.ok()).count();
println!("Rust files: {}", rust_files);
println!("TOML files: {}", toml_files);
println!("Markdown files: {}", md_files);
// Total size of Rust files
let total_size: u64 = glob("**/*.rs")
.unwrap()
.filter_map(|e| e.ok())
.filter_map(|p| fs::metadata(p).ok())
.map(|m| m.len())
.sum();
println!("Total Rust code size: {} bytes", total_size);
// Find test files
let test_files: Vec<_> = glob("**/*test*.rs")
.unwrap()
.filter_map(|e| e.ok())
.collect();
println!("\nTest files:");
for file in test_files {
println!(" {}", file.display());
}
// Find modules
let modules: Vec<_> = glob("**/mod.rs")
.unwrap()
.filter_map(|e| e.ok())
.collect();
println!("\nModule files:");
for file in modules {
println!(" {}", file.display());
}
}
fn main() {
analyze_project();
}Summary
glob(pattern)returns an iterator over matching paths*matches any sequence of characters (except/)?matches exactly one character[abc]matches any character in the set[!abc]matches any character NOT in the set**matches any sequence of directories recursivelyglob_with(options)allows customizing case sensitivity and more- Results are
GlobResult<PathBuf>, handle both IO and pattern errors Pattern::new()compiles patterns for repeated matchingPattern::escape()escapes glob special characters- Use
.filter_map(|e| e.ok())to collect only successful matches - Perfect for: file discovery, build systems, cleanup scripts, file processing
