fd
Prefer fd over find for filesystem searches.
fd is faster on large trees,
has saner defaults
(respects .gitignore, smart case, regex by default),
and a more ergonomic CLI.
Authoritative documentation:
https://github.com/sharkdp/fd
(README + fd --help).
Verify any non-trivial form against the manual before relying on it.
Why
- Parallel directory traversal —
noticeably faster than
findon large repos. - Sensible defaults out of the box:
hidden entries skipped,
.gitignore/.ignore/.fdignorehonored when inside a git repo, smart case (case-insensitive unless the pattern has an uppercase letter), patterns are regex by default. - Pattern matches against the filename by default,
not the full path —
fd foodoes whatfind . -iname '*foo*'does, without the wildcards-and-quotes ritual. - Parallel
-xexecution, null-separated output (-0), and a single set of options that compose without-print/-prunegymnastics.
Rule
fd PATTERN [PATH]
The first positional argument is the pattern;
the second is the search root
(defaults to the current directory).
A path-shaped first argument (containing /) is not a path —
use fd . PATH or fd --full-path PATTERN.
Direct substitutions
| Don't | Do |
|---|---|
find . -name 'foo*' | fd '^foo' |
find . -iname '*.md' | fd -e md |
find . -type f -name foo | fd -t f foo |
find . -type d -name node_modules | fd -t d node_modules |
find . -type l | fd -t l |
find . -name '*.txt' -delete | fd -e txt -X rm |
find . -name '*.txt' -exec rm {} \; | fd -e txt -x rm |
find . -name node_modules -prune | fd -E node_modules |
find . -size +100M | fd -S +100mi |
find . -mtime -7 | fd --changed-within 7d |
find . -mtime +7 | fd --changed-before 7d |
find . -name '*.go' -print0 | xargs -0 wc -l | fd -e go -0 | xargs -0 wc -l |
find /etc -name passwd | fd passwd /etc |
find / -name foo | fd -u foo / |
For an exact find -size match,
use the binary suffixes
(ki, mi, gi) —
-S +100m is 100 * 10^6 bytes,
-S +100mi is 100 * 2^20 bytes.
For find / searches that should also descend into hidden and
.gitignore-d directories,
add -u (alias for --no-ignore --hidden).
Idioms
Match by extension (one or many):
fd -e md
fd -e h -e cpp
Glob mode for shell-style patterns
(-g so ** and * mean the usual thing):
fd -g '**/*.test.ts'
fd -g 'libc.so' /usr
Match against the full path, not just the filename:
fd --full-path 'src/.*\.proto$'
Include hidden entries
(.git, .vscode, etc.)
but keep .gitignore rules:
fd -H pre-commit
Disregard .gitignore rules but keep hidden entries hidden:
fd -I node_modules
Search everywhere — both hidden and ignored:
fd -u PATTERN
Exclude paths (glob syntax, overrides every ignore rule, repeatable):
fd -E .git -E '*.bak' PATTERN
Limit depth:
fd -d 2 -t d
Combine multiple required patterns (all must match the filename, regex by default):
fd 'foo' --and 'bar' --and 'baz'
Execute per result
(parallel, {} substitution, no \; terminator):
fd -e jpg -x convert {} {.}.png
Execute once with all results as arguments
(batch tools like wc, clang-format, rg):
fd -e rs -X wc -l
Pipe null-separated to other tools:
fd -e go -0 | xargs -0 wc -l
Footguns
- First positional argument is the pattern, not the path.
fd /etc/passwdinterprets/etc/passwdas a regex with/in it and errors out with the suggestion to usefd . /etc/passwdor--full-path. - Default search excludes hidden and
.gitignored entries. When looking for something insidenode_modulesor.git, add-H -I(or-u) or the result list will be empty for the wrong reason. --execparallelizes by default. Output order is non-deterministic. Pass-j 1(or--threads 1) when serial execution is required.- Patterns are regex by default.
A bare
.matches any character; for a literal.fooanchor it ('\.foo$') or pass-F/--fixed-strings. - Size suffixes are decimal SI by default.
-S +100mis100 MB(10^6bytes perm); usemi/ki/gifor binary units when matchingfind -sizesemantics exactly.
Fallback to find
fd is not always available —
minimal CI images,
locked-down servers,
container base images without package extras.
Probe with command -v and fall back to find:
if command -v fd >/dev/null; then
fd -e log -t f -X rm
else
find . -type f -name '*.log' -exec rm {} +
fi
For a one-shot snippet in a controlled local environment,
use fd directly.
Switch to find unconditionally only when the target host is known
not to have fd
(e.g. a minimal Alpine container without fd in the image).
Do not install fd automatically just to satisfy this skill —
the fallback is the correct response when it is missing.
Installing fd
- macOS:
brew install fd - Debian/Ubuntu:
apt install fd-find— the binary is namedfdfindto avoid a name conflict (alias fd=fdfindis the standard workaround). - Fedora:
dnf install fd-find - Arch:
pacman -S fd - Alpine:
apk add fd - Prebuilt binaries and other platforms: https://github.com/sharkdp/fd/releases
When mise manages the host's tools,
the registry shortname fd resolves to aqua:sharkdp/fd —
install with mise use -g fd@<version>
(pinned, per the project's mise skill).
Do not use the ubi: backend —
the mise skill disables it.
Scope
Applies anywhere a shell snippet searches the filesystem:
Bashtool invocations.- Snippets in
SKILL.md/README.md/ docs. - Scripts under
scripts/. - Makefile recipes.
- CI configs.
- Install instructions.
If you encounter a find invocation while editing a file,
replace it with the equivalent fd form
(plus the find fallback when the snippet must run on a host that
may not have fd)
as part of the same edit.