Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Directory Traversal

Construct and inspect a path

std-badge cat-filesystem-badge

Most filesystem code is path arithmetic before it touches the disk. PathBuf owns the buffer; Path is the borrowed view. The inspection methods — file_name, file_stem, extension, parent — return Option because a path might not have the component you ask for.

PathBuf::push grows the buffer one component at a time. with_extension and with_file_name derive sibling or backup paths in one call without mutating the original. Conversion to &str goes through Path::to_str, which returns None on non-UTF-8 paths.

use std::ffi::OsStr;
use std::path::{Path, PathBuf};

fn main() {
    let mut path = PathBuf::from("/etc/nginx");
    path.push("sites-available");
    path.push("default.conf");

    assert_eq!(path.parent(), Some(Path::new("/etc/nginx/sites-available")));
    assert_eq!(path.file_name(), Some(OsStr::new("default.conf")));
    assert_eq!(path.file_stem(), Some(OsStr::new("default")));
    assert_eq!(path.extension(), Some(OsStr::new("conf")));

    // Derive a backup name and a sibling path without mutating `path`.
    let backup = path.with_extension("conf.bak");
    assert_eq!(backup.file_name(), Some(OsStr::new("default.conf.bak")));

    let sibling = path.with_file_name("nginx.conf");
    assert_eq!(sibling, Path::new("/etc/nginx/sites-available/nginx.conf"));

    // Borrow a `Path` as `&str` for APIs that need one.
    let s: &str = path.to_str().expect("path is valid UTF-8");
    println!("{s}");
}

Create a nested directory tree

std-badge tempfile-badge cat-filesystem-badge

fs::create_dir_all is mkdir -p: it walks the requested path and creates every missing intermediate directory. It’s idempotent — calling it on a path that already exists returns Ok(()), not an error — so it’s the right call when seeding cache directories or log roots on startup.

fs::create_dir only creates the final component. If any parent on the way is missing it fails with io::ErrorKind::NotFound. Reach for create_dir when the parent is known to exist and you want the operation to fail loudly if your assumption is wrong.

use std::fs;
use std::io;
use tempfile::tempdir;

fn main() -> io::Result<()> {
    let root = tempdir()?;
    let nested = root.path().join("cache").join("2026").join("05");

    // Creates every missing component along the way.
    fs::create_dir_all(&nested)?;
    assert!(nested.is_dir());

    // Calling it again on an existing directory is not an error.
    fs::create_dir_all(&nested)?;

    // `create_dir` only creates the leaf, and only when the parent exists.
    let leaf = nested.join("26");
    fs::create_dir(&leaf)?;
    assert!(leaf.is_dir());

    Ok(())
}

File names that have been modified in the last 24 hours

walkdir-badge cat-filesystem-badge

Gets the current working directory and returns file names modified within the last 24 hours. env::current_dir gets the current working directory, WalkDir::new creates a new WalkDir for the current directory. WalkDir::into_iter creates an iterator, Iterator::filter_map applies Result::ok to WalkDir::DirEntry and filters out the directories.

std::fs::Metadata::modified returns the SystemTime::elapsed time since the last modification. Duration::as_secs converts the time to seconds and compared with 24 hours (24 * 60 * 60 seconds). Iterator::for_each prints the file names.

use walkdir::WalkDir;
use anyhow::Result;
use std::env;

fn main() -> Result<()> {
    let current_dir = env::current_dir()?;
    println!("Entries modified in the last 24 hours in {:?}:", current_dir);

    for entry in WalkDir::new(current_dir)
            .into_iter()
            .filter_map(|e| e.ok())
            .filter(|e| e.metadata().unwrap().is_file()) {
        let path = entry.path();
        let metadata = entry.metadata()?;
        let modified = metadata.modified()?.elapsed()?.as_secs();
        if modified < 24 * 3600 {
            println!("{}", path.display());
        }
    }

    Ok(())
}

Find loops for a given path

same_file-badge walkdir-badge cat-filesystem-badge

Use same_file::is_same_file to detect loops for a given path. For example, a loop is created on a Unix system via symlinks:

mkdir -p /tmp/foo/bar/baz
ln -s /tmp/foo/  /tmp/foo/bar/baz/qux

The following would assert that a loop exists.

use walkdir::WalkDir;
use same_file::is_same_file;

fn main() {
    let mut loop_found = false;
    for entry in WalkDir::new(".")
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok()) {
        let ancestor = entry.path()
            .ancestors()
            .skip(1)
            .find(|ancestor| is_same_file(ancestor, entry.path()).is_ok());

        if ancestor.is_some() {
            loop_found = true;
        }
    }
    // Note: This test would only pass if there are actual symlink loops
    // println!("Loop found: {}", loop_found);
}

Recursively find duplicate file names

walkdir-badge cat-filesystem-badge

Find recursively in the current directory duplicate filenames, printing them only once.

use walkdir::WalkDir;
use std::collections::HashMap;

fn main() {
    let mut filenames = HashMap::new();

    for entry in WalkDir::new(".")
                         .into_iter()
                         .filter_map(Result::ok)
                         .filter(|e| e.file_type().is_file()) {

        let f_name = String::from(entry.file_name().to_string_lossy());
        let counter = filenames.entry(f_name.clone()).or_insert(0);
        *counter += 1;

        if *counter == 2 {
            println!("{}", f_name);
        }
    }
}

Recursively find all files with given predicate

walkdir-badge cat-filesystem-badge

Find JSON files modified within the last day in the current directory. Using follow_links ensures symbolic links are followed like they were normal directories and files.

use walkdir::WalkDir;
use anyhow::Result;

fn main() -> Result<()> {
    for entry in WalkDir::new(".")
        .follow_links(true)
        .into_iter()
        .filter_map(|e| e.ok()) {
        let f_name = entry.file_name().to_string_lossy();
        let sec = entry.metadata()?.modified()?;

        if f_name.ends_with(".json") && sec.elapsed()?.as_secs() < 86400 {
            println!("{}", entry.path().display());
        }
    }
    Ok(())
}

Traverse directories while skipping dotfiles

walkdir-badge cat-filesystem-badge

Uses filter_entry to descend recursively into entries passing the is_not_hidden predicate thus skipping hidden files and directories. Iterator::filter_map applies is_not_hidden on each WalkDir::DirEntry even if the parent is a hidden directory.

Root dir "." yields through WalkDir::depth usage in is_not_hidden predicate.

use walkdir::{DirEntry, WalkDir};

fn is_not_hidden(entry: &DirEntry) -> bool {
    entry
         .file_name()
         .to_str()
         .map(|s| entry.depth() == 0 || !s.starts_with("."))
         .unwrap_or(false)
}

fn main() {
    WalkDir::new(".")
        .into_iter()
        .filter_entry(|e| is_not_hidden(e))
        .filter_map(|v| v.ok())
        .for_each(|x| println!("{}", x.path().display()));
}

Recursively calculate file sizes at given depth

walkdir-badge cat-filesystem-badge

Recursion depth can be flexibly set by WalkDir::max_depth. Calculates sum of all file sizes to 3 subdir levels, ignoring files in the root directory.

use walkdir::WalkDir;

fn main() {
    let total_size = WalkDir::new(".")
        .max_depth(3)
        .into_iter()
        .filter_map(|entry| entry.ok())
        .filter_map(|entry| entry.metadata().ok())
        .filter(|metadata| metadata.is_file())
        .fold(0, |acc, m| acc + m.len());

    println!("Total size: {} bytes.", total_size);
}

Find all png files recursively

glob-badge cat-filesystem-badge

Recursively find all PNG files in the current directory. In this case, the ** pattern matches the current directory and all subdirectories.

Use the ** pattern in any path portion. For example, /media/**/*.png matches all PNGs in media and it’s subdirectories.

use glob::glob;
use anyhow::Result;

fn main() -> Result<()> {
    for entry in glob("**/*.png")? {
        println!("{}", entry?.display());
    }
    Ok(())
}

Find all files with given pattern ignoring filename case

walkdir-badge glob-badge cat-filesystem-badge

Find all image files in the /media/ directory matching the img_[0-9]*.png pattern.

A custom MatchOptions struct is passed to glob_with instead of glob to make the glob pattern case insensitive while keeping the other options Default.

use walkdir::WalkDir;
use anyhow::Result;
use glob::{glob_with, MatchOptions};

fn main() -> Result<()> {
    let options = MatchOptions {
        case_sensitive: false,
        ..Default::default()
    };

    for entry in glob_with("/media/img_[0-9]*.png", options)? {
        println!("{}", entry?.display());
    }

    Ok(())
}