walkdir_minimal is a lightweight, POSIX-only directory walker written in
100% safe Rust, designed for maximum portability, robust error handling,
and predictable iteration order across UNIX-like systems (Linux, BSDs, Solaris).
Unlike the popular walkdir crate, which
offers extensive configurability and Windows support, walkdir_minimal aims to
provide a clean, dependency-free and fully deterministic implementation
that follows the UNIX filesystem model precisely β no abstractions, no hidden
buffering, no non-POSIX extensions.
-
π§± POSIX-only: Works on Linux, FreeBSD, OpenBSD, NetBSD, and Solaris.
-
βοΈ No dependencies: Implemented using only
std::fs,std::path, and minimal data structures. -
𦦠Lightweight and predictable: The walker uses a manual stack (no recursion), allowing predictable memory and performance behavior.
-
π¦ Configurable options via
WalkOptions:follow_links: whether to follow symbolic links to directories.max_depth: optional limit on traversal depth.
-
π§ Cycle detection: Detects and prevents infinite loops caused by symbolic links that form cycles.
-
π« Graceful handling of I/O errors: Broken symlinks, permission-denied directories, and other errors are returned as
Err(WalkError::Io). -
𦦠Filtering: Supports entry-level filtering with a user-provided closure.
-
π§« Deterministic: The order of traversal follows the order provided by the filesystemβs
readdir(3)implementation β consistent across runs on the same system. -
π§ͺ Minimal yet robust: Designed for projects that require reliable, low-level control rather than high-level abstraction.
walkdir_minimal is built under the following principles:
- POSIX compliance first β all filesystem operations map directly to their POSIX
equivalents (
lstat,stat,opendir,readdir, etc., via Rustβsstd::fs). - Deterministic behavior β the iterator never hides errors, skips entries silently, or spawns threads.
- No allocations beyond whatβs necessary β uses
Vecfor the manual stack andHashSetfor visited inode/device pairs (loop detection). - No recursion β prevents stack overflows and maintains stable memory usage even for deeply nested trees.
- Minimalism β the crate is intentionally limited to features that can be reasoned about and verified easily.
- Transparency β the API surfaces raw I/O results instead of silently ignoring or swallowing them.
walkdir_minimal embodies clarity over complexity. Its goal is not to compete
with feature-rich crates, but to provide a clean reference implementation
of a POSIX-only directory walker.
| Feature | walkdir |
walkdir_minimal |
|---|---|---|
| Cross-platform | β (Windows, macOS, Linux) | β POSIX only |
| Dependencies | Many (e.g., same-file, winapi) | β None |
| Error handling | Complex iterator states | Simple Result<Entry, WalkError> |
| Loop detection | Optional, platform-specific | Deterministic (dev, ino) hashing |
| Symbolic links | Optional follow | Optional follow |
| Custom sorting | Supported | Not supported (filesystem order only) |
| Performance | Optimized for general use | Optimized for predictability |
| Safety | 100% safe Rust | 100% safe Rust |
| Recursion | Implicit | Manual stack |
| Binary size | Larger | Tiny |
| Filter API | Supported (filter_entry) |
Supported |
| Error type | walkdir::Error |
WalkError |
| Metadata caching | Yes | No (on-demand) |
| Thread safety | Yes | No (intentionally minimal) |
use walkdir_minimal::{WalkDir, WalkError};
fn main() -> Result<(), WalkError> {
for entry in WalkDir::new(".")?.follow_links(false) {
match entry {
Ok(e) => println!("{}", e.path().display()),
Err(err) => eprintln!("Error: {}", err),
}
}
Ok(())
}Output example:
.
./src
./src/lib.rs
./src/entry.rs
./src/error.rs
./src/walkdir.rs
#[derive(Clone, Debug)]
pub struct WalkOptions {
pub follow_links: bool,
pub max_depth: usize,
}-
follow_linksβ Whentrue, symbolic links to directories are followed. -
max_depthβ Optional limit to recursion depth.Nonemeans unlimited.- The root is always depth
0. - Files or subdirectories at one level below are depth
1, and so on.
- The root is always depth
pub struct Entry {
path: PathBuf,
depth: usize,
}
impl Entry {
pub fn path(&self) -> &Path;
pub fn depth(&self) -> usize;
pub fn metadata(&self) -> io::Result<fs::Metadata>;
pub fn symlink_metadata(&self) -> io::Result<fs::Metadata>;
pub fn file_type(&self) -> io::Result<fs::FileType>;
}metadata()callsfs::metadata, following symlinks.symlink_metadata()callsfs::symlink_metadata, not following symlinks.file_type()reports the symbolic link type correctly.
pub enum WalkError {
Io(io::Error),
LoopDetected(PathBuf),
}-
Io(io::Error)β Covers all I/O-related errors, including:- Broken symbolic links (
ENOENT) - Permission-denied directories (
EACCES) - Filesystem read errors
- Broken symbolic links (
-
LoopDetected(PathBuf)β Reported when a cyclic symbolic link is detected (only if loop detection is enabled).
| Case | Behavior |
|---|---|
| Broken symlink | Yields Err(WalkError::Io) |
| Permission denied directory | Yields Err(WalkError::Io) and continues |
| Loop via symlink | Yields Err(WalkError::LoopDetected) if detection is on |
| Regular file as root | Returns file directly, no traversal |
| Unreadable entry | Returns Err(WalkError::Io) |
Exceeds max_depth |
Skips entry silently (depth-guarded) |
walkdir_minimal implements a depth-first directory traversal without relying on
any external dependencies, using only POSIX APIs available through Rustβs standard
library. The iterator is built around a manual stack-based traversal that mimics
recursion, avoiding stack overflows for deeply nested directories.
- Stack-based iteration: Uses an internal vector of
StackEntrystructs, each holding an activeReadDirhandle and its depth. - Loop detection: Uses a
HashSet<(dev, ino)>to detect and skip cyclic symlinks, preventing infinite recursion. - Filter callbacks: Optional user-provided closures (
filter_entry) allow pruning of the traversal tree dynamically. - Error resilience: Each I/O operation is wrapped in
Result, and errors are surfaced asWalkErrorvariants (Io,LoopDetected).
walkdir_minimal follows a fail-soft philosophy:
- Broken symlinks are returned as
Ok(Entry)unless metadata is explicitly requested. - Directories without permission to read (
EACCES) return anErr(WalkError::Io), allowing iteration to continue with the next entry. - Files disappearing mid-iteration yield
Err(WalkError::Io)gracefully.
This mirrors walkdirβs behavior but keeps it predictable and minimal.
Entry deliberately does not cache metadata by default. This ensures:
- Minimal memory overhead.
- Consistent behavior with file system changes.
- Full control for users who may wish to query
metadata()orsymlink_metadata()selectively.
let entry = Entry::new(path, depth);
if let Ok(meta) = entry.metadata() {
println!("File size: {} bytes", meta.len());
}walkdir_minimal targets POSIX systems only β this includes:
- GNU/Linux
- *BSD family (FreeBSD, OpenBSD, NetBSD, DragonFly)
- Solaris and Illumos
It relies on MetadataExt for device/inode access, which is non-portable
to Windows. No attempt is made to support non-POSIX environments.
- Single
ReadDirhandle open at a time per stack frame. - Minimal heap allocations aside from the stack and visited set.
- No synchronization primitives β designed for single-threaded deterministic traversal.
- Filtering and loop detection incur negligible overhead for typical file trees.
- No unsafe code.
- Uses standard library types exclusively (
HashSet,Vec,ReadDir, etc.). - All system calls are wrapped in safe Rust abstractions.
- Loop detection ensures full reliability even on pathological file systems.
- Static analysis tools.
- POSIX-friendly installers and archivers.
- File packers and dependency scanners.
- System recovery tools that must run without external crates.
Example: skipping hidden files and following symlinks safely:
use walkdir_minimal::WalkDir;
let iter = WalkDir::new("/usr")
.unwrap()
.follow_links(true)
.filter_entry(|e| !e.path().file_name().map(|n| n.to_string_lossy().starts_with('.')).unwrap_or(false));
for entry in iter {
match entry {
Ok(e) => println!("{}", e.path().display()),
Err(err) => eprintln!("Error: {}", err),
}
}- Ideal for small binaries, system utilities, and initramfs tools.
- Zero build dependencies (fast compile times).
- Deterministic and predictable traversal order.
- Designed to be readable and hackable.
- Loop detection uses
(dev, ino)pairs to identify unique directories. - When
follow_linksis disabled, symlink loops are naturally impossible. max_depthlimits traversal, excluding deeper entries.- The iterator yields entries as soon as they are discovered β no preloading or buffering.
Contributions are very welcome! Whether itβs fixing a bug, improving documentation, or adding new features that align with the minimalist and POSIX-only philosophy, your input is appreciated.
Please follow these guidelines when contributing:
- Keep it minimal β avoid adding dependencies or non-POSIX abstractions.
- Preserve safety β no
unsafecode will be accepted. - Document behavior clearly β especially for error cases and edge conditions.
- Add tests β every feature or bug fix should include a minimal test.
If you find a bug or have a suggestion for improvement:
- Open an issue describing the behavior or proposal clearly.
- Include steps to reproduce (for bugs) or examples (for feature requests).
Pull requests should target the main branch and include clear commit messages.
walkdir_minimal includes tests for common scenarios:
- Regular files and nested directories.
- Symbolic link loops and detection.
- Permission-denied directories.
- Broken symbolic links.
- Filtered traversals and depth limits.
Run the test suite with:
cargo testwalkdir_minimal/
βββ src/
β βββ lib.rs # Main crate entry
β βββ entry.rs # Defines the Entry type
β βββ error.rs # WalkError and error utilities
β βββ options.rs # WalkOptions definition
β βββ tests.rs # Unit and integration tests
β βββ walkdir.rs # Core iterator implementation
βββ README.md # Project documentation
βββ LICENSE # License file (MIT)
βββ Cargo.toml # Package metadata
This project is licensed under the MIT License.
See the LICENSE file for full details.
By contributing to this repository, you agree that your contributions will be licensed under the same MIT terms.
All notable changes to this project will be documented
in the changelog file.
Created and maintained by LinuxDicasPro.
If you find this project useful, consider starring the repository or contributing feedback to improve it further.
