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

bash
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

bash
fd pattern               # search by name in cwd (regex by default)

Output:

bash
src/main.rs
src/utils/parser.rs
tests/parser_test.rs
bash
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:

lua
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.

bash
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.

bash
fd -t f pattern          # files only
fd -t d pattern          # directories only

Output:

bash
src/components/
src/utils/
tests/fixtures/
docs/api/
bash
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.

bash
fd -e py                 # Python files

Output:

bash
src/main.py
src/utils/helpers.py
tests/test_main.py
tests/test_helpers.py
scripts/deploy.py
bash
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.

bash
fd -H pattern            # include hidden (dot) files

Output:

bash
.github/workflows/ci.yml
.env.example
src/.cache/data.json
.gitignore
bash
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.

bash
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.

bash
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.

bash
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:

bash
  42 src/main.py
  87 src/utils/helpers.py
  31 tests/test_main.py
  18 tests/test_helpers.py
 178 total
bash

# {} placeholders
# {}    full path
# {/}   filename only
# {//}  parent directory
# {.}   path without extension
# {/.}  filename without extension

Output: (none — exits 0 on success)

Practical examples

bash
# 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.

bash
fd pattern --absolute-path      # print absolute paths

Output:

swift
/home/alice/projects/myapp/src/main.rs
/home/alice/projects/myapp/src/utils/parser.rs
/home/alice/projects/myapp/tests/parser_test.rs
bash
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:

bash
.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
bash
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.

bash
fd pattern -E "*.min.js"        # exclude glob
fd pattern -E ".git"            # exclude directory
fd pattern -E "node_modules" -E "dist"  # multiple excludes

Output:

css
src/index.js
src/components/Button.js
src/utils/format.js

fd vs find equivalents

findfd
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 -1fd --changed-within 1d
find . -size +1Mfd -S +1M
find . -name "*.sh" -exec chmod +x {} \;fd -e sh -x chmod +x {}
find . -not -path "./.git/*"fd -E ".git"

fd respects .gitignore, .fdignore, and .ignore files. Add a global ignore file at ~/.config/fd/ignore (same gitignore syntax) to permanently skip things like node_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.

Behaviourfdfind
Hidden files (.foo)hidden by defaultshown
.gitignore / .ignore / .fdignorerespectednot consulted
Pattern is matched againstbasename only (with -p: full path)basename via -name, path via -path
Pattern syntaxregexshell glob
Match anchoringsubstring (not anchored)basename glob (anchored to whole basename)
Case sensitivitysmart-case (lower → insensitive)case-sensitive
Output coloringyes when TTYno
Threadingparallel walksingle-threaded
Symlinksnot followednot followed
Default actionprintprint

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).

bash
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.

bash
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 -g whenever the pattern has braces, stars, or question marks but no regex meta-characters. fd -g "*.{py,rb}" reads more naturally than fd '\.(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."

CodeMeaning
fregular file
ddirectory
lsymbolic link
xexecutable file
eempty file or directory
ssocket
pnamed pipe (FIFO)
bblock device
ccharacter device
bash
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.

FlagWhat it does
(default)skip hidden, respect all ignore files
-H / --hiddeninclude dotfiles
-I / --no-ignoreignore .gitignore, .ignore, .fdignore, global ignore
--no-ignore-vcsignore only .gitignore and .git/info/exclude
--no-ignore-parentdon't search up the tree for ignore files
-u-H -I (hidden + no-ignore)
-uu-H -I --no-ignore-vcs (everything)
bash
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:

text
.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.

text
# ~/.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.

bash
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.

bash
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).

bash
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:

text
./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:

PlaceholderExpands to
{}full path including basename
{/}basename only
{//}parent directory
{.}full path without extension
{/.}basename without extension
bash
# 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.

bash
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:

text
./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.

bash
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

bash
# 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

bash
# 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

bash
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

bash
fd -e md -x $EDITOR
fd -e md | xargs -o $EDITOR        # alternative with xargs

Output: (none — exits 0 on success)

More real-world recipes

bash
# 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

Aspectfdfind
Speed (typical repo)5-10× fasterbaseline
Threadingparallelsingle-threaded
Defaultsdev-friendly (skip hidden/ignore)exhaustive
Pattern typeregex (or -g glob, or -F fixed)glob (-name), path (-path), or regex (-regex)
Case-sensitivitysmart-case defaultcase-sensitive
.gitignore awareyesno
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
Portabilityinstall requiredPOSIX, 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:

  1. ~/.config/fd/ignore (or $XDG_CONFIG_HOME/fd/ignore) — global, applies to every invocation.
  2. .fdignore in any parent directory — per-project, fd-specific. Check it in alongside .gitignore.
  3. .ignore in any parent directory — shared with rg, ag, and other walkers.
  4. .gitignore and .git/info/exclude — VCS rules; disable with --no-ignore-vcs.
  5. Command-line flags — -H, -I, -u, -uu, -E override the above.
text
# ~/.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:

VariableEffect
LS_COLORSPer-extension colours (shared with ls, eza, tree).
NO_COLORDisables all colour output (respected since 9.0; also honoured by --list-details since 10.0).
XDG_CONFIG_HOMEOverrides the location of ~/.config/fd/ignore.

Command-line aliases that pay for themselves:

bash
# ~/.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 / changeSinceWhat it does
--ignore-contain <name>10.4Skip any directory containing an entry with this name (e.g. CACHEDIR.TAG, .no-fd).
--hyperlink10.2Emit OSC 8 terminal hyperlinks so paths are clickable in modern terminals (WezTerm, iTerm2, Kitty, Ghostty).
--format <template>10.1Inline format templates (same {} family as -x) without spawning a process.
--strip-cwd-prefix=<auto|always|never>10.1Control whether leading ./ is stripped from output.
--mindepth10.3Hidden alias for --min-depth.
-t dir10.0Alias for -t directory.
--newer @171400000010.0Unix-epoch seconds accepted in --changed-within / --changed-before (GNU date @%s style).
.git/ no longer auto-ignored under -H10.0Reverts the 9.0 default; pass --no-ignore-vcs or -uu if you still want to skip it.
Up to 13× faster on 1M-file trees9.0Rewritten walker; thread count capped at 64 to avoid lock contention.
--type b / --type c9.0Match block and character device files.

Two flags from this list pay for themselves on day one:

bash
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)

--format is 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

  1. Pattern is regex, not globfd "*.py" is a regex error. Use fd -e py, fd '\.py$', or fd -g "*.py".
  2. Smart-case surprisesfd readme matches Readme.md; fd README does not. Use -s or -i to force a mode when scripting.
  3. Pattern is basename-only by defaultfd 'src/main' finds nothing. Add -p for full-path matching.
  4. .gitignore silently hides results — outside a git repo with no .fdignore, the same query returns more results. If you're searching node_modules, you need -I or -u.
  5. -x runs in parallel — order of output is non-deterministic. Use -X (single batch) or pipe through sort if order matters.
  6. {} is not shell-quoted automatically — if a filename contains a space, fd -x sh -c 'echo {}' is broken. Use fd -x sh -c 'echo "$1"' _ {}.
  7. macOS Ubuntu name mismatch — the Ubuntu/Debian package installs as fdfind; alias it to fd in ~/.bashrc.

fd --type empty (or -t e) plus --type directory (or -t d) finds dangling empty dirs left behind by build tools — pipe to xargs rmdir to clean them up.

Sources