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
- For interactive, quick searches:
fd— readable defaults, regex-first, respects.gitignore, colored output, parallel walking. - For ultra-flexible constructs, timestamp comparisons, pruning, and guaranteed portability:
find— gnarly syntax, but ubiquitous and feature-rich.
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:
# 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 / pacmansudo pacman -S fd
# Cargo (if you like compiling things yourself)cargo install fd-find --lockedOnce 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.
| Task | find | fd |
|---|---|---|
| Find files by name (glob) | find . -name '*.py' -type f | fd -e py -t f |
| Case-insensitive name | find . -iname '*Readme*' | fd -i readme |
| Regex match (filename) | find . -regex '.*foo[0-9]+\.js$' | fd 'foo[0-9]+\.js$' |
| Find directories | find . -type d -name node_modules | fd -t d node_modules |
| Max depth | find . -maxdepth 2 -name '*.log' | fd -d 2 -e log |
| Exclude a dir | find . -path ./node_modules -prune -o -name '*.js' -print | fd -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 days | find . -mtime -7 -type f | fd --changed-within 7d -t f |
| Execute per-file | find . -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 . -print0 | fd --print0 |
| Absolute paths | find $(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.
finddoesn’t know or care about.gitignore. It walks everything unless you tell it to skip a path. That makes it predictable and portable, but noisy by default.fdignores patterns in.gitignoreand other ignore files by default. That means your interactive searches don’t return dozens of compiled files you never want to touch.
Example: search for TODO filenames in a repo (not content):
# find (will include node_modules etc unless you prune)find . -type f -iname '*todo*'
# fd (clean, respects .gitignore)fd todoIf you need to override fd and search everything (including hidden and ignored):
# Include hidden + ignoredfd -u TODO# Or disable ignore behavior onlyfd -I TODO# Include hidden files onlyfd -H TODOWhy 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:
fdhas “smart case” by default: a lowercase pattern is case-insensitive unless you use uppercase letters. Use-i/--ignore-caseto force-insensitive and-s/--case-sensitiveto force-sensitive.finduses-name(shell glob) and-iname(case-insensitive) or-regex/-iregexfor regex matching.
Examples:
# fd regex defaultfd '^test.*\.py$'
# fd glob stylefd -g '*.md'
# find regexfind . -regex '.*test.*\.py$'
# case-insensitive findfind . -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.
# find: files onlyfind . -type f -name '*.sh'
# fd: files onlyfd -t f -e sh
# find: directories onlyfind . -type d -name build
# fd: directories onlyfd -t d buildfd 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.
# find sees hidden filesfind . -type f -name '.env'
# fd ignores hidden by defaultfd ".env" # may return nothing if .env is ignored/hidden# include hiddenfd -H .envIf 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:
# 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:
-x/--exec: run the command once per result (parallel by default)-X/--exec-batch: run the command once with all results as args
# run unzip per result (parallel)fd -e zip -x unzip
# batch-mode: one vim with all filesfd -g 'test_.*\.py' -X vim
# example with placeholders ({} and {.})fd -e jpg -x convert {} {.}.pngfd’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:
-prune: extremely handy for excluding directories in-place and avoiding costly descents (e.g.,-path ./node_modules -prune -o -print).fd’s-E/--excludecan skip matches, but-pruneis expressive when building complex expressions with side-effects.-newerand-newerXY: compare timestamps between files (not just “modified within N days”). Useful for build systems: “all files newer than this stamp”.-mtime/-atime/-ctime: direct, scriptable epoch-logic you might rely on in cronjobs.
Example find usage that’s hard to replicate with fd exactly:
# Skip node_modules, find JS files changed since marker.filefind . -path ./node_modules -prune -o -name '*.js' -newer marker.file -printIf 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:
- Written in Rust with careful IO performance.
- Parallelizes directory traversal and file statting.
- Skips ignored directories by default, so it often walks fewer nodes.
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.
- Use
fdfor local, interactive workflows and developer scripts where you control the environment (dotfiles, dev machines). - Use
findfor scripts that must run on clean servers, rescue mediums, Docker containers, or systems you don’t control.
Idiomatic compromise: detect fd at runtime and fall back to find.
# safe-search.shif command -v fd >/dev/null 2>&1; then fd --changed-within 7d -t f "$@"else find . -type f -mtime -7 "$@"fiNever 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:
# ~/.bashrc or ~/.zshrc# Prefer system-installed fd, fall back to fdfindif command -v fd >/dev/null 2>&1; then : # fd is availableelif command -v fdfind >/dev/null 2>&1; then alias fd=fdfindfi
# Helpful interactive aliasesalias ff='fd' # short and sweetalias fgrep='rg' # actually use ripgrep for content searchesFor 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):
-
Interactive dev work / exploration: fd wins. It’s readable, fast, colorful, and quietly respects your
.gitignore. It’s like hiring a sensible bicycle to move boxes around your garage: fast and the neighbors won’t call the cops. -
Batch processing where you need parallelism for transformations: fd is a very strong choice thanks to
-xand-X(and--threads), but test for edge cases. -
Complex filesystem queries that rely on timestamp arithmetic,
-newerXY, or nuanced-prunebehavior: find remains mandatory. It’s the Swiss Army tool you keep in the truck for those weird jobs. Like hiring a forklift to move a couch — sometimes it’s exactly what’s needed. -
Portability across systems, rescue images, minimal Docker images, and scripts that must run anywhere: find always wins. It’s guaranteed to be present where fd might not be.
Final pragmatic rules:
- Use
fdfor day-to-day exploration and speed. - Use
findfor deep, portable, or very-specific filesystem logic. - Don’t alias
fd=find. Trust me — that “shortcut” breaks the point of both tools.
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 --helpandfd --versionman findorman findutils- If you like both world workflows, add a tiny wrapper to safely use
fdwhen available andfindwhen not.