How to walk a directory tree
Short answer: std has no tree walker — write a small recursive function over fs::read_dir, and decide whether to descend with entry.file_type(), not path.is_dir(). The second one follows symlinks, and a symlink that points back up the tree sends the walk into the same directory again and again.
From a fresh project — cargo new walk-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. Each block creates the tree it walks, so it runs on its own and in any order. The fs::create_dir_all and fs::write calls at the top are fixtures; in real code the tree would already exist.
Recursion over read_dir
Section titled “Recursion over read_dir”read_dir gives you one level. Walking a tree is the same call applied to every directory it finds, so the walker is ten lines and one recursive function:
use std::fs;use std::path::Path;
fn walk(dir: &Path) -> std::io::Result<()> { let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?; entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); println!("{}", path.display()); if entry.file_type()?.is_dir() { walk(&path)?; } } Ok(())}
fn main() -> std::io::Result<()> { // Removed first so the block can run twice. let _ = fs::remove_dir_all("demo"); fs::create_dir_all("demo/src")?; fs::create_dir_all("demo/tests")?; fs::write("demo/README.md", "# demo\n")?; fs::write("demo/main.rs", "fn main() {}\n")?; fs::write("demo/src/lib.rs", "")?; fs::write("demo/tests/cli.rs", "")?;
walk(Path::new("demo"))?; Ok(())}demo/README.mddemo/main.rsdemo/srcdemo/src/lib.rsdemo/testsdemo/tests/cli.rsTwo details before the interesting one. The entries are sorted because read_dir returns them in no order you can rely on, and a walk that reshuffles itself between runs is unreadable. And entry.file_type()? is what decides whether to descend — that choice is the whole subject of the next section.
Symlinks: two answers to “is this a directory?”
Section titled “Symlinks: two answers to “is this a directory?””Build a tree with a symlink to a directory in it and ask every entry both questions:
use std::fs;use std::os::unix::fs::symlink;use std::path::Path;
fn walk(dir: &Path) -> std::io::Result<()> { let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?; entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); println!("{}", path.display()); if entry.file_type()?.is_dir() { walk(&path)?; } } Ok(())}
fn main() -> std::io::Result<()> { // Removed first so the block can run twice. let _ = fs::remove_file("demo/link"); let _ = fs::remove_dir_all("demo"); fs::create_dir_all("demo/real")?; fs::write("demo/real/notes.txt", "todo\n")?; symlink("real", "demo/link")?;
let mut entries: Vec<_> = fs::read_dir("demo")?.collect::<Result<Vec<_>, _>>()?; entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let ft = entry.file_type()?; println!( "{}: file_type().is_dir() = {}, is_symlink() = {}, path.is_dir() = {}", entry.file_name().to_string_lossy(), ft.is_dir(), ft.is_symlink(), entry.path().is_dir(), ); } println!(); walk(Path::new("demo"))?; Ok(())}link: file_type().is_dir() = false, is_symlink() = true, path.is_dir() = truereal: file_type().is_dir() = true, is_symlink() = false, path.is_dir() = true
demo/linkdemo/realdemo/real/notes.txtThe two answers disagree. DirEntry::file_type() answers from the directory entry itself and, as the standard library puts it, “will not traverse symlinks if this entry points at a symlink” — so demo/link reads as a symlink, not a directory. Path::is_dir() goes the other way: it calls fs::metadata, which follows the link and reports what is at the end of it. The walk therefore prints demo/link once and moves on, exactly as it did in the first block for an ordinary file.
That is the behaviour you usually want from a tree walk, and not only for tidiness. A symlink can point anywhere, including back up the tree; swap the decision to path.is_dir() and the walk follows:
use std::fs;use std::os::unix::fs::symlink;use std::path::Path;
fn walk(dir: &Path) -> std::io::Result<()> { let mut entries: Vec<_> = fs::read_dir(dir)?.collect::<Result<Vec<_>, _>>()?; entries.sort_by_key(|entry| entry.file_name()); for entry in entries { let path = entry.path(); println!("{}", path.display()); if path.is_dir() { walk(&path)?; } } Ok(())}
fn main() -> std::io::Result<()> { // Removed first so the block can run twice. let _ = fs::remove_file("demo/link"); let _ = fs::remove_dir_all("demo"); fs::create_dir_all("demo/real")?; fs::write("demo/real/notes.txt", "todo\n")?; symlink("real", "demo/link")?;
walk(Path::new("demo"))?; Ok(())}demo/linkdemo/link/notes.txtdemo/realdemo/real/notes.txtnotes.txt now turns up twice — once through the link, once through demo/real. Following links is sometimes what you want, but know what a cycle costs. On a tree where the symlink pointed at demo itself, this walk re-entered the same directory again and again — demo/loop, demo/loop/loop, demo/loop/loop/loop, 33 levels deep — until the OS refused to resolve the chain any further. It still exited 0: from there is_dir() quietly answers false, because a failed fs::metadata becomes “not a directory”, and the walk finished without reporting anything, having listed the same directory 33 times. Descend through symlinks only when you know where they lead.
When the tree is bigger than a toy
Section titled “When the tree is bigger than a toy”Ten lines cover a small tree, and past that the walkdir crate wraps the same recursion in one iterator: WalkDir::new("demo") yields the whole tree, and each item is a Result you can ? on or skip with filter_map — the same two choices read_dir gave you. Following symlinks is opt-in with follow_links(true), off by default like the walk above, and when it is on, walkdir reports a symlink loop as an error — its Error type carries “if a loop was detected while following symbolic links”. filter_entry can cut whole subtrees before they are read, which is how you skip hidden directories without walking into them first. The crate’s own docs ask for one line in Cargo.toml: walkdir = "2".
Related
Section titled “Related”- How to list the files in a directory — one level, no recursion
- How to check whether a file or directory exists — asking about one path instead of walking
- Reading and writing files — the rest of the chapter