fafind (and the shorter alias faf) is a zero-allocation, parallel filesystem search tool written in Rust, focused purely on filename matching.
It’s built to rip through millions of files with minimal overhead.
Most search tools either:
- scan file contents (slow for this use case),
- allocate constantly, or
- bottleneck on output or synchronization
faf avoids all of that.
This is a hot-path optimized walker with:
- zero allocations per entry
- SIMD substring matching
- ASCII fast paths with Unicode fallback
- parallel traversal using a work-stealing scheduler
git clone https://github.com/rywils/fafind
cd fafind
cargo build --release
sudo cp target/release/faf target/release/fafind /usr/local/bin/Both faf and fafind are built from the same codebase. Use whichever name you prefer.
- Arch (AUR):
fafind-bin— seepackaging/aur/README.md - Homebrew: see
RELEASE.md
The AUR package installs fafind and a faf symlink. From source, copy or link both names as you prefer.
faf <target> [root]
# same as:
fafind <target> [root]If root is not provided, it defaults to /.
Matches filename without extension.
faf main .Matches:
main.rsmain.go
Does NOT match:
domain.rs
faf -s foo .Matches:
foobar.txtmyfoo.rsprefoo
faf -p Makefile .Matches:
Makefile
Only exact filename match (including extension).
When stdout is a terminal (and not -0 / --null), matches are highlighted:
| Color | Applies to |
|---|---|
| Dim | Path before the filename (/path/to/) |
| Green | The matched part of the name (stem in default/-p; each hit in -s) |
| Bold green | Stem in -p (exact) mode |
| Yellow | Extension (.rs, .js, .docx, …) |
| Orange | Non-matching parts of the name in -s only |
Stem match — faf main . on /app/main.rs: dim /app/, green main, yellow .rs
Substring — faf -s main . on has_dot_entry_main_corner.js:
- Orange:
has_dot_entry_and_corner - Green:
main - Yellow:
.js
Control coloring with --color auto (default), --color always, or --color never. Colors are off when output is piped to a file or tool unless you force --color always.
faf -i readme .faf --max-depth 3 main .faf --exclude target,node_modules main .faf --gitignore main .faf --type f main . # files only
faf --type d src . # directories only
faf --type a main . # any (default)faf -0 main . | xargs -0 rmDisables color highlighting.
faf -v main .Sends to stderr:
[SCAN]for every visited entry[SKIP]for excluded directories[ERROR]for unreadable entries
Matches are still written to stdout as normal.
Suppresses the summary line printed to stderr after the search completes.
faf -q main .faf -i --exclude target,node_modules --max-depth 5 main .- no heap usage per file
- stack buffers for ASCII matching
- fallback only when necessary
- uses all available CPU cores
- work-stealing via
ignore::WalkBuilder
- each worker formats matches into a private buffer
- TTY: batched stdout writes (64 KiB) to avoid locking on every match
- pipes: flush after each match line so scripts see results immediately
- atomic counters for scan/match totals (no mutex on the hot path)
- ~95% of filenames handled without Unicode overhead
- powered by
memchr::memmem - length prefilter skips obvious non-matches before full comparison
clapfor CLI parsingignorefor parallel walkingmemchrfor fast substring searchsmallvecfor stack-allocated exclude lists
- no channels
- no shared match queues
- no UTF-8 conversion on Unix (raw bytes)
- matcher length prefilter and per-worker config caching
- newline-separated by default
- NUL-separated with
-0 - raw OS bytes on Unix (no encoding overhead)
0 = matches found
1 = no matches
2 = error / invalid usage
- not a content search tool (use
greporrg) - not a fuzzy matcher
- not a UI tool
This is a fast, deterministic filename matcher.
See CHANGELOG.md.
MIT