cheat sheet
fd
Modern find replacement written in Rust. Simpler syntax, respects .gitignore, supports regex and glob patterns, parallel execution, and coloured output.
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" |
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)
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)
{}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)
--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.
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.