Skip to content
Go back

fd vs find: Rust Speed vs POSIX Power

By SumGuy 11 min read
fd vs find: Rust Speed vs POSIX Power

Take a breath. Close those tabs.

Two tools stroll into a repo: one’s an old, battered Swiss Army knife that mysteriously does everything (and has a dozen undocumented blades). The other’s a sleek Rust fox that gets you the result first and looks good doing it. That’s find vs fd in a nutshell.

If you’re the kind of person who learned find once and now always reaches for a search engine when your brain freezes at -exec \\;, this is for you. If you like sane defaults, color, and speed—stick around. If you’re writing scripts that must run on every POSIX box in the galaxy, you already know find will be there with its battle scars.

TL;DR


Install & setup (cargo, package managers)

find ships with your system as part of findutils on Linux or the BSD toolset on macOS. No setup required — it’s the one guaranteed to be on the rescue USB.

fd (aka fd-find in some distributions) is newer and installed via package managers or Cargo:

install-examples.sh
# Debian/Ubuntu (package named fd-find -> binary fdfind)
sudo apt install fd-find
# Make it usable as `fd` on Debian-family distros:
ln -s $(which fdfind) ~/.local/bin/fd || sudo ln -s $(which fdfind) /usr/local/bin/fd
# Homebrew (macOS)
brew install fd
# Arch / pacman
sudo pacman -S fd
# Cargo (if you like compiling things yourself)
cargo install fd-find --locked

Once installed, fd is just fd. On some distros the package is fd-find and the binary is fdfind — aliasing that to fd is common.

Pro tip: add a tiny shell function that prefers fd if available but falls back to find in scripts (see “Shell alias setup”). Your 2 AM self will appreciate it.


Syntax comparison table — common queries

Below are 12 common tasks with find and fd side-by-side so you can compare readability and learn quick swaps.

Taskfindfd
Find files by name (glob)find . -name '*.py' -type ffd -e py -t f
Case-insensitive namefind . -iname '*Readme*'fd -i readme
Regex match (filename)find . -regex '.*foo[0-9]+\.js$'fd 'foo[0-9]+\.js$'
Find directoriesfind . -type d -name node_modulesfd -t d node_modules
Max depthfind . -maxdepth 2 -name '*.log'fd -d 2 -e log
Exclude a dirfind . -path ./node_modules -prune -o -name '*.js' -printfd -E node_modules '\.js$'
Respect .gitignore?No (use git ls-files for repo-only)Yes (default), use -I to disable
Files changed in last N daysfind . -mtime -7 -type ffd --changed-within 7d -t f
Execute per-filefind . -name '*.zip' -exec unzip '{}' \;fd -e zip -x unzip
Execute batch (one cmd with all paths)find . -name '*.c' -exec clang-format -i '{}' +fd -e c -X clang-format -i
Print null-separated (for safe piping)find . -print0fd --print0
Absolute pathsfind $(pwd) -name '*.md'fd -a -e md (or fd -a / --absolute-path)

That table should give you a quick map — notice how fd leans toward short, composable flags and readable intent. find can do everything, but it’s often more verbose.


Gitignore behavior & why it matters

Here’s the thing: most repositories are full of build artifacts, node_modules, target/, venv/, and generated piles of crap you don’t want in search results.

Example: search for TODO filenames in a repo (not content):

Terminal window
# find (will include node_modules etc unless you prune)
find . -type f -iname '*todo*'
# fd (clean, respects .gitignore)
fd todo

If you need to override fd and search everything (including hidden and ignored):

fd-unrestricted.sh
# Include hidden + ignored
fd -u TODO
# Or disable ignore behavior only
fd -I TODO
# Include hidden files only
fd -H TODO

Why this matters: when you’re iterating interactively, the default fd behavior surfaces real code and not the build artifacts. It’s like hiring a bicycle to move a box, not a forklift—faster and less disruptive. Conversely, if you truly need to search everything (forensics, cleanup), find or fd -uu/-I -H are the tools.


Regex & case-insensitivity (-E exclude, -i/-u flags)

fd treats the pattern as a regular expression by default. If you want glob-like behavior, use -g/--glob.

Case handling:

Examples:

regex-examples.sh
# fd regex default
fd '^test.*\.py$'
# fd glob style
fd -g '*.md'
# find regex
find . -regex '.*test.*\.py$'
# case-insensitive find
find . -iname 'README.md'

fd also has -E/--exclude for quick glob excludes (like skipping .git or node_modules) and supports .fdignore and global ignore files.


Type filtering (-type f/d vs —type f/d)

Both tools let you filter by file type.

type-filtering.sh
# find: files only
find . -type f -name '*.sh'
# fd: files only
fd -t f -e sh
# find: directories only
find . -type d -name build
# fd: directories only
fd -t d build

fd packs --type into shorter flags (-t f, -t d), which reads nicely in pipelines.


Hidden files (—hidden vs find . -type f)

find . -type f will list dotfiles and files inside dot-directories by default. fd will not show dotfiles unless asked.

Terminal window
# find sees hidden files
find . -type f -name '.env'
# fd ignores hidden by default
fd ".env" # may return nothing if .env is ignored/hidden
# include hidden
fd -H .env

If you’re used to find and surprised by missing results in fd, remember it’s trying to be polite. If you want the raw dump, use -H and -I.


Exec patterns (-x / —exec) with examples

Both tools let you execute commands on results, but the UX differs.

find’s traditional exec forms:

find-exec.sh
# per-file execution (slower)
find . -name '*.zip' -exec unzip '{}' \;
# batched execution (passes all matches to one invocation when possible)
find . -name '*.c' -exec clang-format -i '{}' +

fd gives two ergonomic options:

fd-exec.sh
# run unzip per result (parallel)
fd -e zip -x unzip
# batch-mode: one vim with all files
fd -g 'test_.*\.py' -X vim
# example with placeholders ({} and {.})
fd -e jpg -x convert {} {.}.png

fd’s -x runs things in parallel (use --threads or -j to limit). find -exec is serial by default unless you manage backgrounding yourself. That parallelism makes fd a great helper for fast file transformations.


When find -newer, -prune, -mtime are still mandatory

fd has --changed-within and --changed-before which cover most -mtime cases, but find has some primitives and flexibility that fd doesn’t fully replicate:

Example find usage that’s hard to replicate with fd exactly:

find-prune-newer.sh
# Skip node_modules, find JS files changed since marker.file
find . -path ./node_modules -prune -o -name '*.js' -newer marker.file -print

If your workflow needs -newerXY nuance or tightly controlled pruning in complex boolean expressions, find keeps the crown.


Performance & parallel walking

fd is fast. Very fast. Why:

find is single-threaded (traditional find), deterministic, and optimizable with -O levels. For big, raw filesystem scans fd often wins in benchmarks; for targeted, cheap name checks find can be competitive.

Benchmarks vary by filesystem, cache state, and filters. The practical takeaway: for interactive searches and iterative use, fd will usually feel snappy. For complex, carefully pruned batch jobs with timestamp logic, find is rock-solid and sometimes faster when its optimizations kick in.


fd inside scripts (portability trade-offs)

This is the real tradeoff: fd makes life nicer, but it’s not guaranteed to exist on every machine.

Idiomatic compromise: detect fd at runtime and fall back to find.

safe-search.sh
# safe-search.sh
if command -v fd >/dev/null 2>&1; then
fd --changed-within 7d -t f "$@"
else
find . -type f -mtime -7 "$@"
fi

Never alias fd=find globally in scripts. That makes life confusing for readers of your code and breaks the whole point of fd’s ergonomics.


Shell alias setup (practical)

For interactive shells, a tiny convenience alias is fine. For Debian/Ubuntu where the binary is fdfind:

shell-setup.sh
# ~/.bashrc or ~/.zshrc
# Prefer system-installed fd, fall back to fdfind
if command -v fd >/dev/null 2>&1; then
: # fd is available
elif command -v fdfind >/dev/null 2>&1; then
alias fd=fdfind
fi
# Helpful interactive aliases
alias ff='fd' # short and sweet
alias fgrep='rg' # actually use ripgrep for content searches

For scripts, prefer explicit fallbacks instead of magical aliases. Your colleagues (and automation) will thank you when things don’t silently fail.


Conclusion: use cases & winner per scenario

Take a breath. Close those tabs. Here’s the practical verdict in SumGuy voice (beer in hand):

Final pragmatic rules:

There’s no prize for running fd everywhere if find would have done the job in a portable one-liner. Conversely, there’s no prize for wrestling find interactively when fd will get you home before your beer foams over.

Your 2 AM self will appreciate the ergonomics. Your production scripts will appreciate the reliability. Pick the right tool for the job and keep both in your toolbox.

Happy grepping. Or rather—happy fd-ing.

Further reading & cheatsheet

  • fd --help and fd --version
  • man find or man findutils
  • If you like both world workflows, add a tiny wrapper to safely use fd when available and find when not.

Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts