fd — Fast File Finder#
What it is#
fd is a free, open-source Rust-based alternative to the POSIX find command, maintained by David Peter on GitHub. It uses a simpler syntax, respects .gitignore rules by default, supports regex and glob patterns, runs searches in parallel, and produces colorized output — making it significantly faster and easier to use than find on most codebases. Reach for fd in day-to-day development workflows; use POSIX find when you need maximum portability or the full depth of expression-based filtering that fd does not expose.
Installation#
sudo apt install fd-find # Debian/Ubuntu (binary: fdfind)
sudo dnf install fd-find # Fedora/RHEL
brew install fd # macOS
cargo install fd-find # cargo
# Ubuntu alias: add to ~/.bashrc
alias fd=fdfind
Output: (none — exits 0 on success)
Basic usage#
fd pattern # search by name in cwd (regex by default)
Output:
src/main.rs
src/utils/parser.rs
tests/parser_test.rs
fd pattern /path/to/dir # search in specific directory
fd # list ALL non-ignored files (like ls -R)
fd -g "*.log" # glob pattern instead of regex
Output:
var/log/syslog.log
var/log/auth.log
var/log/nginx/access.log
var/log/nginx/error.log
Pattern matching#
fd treats its pattern as a regex by default and matches against the file name only (not the full path). It is case-insensitive by default; switch to glob mode with -g when the regex syntax feels like overkill for simple wildcards.
fd 'config\.ya?ml' # regex: config.yaml or config.yml
fd -g "*.{py,rb}" # glob: .py or .rb files
fd -F "main.c" # fixed string (no regex)
fd -i "readme" # case-insensitive
fd "^test_" src/ # anchored to start of filename
Output: (none — exits 0 on success)
Filter by type#
-t restricts results to a specific entry kind, equivalent to find -type. The most common values are f (regular file), d (directory), l (symlink), and x (executable); combine multiple -t flags to allow more than one type.
fd -t f pattern # files only
fd -t d pattern # directories only
Output:
src/components/
src/utils/
tests/fixtures/
docs/api/
fd -t l pattern # symlinks only
fd -t x pattern # executable files
fd -t e pattern # empty files/dirs
Output: (none — exits 0 on success)
Filter by extension#
-e matches on file extension without needing a regex, and the dot is implied — use py, not .py. Multiple -e flags are OR’d together, so fd -e ts -e tsx finds both TypeScript variants in a single pass.
fd -e py # Python files
Output:
src/main.py
src/utils/helpers.py
tests/test_main.py
tests/test_helpers.py
scripts/deploy.py
fd -e ts -e tsx # TypeScript files (multiple -e)
fd -e log /var/log/ # .log files in /var/log
Output: (none — exits 0 on success)
Hidden and ignored files#
fd skips dot-files and anything listed in .gitignore / .fdignore by default — which is what you want in most dev workflows. Use -H to include hidden files, -I to ignore the ignore-files, or -u (unrestricted) as shorthand for both at once.
fd -H pattern # include hidden (dot) files
Output:
.github/workflows/ci.yml
.env.example
src/.cache/data.json
.gitignore
fd -I pattern # no-ignore: don't respect .gitignore
fd -HI pattern # hidden + no-ignore
fd --no-ignore-vcs # ignore VCS rules only
Output: (none — exits 0 on success)
Depth control#
-d / --max-depth caps how many directory levels fd descends, with 1 meaning immediate children of the search root. --min-depth skips results shallower than the given level, useful for excluding top-level config files when you only want nested ones.
fd -d 1 pattern # max depth 1 (immediate children)
fd -d 3 pattern # max depth 3
fd --min-depth 2 pattern # skip top-level matches
Output: (none — exits 0 on success)
Size and time filters#
-S accepts a size with a + (larger than) or - (smaller than) prefix and units like k, M, G. The --changed-within / --changed-before flags accept human durations (1d, 2w) or ISO dates, making it easy to find recently touched or stale files without find’s -mtime arithmetic.
fd -S +1M # files larger than 1 MB
fd -S -100k # files smaller than 100 KB
fd -S +1G /var # files larger than 1 GB
fd --changed-within 1d # modified in last 24 hours
fd --changed-within 1w # modified in last week
fd --changed-before 30d # older than 30 days
fd --changed-before 2024-01-01 # before a specific date
Output: (none — exits 0 on success)
Execute actions#
-x runs a command once per result (like find -exec), while -X collects all results and passes them as a single batch to one process invocation — use -X when the command benefits from seeing all matches at once (e.g., wc -l for a combined total). The {} placeholder and its variants ({/}, {.}, {/.}) let you reference parts of the matched path.
fd -e log -x rm {} # delete all .log files
fd -e jpg -x convert {} {.}.png # convert each jpg → png
fd -e py -X wc -l # pass ALL results at once to wc
Output:
42 src/main.py
87 src/utils/helpers.py
31 tests/test_main.py
18 tests/test_helpers.py
178 total
# {} placeholders
# {} full path
# {/} filename only
# {//} parent directory
# {.} path without extension
# {/.} filename without extension
Output: (none — exits 0 on success)
Practical examples#
# Find all Python files and run flake8
fd -e py -X flake8
# Find files modified today and copy to backup
fd --changed-within 24h -t f -x cp {} /backup/{}
# Find and delete all __pycache__ directories
fd -t d __pycache__ -X rm -rf
# Find all .env files (hidden, not gitignored normally)
fd -HI -g ".env*"
# Find large video files
fd -e mp4 -e mkv -e mov -S +500M -x du -sh {}
# Rename: add timestamp prefix to all .log files
fd -e log -x mv {} "$(date +%Y%m%d)_{/}"
# Find all symlinks pointing to non-existent targets
fd -t l -x sh -c '[ ! -e "$1" ] && echo "$1"' _ {}
# Count files by extension
fd -t f | sed 's/.*\.//' | sort | uniq -c | sort -rn | head -20
# Find the largest files under a directory
fd -t f -S +1M --no-ignore | xargs du -sh | sort -rh | head -20
Output: (none — exits 0 on success)
Output options#
By default fd prints relative paths, one per line. Use --absolute-path when you need full paths for piping into other tools, -0 for NUL-delimited output safe with filenames containing spaces or newlines, and --list-details for an ls -l-style view without a separate eza invocation.
fd pattern --absolute-path # print absolute paths
Output:
/home/alice/projects/myapp/src/main.rs
/home/alice/projects/myapp/src/utils/parser.rs
/home/alice/projects/myapp/tests/parser_test.rs
fd pattern -0 | xargs -0 ... # NUL-delimited (safe for spaces)
fd pattern --color never # disable colour
fd pattern --one-file-system # don't cross mount points
fd --list-details # ls-style long output
Output:
.rw-r--r-- 4.2k alice 24 Apr 14:30 src/main.rs
.rw-r--r-- 1.8k alice 24 Apr 14:28 src/utils/parser.rs
.rw-r--r-- 932 alice 20 Apr 10:45 tests/parser_test.rs
fd --follow # follow symlinks during traversal
Output: (none — exits 0 on success)
Exclude paths#
-E takes a glob pattern and skips any path — file or directory — that matches it. Multiple -E flags are AND’d (all exclusions apply), and you can make exclusions permanent by adding them to ~/.config/fd/ignore in gitignore syntax.
fd pattern -E "*.min.js" # exclude glob
fd pattern -E ".git" # exclude directory
fd pattern -E "node_modules" -E "dist" # multiple excludes
Output:
src/index.js
src/components/Button.js
src/utils/format.js
fd vs find equivalents#
| find | fd |
|---|---|
find . -name "*.py" | fd -e py |
find . -type f -name "*.log" | fd -t f -e log |
find . -type d -name __pycache__ | fd -t d __pycache__ |
find . -mtime -1 | fd --changed-within 1d |
find . -size +1M | fd -S +1M |
find . -name "*.sh" -exec chmod +x {} \; | fd -e sh -x chmod +x {} |
find . -not -path "./.git/*" | fd -E ".git" |
[!TIP]
fdrespects.gitignore,.fdignore, and.ignorefiles. Add a global ignore file at~/.config/fd/ignore(same gitignore syntax) to permanently skip things likenode_modules.
Default behaviour vs. find#
fd makes choices that are quietly different from find and which matter on every search. Knowing them up front avoids “why does fd not see my file?” head-scratching.
| Behaviour | fd | find |
|---|---|---|
Hidden files (.foo) | hidden by default | shown |
.gitignore / .ignore / .fdignore | respected | not consulted |
| Pattern is matched against | basename only (with -p: full path) | basename via -name, path via -path |
| Pattern syntax | regex | shell glob |
| Match anchoring | substring (not anchored) | basename glob (anchored to whole basename) |
| Case sensitivity | smart-case (lower → insensitive) | case-sensitive |
| Output coloring | yes when TTY | no |
| Threading | parallel walk | single-threaded |
| Symlinks | not followed | not followed |
| Default action |
Smart-case is the most surprising default: fd foo matches Foo.txt and FOO.md because the pattern is all lowercase; fd Foo matches only paths containing literal Foo. Force a mode with -s (case-sensitive) or -i (always insensitive).
fd readme # matches README.md, readme.txt, ReadMe.org
fd README # only matches the exact case
fd -s readme # forced case-sensitive: only lowercase readme
fd -i README # forced case-insensitive
Output: (none — exits 0 on success)
Pattern matching deep dive#
The default search is a regex against the basename. Use -g for glob mode, -F for fixed strings, and -p/--full-path to match against the entire path. Anchors (^, $) apply to the basename or full path depending on -p.
fd '\.test\.ts$' # regex anchored to end of basename
fd -g '*.test.{ts,tsx}' # glob with brace expansion
fd -F '+page.svelte' # fixed string — the + is not a regex quantifier
fd -p 'src/.*/index\.ts' # regex against the FULL path
fd -p -g '**/components/**/*.tsx' # glob against full path
Output: (none — exits 0 on success)
[!TIP] Use
-gwhenever the pattern has braces, stars, or question marks but no regex meta-characters.fd -g "*.{py,rb}"reads more naturally thanfd '\.(py|rb)$'and behaves the same.
Type filter combinations#
-t accepts the same short codes as find -type plus a few fd-specific ones. Multiple -t flags are OR’d, which is the right behavior for queries like “files or symlinks but not directories.”
| Code | Meaning |
|---|---|
f | regular file |
d | directory |
l | symbolic link |
x | executable file |
e | empty file or directory |
s | socket |
p | named pipe (FIFO) |
b | block device |
c | character device |
fd -t f -t l pattern # files OR symlinks
fd -t d -t e # empty directories
fd -t x # executables (anything with +x)
fd -t f -e py -e pyi # python source + stub files
Output: (none — exits 0 on success)
Hidden, ignored, and unrestricted#
fd has three independent ignore layers: dotfiles (hidden), VCS rules (.gitignore), and other ignore files (.ignore, .fdignore, ~/.config/fd/ignore). Each layer has its own flag, and the shorthand -u peels them off in stages.
| Flag | What it does |
|---|---|
| (default) | skip hidden, respect all ignore files |
-H / --hidden | include dotfiles |
-I / --no-ignore | ignore .gitignore, .ignore, .fdignore, global ignore |
--no-ignore-vcs | ignore only .gitignore and .git/info/exclude |
--no-ignore-parent | don’t search up the tree for ignore files |
-u | -H -I (hidden + no-ignore) |
-uu | -H -I --no-ignore-vcs (everything) |
fd .env # may return nothing — .env is hidden
fd -H .env # now visible
fd -HI node_modules # find them even though .gitignore lists them
fd -uu pattern # nuclear option: include everything
Output:
.env
.env.example
config/.env.production
Global ignore file#
A persistent ignore file at ~/.config/fd/ignore (or $XDG_CONFIG_HOME/fd/ignore) uses gitignore syntax and applies to every fd invocation. This is the right place to silence node_modules, target, .venv, and other build directories everywhere on your machine.
# ~/.config/fd/ignore
node_modules/
target/
.venv/
__pycache__/
dist/
.cache/
*.pyc
A project-local .fdignore overrides only within that subtree, and is checked in just like .gitignore.
mkdir -p ~/.config/fd
cat > ~/.config/fd/ignore <<'EOF'
node_modules/
target/
.venv/
__pycache__/
EOF
Output: (none — exits 0 on success)
Time and size filters in depth#
--changed-within and --changed-before accept human-friendly durations or absolute dates. Durations use single-character units: s seconds, m minutes, h hours, d days, w weeks. Absolute dates are ISO YYYY-MM-DD with an optional time. The flags can be combined to bracket a window.
fd --changed-within 30m # touched in the last half hour
fd --changed-within 2w # last two weeks
fd --changed-before 1d # older than 24 hours
fd --changed-within 7d --changed-before 1d # 1–7 days old
# Absolute date
fd --changed-within 2026-04-01 # since April 1
fd --changed-before "2026-04-01 12:00:00"
Output: (none — exits 0 on success)
For size, -S (or --size) uses a +/- prefix and a unit suffix; multiple -S flags combine as a range. Binary units (ki, Mi, Gi) are also accepted alongside decimal (k, M, G).
fd -S +10M # larger than 10 MB
fd -S -1k # smaller than 1 KB
fd -S +100M -S -1G # between 100 MB and 1 GB
fd -S +1Gi -t f # binary gibibytes
Output:
./videos/intro.mp4
./videos/outro.mp4
./assets/training-data.zip
Execute: -x vs. -X with placeholders#
The single most powerful difference between -x and -X: -x forks a process per result (parallelized across CPU cores by default), while -X collects all results into one big argument list and runs the command once. Use -x for per-file work that benefits from parallelism (image conversion, linting); use -X for tools that need to see the whole set (wc, tar, bat).
The {} placeholder family lets you compose new paths from each match without spawning a sh -c:
| Placeholder | Expands to |
|---|---|
{} | full path including basename |
{/} | basename only |
{//} | parent directory |
{.} | full path without extension |
{/.} | basename without extension |
# Parallel: one ImageMagick process per file, up to one per CPU
fd -e jpg -x convert {} {.}.webp
# Batched: pass all results to one tar invocation
fd -e log --changed-before 7d -X tar -czf old-logs.tgz
# Rename: move file.bak -> file
fd -e bak -x mv {} {.}
# Per-file with placeholder in two positions
fd -e mp4 -x ffmpeg -i {} {.}.mp3
# Limit parallelism — useful for heavy commands
fd -e jpg -x -j 2 convert {} {.}.webp # at most 2 in parallel
Output: (none — exits 0 on success)
[!TIP]
{}only quotes its replacement when the shell needs it. If you’re piping the result into a second command, use-x sh -c '...' _ {}and quote"$1"inside the shell snippet.
Exclude patterns#
-E accepts a glob and skips any matching file or directory. Multiple -E flags are AND’d (all exclusions apply). To make an exclusion persistent, add it to ~/.config/fd/ignore or .fdignore.
fd -e ts -E "*.test.ts" # source files, skip tests
fd -e py -E "__pycache__" # skip cache dirs
fd -E "dist" -E "build" -E "target" # skip multiple build dirs
fd -t f -E '*.{lock,log,tmp}' # skip transient files
# Exclude a path pattern (not just basename)
fd -p -E '*/test/fixtures/*'
Output:
./src/main.ts
./src/api/routes.ts
./src/utils/helpers.ts
Color customization#
fd honors the LS_COLORS environment variable for filename colors — the same one used by ls, eza, and tree. The --color flag toggles output: auto (default), always, never. To force colors through a pager, use --color=always | less -R.
fd --color=always pattern | less -R
fd --color=never pattern | sort
# LS_COLORS controls per-extension coloring
LS_COLORS='*.md=00;36:*.py=00;33' fd
Output: (none — exits 0 on success)
Pairing with other tools#
fd shines as the front of a pipeline. It is fast, predictable, and outputs paths that other tools consume directly. The patterns below show up constantly in real workflows.
Pair with fzf for interactive selection#
# Pick a file to open in $EDITOR
$EDITOR "$(fd -t f | fzf)"
# Pick a directory to cd into
cd "$(fd -t d | fzf)"
# Set fzf default to use fd (much faster than the built-in walker)
export FZF_DEFAULT_COMMAND='fd -t f -H -I'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"
Output: (none — exits 0 on success)
Pair with ripgrep for path-then-content filtering#
# Find typescript files, then grep them
fd -e ts -e tsx | xargs rg "useEffect"
# rg's --files can do this too, but fd handles time filters
fd -e ts --changed-within 7d | xargs rg "TODO"
Output: (none — exits 0 on success)
Pair with xargs and -0 for hostile filenames#
fd -0 -e log --changed-before 30d | xargs -0 rm --
fd -0 -t f | xargs -0 -n1 -P4 sha256sum > checksums.txt
Output: (none — exits 0 on success)
Open every match in $EDITOR#
fd -e md -x $EDITOR
fd -e md | xargs -o $EDITOR # alternative with xargs
Output: (none — exits 0 on success)
More real-world recipes#
# Rename: replace _ with - in every filename under cwd
fd . -t f -x bash -c 'mv "$0" "${0//_/-}"' {}
# Find Python files that haven't been touched in 90 days
fd -e py --changed-before 90d
# Largest 10 files in the tree (respecting gitignore)
fd -t f -X du -h | sort -rh | head
# Files newer than last commit
fd --changed-within "$(git log -1 --format=%cI)" -t f
# Project root detection: walk up until .git or package.json
fd -H -d 1 -a '\.git$|package\.json' --search-path "$PWD"
# Make every .sh file in scripts/ executable
fd -e sh . scripts/ -x chmod +x {}
# Delete .DS_Store everywhere
fd -H -I '\.DS_Store$' -x rm
# Count files per extension
fd -t f -x echo {} \; | sed 's/.*\.//' | sort | uniq -c | sort -rn | head
Output: (none — exits 0 on success)
fd vs. find comparison#
| Aspect | fd | find |
|---|---|---|
| Speed (typical repo) | 5-10× faster | baseline |
| Threading | parallel | single-threaded |
| Defaults | dev-friendly (skip hidden/ignore) | exhaustive |
| Pattern type | regex (or -g glob, or -F fixed) | glob (-name), path (-path), or regex (-regex) |
| Case-sensitivity | smart-case default | case-sensitive |
.gitignore aware | yes | no |
| Time syntax | --changed-within 1d | -mtime -1 (off-by-one trap) |
| Exec | -x (parallel) / -X (batched) | -exec ... \; / -exec ... + |
| Exclude dirs | -E pattern | -prune idiom |
| Placeholders | {}, {.}, {/}, {//}, {/.} | {} only |
| Output for scripts | -0 (NUL) / --list-details | -print0 / -printf |
| Portability | install required | POSIX, everywhere |
Reach for fd when developing inside a project. Reach for find (sections/linux/find) on servers, in portable scripts, or when you need behavior fd doesn’t expose (permission tests, full POSIX expression operators).
Configuration#
fd has no .toml config file — its only configuration is a stack of ignore files and a handful of environment variables. The ignore files use gitignore syntax: pattern per line, # comments, ! to re-include, trailing / for directories. fd walks up the tree from the search root and reads every ignore file it finds, so a .fdignore in a parent directory applies to everything below it just like .gitignore.
Files are read in this order; later sources can re-include with !pattern:
~/.config/fd/ignore(or$XDG_CONFIG_HOME/fd/ignore) — global, applies to every invocation..fdignorein any parent directory — per-project, fd-specific. Check it in alongside.gitignore..ignorein any parent directory — shared withrg,ag, and other walkers..gitignoreand.git/info/exclude— VCS rules; disable with--no-ignore-vcs.- Command-line flags —
-H,-I,-u,-uu,-Eoverride the above.
# ~/.fdignore — applies to your whole home (if cwd is under it)
node_modules/
target/
.venv/
__pycache__/
.cache/
dist/
build/
*.pyc
!important.pyc # but don't ignore this one
Useful environment variables:
| Variable | Effect |
|---|---|
LS_COLORS | Per-extension colours (shared with ls, eza, tree). |
NO_COLOR | Disables all colour output (respected since 9.0; also honoured by --list-details since 10.0). |
XDG_CONFIG_HOME | Overrides the location of ~/.config/fd/ignore. |
Command-line aliases that pay for themselves:
# ~/.bashrc or ~/.zshrc
alias fda='fd -HI' # show everything
alias fdt='fd --changed-within 1d' # today's changes
alias fde='fd -t e' # empty files/dirs
alias fdx='fd -t x' # executables
Output: (none — exits 0 on success)
Recent additions (fd 9.x / 10.x)#
The 9.x and 10.x line brought a large traversal-speed jump plus a handful of flags that are worth knowing about — most of them clean up rough edges in scripting and CI workflows.
| Flag / change | Since | What it does |
|---|---|---|
--ignore-contain <name> | 10.4 | Skip any directory containing an entry with this name (e.g. CACHEDIR.TAG, .no-fd). |
--hyperlink | 10.2 | Emit OSC 8 terminal hyperlinks so paths are clickable in modern terminals (WezTerm, iTerm2, Kitty, Ghostty). |
--format <template> | 10.1 | Inline format templates (same {} family as -x) without spawning a process. |
--strip-cwd-prefix=<auto|always|never> | 10.1 | Control whether leading ./ is stripped from output. |
--mindepth | 10.3 | Hidden alias for --min-depth. |
-t dir | 10.0 | Alias for -t directory. |
--newer @1714000000 | 10.0 | Unix-epoch seconds accepted in --changed-within / --changed-before (GNU date @%s style). |
.git/ no longer auto-ignored under -H | 10.0 | Reverts the 9.0 default; pass --no-ignore-vcs or -uu if you still want to skip it. |
| Up to 13× faster on 1M-file trees | 9.0 | Rewritten walker; thread count capped at 64 to avoid lock contention. |
--type b / --type c | 9.0 | Match block and character device files. |
Two flags from this list pay for themselves on day one:
fd --hyperlink -e md # clickable list of every markdown file
fd --format '{/.}' -e jpg # print just the stem of every JPG (no extension)
fd --ignore-contain CACHEDIR.TAG -e log # skip any cache dir that marks itself
fd --changed-within @1714000000 # everything since Apr 25 2024 (epoch)
Output: (none — exits 0 on success)
[!TIP]
--formatis the lightweight cousin of-x: no process spawn, no parallelism overhead — perfect for shaping output before piping into the next tool.fd -e jpg --format '{/.}.webp'prints the target filename for every source.
Common pitfalls#
- Pattern is regex, not glob —
fd "*.py"is a regex error. Usefd -e py,fd '\.py$', orfd -g "*.py". - Smart-case surprises —
fd readmematchesReadme.md;fd READMEdoes not. Use-sor-ito force a mode when scripting. - Pattern is basename-only by default —
fd 'src/main'finds nothing. Add-pfor full-path matching. .gitignoresilently hides results — outside a git repo with no.fdignore, the same query returns more results. If you’re searchingnode_modules, you need-Ior-u.-xruns in parallel — order of output is non-deterministic. Use-X(single batch) or pipe throughsortif order matters.{}is not shell-quoted automatically — if a filename contains a space,fd -x sh -c 'echo {}'is broken. Usefd -x sh -c 'echo "$1"' _ {}.- macOS Ubuntu name mismatch — the Ubuntu/Debian package installs as
fdfind; alias it tofdin~/.bashrc.
[!TIP]
fd --type empty(or-t e) plus--type directory(or-t d) finds dangling empty dirs left behind by build tools — pipe toxargs rmdirto clean them up.