fd vs find: finding files without the flag archaeology

Examples checked with fd 10.2.0 on Debian 13. We attempt to test on all versions, but older releases behave differently. See "Versions differ" below.

find has shipped with Unix systems for decades. It’s on every server you’ll ever log into, it can test almost any file attribute, and its syntax feels like it came from a different century. fd is a newer tool that covers the most common part of find’s job with shorter commands and friendlier defaults. This page explains what fd does differently, where those differences help or hurt, and when you should still type find.

1. What problem fd solves

Most file searches are simple: "where is that config file?", "every Markdown file in this repo", "what changed in the last day?". With find, even simple questions need careful syntax. The path comes first, name patterns need quoting, case-insensitive matching is a separate flag (-iname), and running a command on each result means remembering -exec … {} \; or {} +.

fd changes the defaults:

  • The pattern comes first and the path is optional. fd config searches the current directory, recursively, for names containing "config".
  • The pattern is a regular expression, not a shell glob. This catches most people out, so there’s more on it below.
  • Smart case. A lower-case pattern matches case-insensitively. As soon as the pattern contains an upper-case letter, matching becomes case-sensitive.
  • It skips hidden files and anything your .gitignore excludes, so node_modules/ and build output don’t bury the results.
  • Output is colored using your LS_COLORS, and directories are searched in parallel.

The fd README documents these defaults and includes a benchmark that it describes as "one particular benchmark on one particular machine". Don’t expect a specific speed-up on yours. If speed matters to you, measure it on your own tree with hyperfine.

2. Installing it

SystemCommandBinary name
Debian / Ubuntusudo apt install fd-findfdfind
Fedorasudo dnf install fd-find (from the fd README, not tested)fd
Archsudo pacman -S fd (from the fd README, not tested)fd
macOS (Homebrew)brew install fd (from the fd README, not tested)fd

Why it’s called fdfind on Debian and Ubuntu

On Debian and Ubuntu the name /usr/bin/fd already belongs to another package, fdclone (a clone of the FD file manager), so the fd package installs its binary as fdfind. The fix is a symlink in your personal bin directory:

mkdir -p ~/.local/bin
ln -s "$(command -v fdfind)" ~/.local/bin/fd

Use a symlink rather than alias fd=fdfind. Aliases only exist in your interactive shell, so scripts and cron jobs don’t see them.

~/.local/bin also has to be on your PATH. On Debian and Ubuntu, the default ~/.profile adds it, but only at login and only if the directory already existed when you logged in. After creating it, either log out and back in, or add it to the current shell yourself:

export PATH="$HOME/.local/bin:$PATH"

zsh doesn’t read ~/.profile. If zsh is your login shell, put that export line in ~/.zshrc (or ~/.zprofile).

In a script that has to run on several distributions, don’t rely on either name being present. Check for both:

FD=$(command -v fd || command -v fdfind) || { echo "fd not installed" >&2; exit 1; }
"$FD" -e conf . /etc

3. fd and find side by side

Taskfindfd
Names containing "config"find . -iname '*config*'fd config
All .log files (any case)find . -iname '*.log'fd -e log
Only regular filesfind . -type ffd -t f
Only directories, one level downfind . -mindepth 1 -maxdepth 1 -type dfd -t d -d 1
Changed in the last 24 hoursfind . -mmin -1440fd --changed-within 24h
Empty files and directoriesfind . -emptyfd -t e
Empty regular files onlyfind . -type f -emptyfd -t e -t f
Run one command with all resultsfind . -name '*.txt' -exec wc -c {} +fd -e txt -X wc -c
Run a command per resultfind . -name '*.txt' -exec wc -c {} \;fd -e txt -x wc -c
Include hidden and ignored files(that’s find’s default)fd -u … (or -H / -I)

A few notes on the table:

  • -e ignores case. fd -e log also finds My File.LOG. That’s why the find equivalent uses -iname, not -name.
  • The defaults differ. find searches everything, including .git/, hidden directories, and node_modules/. fd skips hidden and git-ignored paths. When you compare their output, add -u to fd, or you aren’t comparing like with like.
  • -x runs in parallel. fd’s -x starts commands for several results at once. That’s fine for independent jobs such as checksums or image conversion. For anything that writes to a shared file or must run in order, use -X (one batch), or limit fd to one job at a time. Note that -j 1 has to come before -x, because everything after -x is treated as part of the command: fd -j 1 -e txt -x cmd.

4. Workflows that are actually useful

Find a file when you only remember part of the name.

fd nginx /etc

Searches /etc for anything with "nginx" in its name. Add -t f to skip directories.

List what you touched today in a project.

fd -t f --changed-within 24h

Durations like 2d, 12h, and 30min work, and so do dates. This replaces find -mmin arithmetic and -newermt.

Act on files whose names contain spaces, safely.

fd -0 -e txt | xargs -0 wc -c

-0 separates results with a NUL byte, which xargs -0 reads, so file names with spaces or even newlines get through intact. fd’s own -x and -X pass each path as a separate argument, so they’re safe too.

Convert images using placeholders.

fd -e jpg -x magick {} {.}.png

(ImageMagick command from its documentation, not tested.) {} is the full path, {.} the path without its extension, {/} the file name, {//} the parent directory, and {/.} the file name without its extension. Running fd main.rs -x echo '{} {/} {//} {.} {/.}' printed ./src/main.rs main.rs ./src src/main main. Quote the placeholders if your shell treats braces specially.

ImageMagick 7 renamed its main command to magick. The old convert name still works on many systems but is deprecated. If you’re on ImageMagick 6, which some older distributions still ship, magick won’t exist and you need convert {} {.}.png instead. magick -version tells you which one you have.

Feed fzf. Adapted from the fd and fzf READMEs (not tested):

export FZF_DEFAULT_COMMAND='fd --type file --follow --hidden --exclude .git'
export FZF_CTRL_T_COMMAND="$FZF_DEFAULT_COMMAND"

This lists files the way fd sees them: symlinks followed, hidden files included, .git and git-ignored paths left out. Two things have to be in place for it to work:

  • The fd name must resolve. On Debian and Ubuntu, create the fdfind symlink from section 2 first. Otherwise fzf runs a command that doesn’t exist and shows an empty list.
  • FZF_CTRL_T_COMMAND only matters once fzf’s key bindings are loaded. With fzf 0.48 or newer, add one line to your shell’s config: bash eval "$(fzf --bash)", zsh source <(fzf --zsh), fish fzf --fish | source. Older packaged versions, such as fzf 0.38 on Debian 12 and 0.44 on Ubuntu 24.04, don’t have those options. There, use the script shipped with the package, e.g. source /usr/share/doc/fzf/examples/key-bindings.bash for bash.

5. Pitfalls

"regex parse error" on a pattern that looks fine. fd patterns are regular expressions. In a regex, * means "repeat the previous thing", so a leading * is an error:

output
$ fd '*.log'
[fd error]: regex parse error:
    *.log
    ^
error: repetition operator missing expression

To find log files, use fd -e log (add -u to include hidden and ignored ones: fd -u -e log), or a real regex such as fd '\.log$'. Always single-quote patterns so the shell doesn’t expand them first.

Glob mode matches the whole name. -g switches to shell-style globs, but a glob has to match the entire file name. fd -g '*.log' works, while fd -g log only finds files named exactly log. A glob that contains a / is matched against the path, which needs -p as well. -p compares against the full absolute path, so start the glob with **/: fd -p -g '**/src/*.log' works, but fd -p -g 'src/*.log' finds nothing. Note that * doesn’t cross directory separators, so that pattern only matches files sitting directly in src/. To include nested folders, use fd -p -g '**/src/**/*.log'.

"fd doesn’t find my file." It’s nearly always the defaults. The file is hidden (add -H), git-ignored (add -I), or both (-u on fd 8.4.0 and newer). fd also matches only the file name by default. To match against the whole path, like find -path, add -p: fd -p 'src/.*\.rs$'.

Case surprises. fd readme finds README.md, but fd Readme doesn’t, because the capital letter turns on case-sensitive matching. Force the behaviour you want with -i (ignore case) or -s (case-sensitive).

-H shows the inside of .git/. On fd 10.x (and before 9.0.0), -H lists everything inside .git/. In a small test repository that was dozens of files. Exclude it with fd -H -E .git. For a permanent exclude, put patterns in a .fdignore file at the top of the project. According to the fd README, a global ignore file at ~/.config/fd/ignore applies to every search (from the fd docs, not tested).

Exit codes in scripts. On 10.2.0, a search with no results still exits 0, so if fd …; then won’t tell you whether anything matched. Use -q (--quiet). It prints nothing and exits 0 if there was a match and 1 if there wasn’t.

if fd -q -e orig; then echo "leftover merge files (*.orig) in this tree"; fi

Patterns that start with a dash are read as options. Put -- before them: fd -- '-weird'.

Deleting. fd can feed rm, but always run the search on its own first and add the action afterwards. -X rm -i asks before each file:

fd -H '^\.DS_Store$' -t f            # look first
fd -H '^\.DS_Store$' -t f -X rm -i   # then delete, with confirmation

6. When to use find instead

  • Portability. find is on every POSIX system, including minimal containers, rescue shells, and old servers. If a script has to run anywhere, write it with find.
  • Tests fd doesn’t have. Permission bits (-perm), link counts (-links), inode numbers, -newer compared with another file, and complex -o/-a boolean expressions are find territory. fd covers type, size (-S), owner (-o), and modification time, but not the long tail.
  • Searching everything on purpose. Audits, forensics, or cleaning up a whole filesystem, where skipping hidden or ignored files would hide exactly what you’re looking for. fd -u gets you most of the way, but find makes "everything" the default, and that suits the job.

For everyday interactive searching in projects and home directories, fd is shorter to type and harder to get wrong. Keep find for portable scripts and for the questions fd can’t ask.

  • ripgrep: fd finds files by name, ripgrep searches inside them. fd -e py -X rg 'TODO' combines the two.
  • fzf: interactive picking on top of fd’s output.

Sources

Updated 2026-10-24: rewritten. Fixed an example (fd -u '*.log', now fd -u -e log) that failed with a regex error.