find is one of the most capable commands on a Unix system and one of the least pleasant to type. Its syntax predates the conventions everything else follows, which is why the simplest possible search — “find files called config” — needs a path, a predicate and a quoted pattern in a specific order.
find . -type f -name "*config*"
# fd, doing the same thing
fd configThat is the pitch in two lines. Whether it is worth adopting depends on what you use find for.
The short verdict
Use fd when you are looking for something. Interactive searching, where you half-remember a filename and want it now, is where the shorter syntax pays for itself several times a day.
Use find when you are doing something to what you found — and always in scripts. Its predicate system handles permissions, ownership, inode links and complex boolean logic that fd deliberately does not attempt.
The split is cleaner here than with most tool pairs. fd is a better search; find is a better batch operation.
Installing it, and the naming trap
# Debian, Ubuntu - note the package name
sudo apt install fd-find
# Fedora, RHEL, Rocky, Alma
sudo dnf install fd-find
# Arch
sudo pacman -S fd
# macOS
brew install fdOn Debian and Ubuntu the command is installed as fdfind, not fd. The name fd was already taken by an unrelated package, so Debian renamed the binary. Every tutorial you read will say fd and nothing will happen when you type it.
# Confirm what you actually got
which fdfind
# Make a personal link so tutorials work
mkdir -p ~/.local/bin
ln -s $(which fdfind) ~/.local/bin/fd
# Or just alias it, in ~/.bashrc
alias fd=fdfindThe symlink is the better of the two, because an alias only exists in interactive shells — scripts and xargs will not see it. ~/.local/bin is the conventional place for it, as covered in the Linux filesystem. The same trap applies to bat, which Debian installs as batcat for the same reason.
What fd changes
| find | fd | |
|---|---|---|
| Pattern matching | Glob, must be quoted | Regex, substring by default |
| Searches current directory by default | Needs . | Yes |
| Case handling | Case sensitive, or -iname | Smart case |
Respects .gitignore | No | Yes |
| Skips hidden files | No | Yes |
| Parallel | No | Yes |
| Colour output | No | Yes |
| Permission errors | Printed as noise | Silently skipped |
| Filter by owner, permissions, inode | Yes | Limited |
| Complex boolean logic | Yes | No |
| Installed everywhere | Yes | No |
Two of those deserve expanding. Permission errors being suppressed is a bigger quality-of-life change than it sounds — searching from / with find buries real results under hundreds of “Permission denied” lines, which is why every example you have seen ends in 2>/dev/null. fd just does not print them.
And the gitignore behaviour cuts both ways, exactly as it does with ripgrep. Searching a project for a build artefact will come back empty, because build artefacts are precisely what gitignore lists:
# Include ignored files
fd --no-ignore pattern
# Include hidden files
fd --hidden pattern
# Both - the "why can't it find my file" escape hatch
fd -HI patternTranslating what you already know
| You want | find | fd |
|---|---|---|
| Files matching a name | find . -name "*.log" | fd -e log |
| Case insensitive | find . -iname "*readme*" | fd readme |
| Files only | find . -type f | fd -t f |
| Directories only | find . -type d | fd -t d |
| Symlinks only | find . -type l | fd -t l |
| Limit the depth | find . -maxdepth 2 | fd -d 2 |
| Search a specific directory | find /var/log -name "*.gz" | fd -e gz . /var/log |
| Modified in the last day | find . -mtime -1 | fd --changed-within 1d |
| Older than a week | find . -mtime +7 | fd --changed-before 1w |
| Larger than 100 MB | find . -size +100M | fd -S +100M |
| Exclude a directory | find . -path ./node_modules -prune -o -print | fd -E node_modules |
| Run a command on each result | find . -name "*.txt" -exec gzip {} \; | fd -e txt -x gzip |
| Run once with all results | find . -name "*.txt" -exec ls {} + | fd -e txt -X ls |
Note the argument order in the “specific directory” row — fd takes the pattern first and the path second, the opposite of find. fd -e gz . /var/log reads as “extension gz, any name, under /var/log”.
-x and -X are nicer than -exec
find’s -exec needs a trailing \; or + that everyone forgets, and it runs one process per file. fd’s equivalents are shorter and run in parallel:
# Once per file, in parallel across cores
fd -e jpg -x convert {} {.}.png
# Once, with every result as arguments
fd -e log -X rm
# Placeholders fd understands
# {} the full path
# {.} path without the extension
# {/} the filename only
# {//} the parent directoryThe {.} placeholder is genuinely useful and has no find equivalent — converting a directory of images while keeping their base names is a one-liner rather than a shell loop.
A warning that applies to both: anything of the shape fd -X rm or find -exec rm deletes without confirmation. Run the search on its own first, read the list, and only then add the action. Deleting the wrong thing here is fast and total.
Where find is still the right answer
fd covers the common cases. find covers all of them, and the gap shows up in exactly the situations where you are doing systems work rather than looking for a file.
# Everything owned by a user - before deleting their account
sudo find / -user alice -not -path '/proc/*'
# World-writable files, a real security audit
sudo find / -type f -perm -o+w -not -path '/proc/*'
# Files with the setuid bit set
sudo find / -perm -4000 -type f
# Empty directories
find . -type d -empty
# Boolean logic: shell scripts NOT under vendor/
find . -name "*.sh" -not -path "./vendor/*"
# Fix permissions across a tree, directories and files differently
find /var/www -type d -exec chmod 755 {} \;
find /var/www -type f -exec chmod 644 {} \;Those -perm and -user searches have no fd equivalent worth using, and they are the ones that matter when you are auditing a machine or cleaning up after a departed colleague. The permissions pair at the end is a standard step after any file transfer — see migrating a server.
The other reasons are the familiar ones. find is on every machine, including the container with no package manager and the server you are logged into during an incident. It is POSIX-specified, so a script using it behaves the same on Linux, BSD and macOS. And it will not silently skip your build directory because git ignores it.
Same rule as everywhere else in this section: learn find, install fd. Reach for fd when you are hunting; reach for find when you are operating.
Two things worth knowing
fd pairs well with fzf. Piping fd into a fuzzy finder turns “where is that file” into a few keystrokes:
# Pick a file interactively, then open it
fd -t f | fzf | xargs -r $EDITORAnd locate still exists. It searches a database updated nightly rather than the live filesystem, which makes it faster than both for “where on this whole machine is that file” — at the cost of not knowing about anything created since the last update. sudo updatedb refreshes it.
Quick reference
| You want | Command |
|---|---|
| Find by partial name | fd name |
| By extension | fd -e log |
| Directories only | fd -t d name |
| In a specific directory | fd pattern /var/log |
| Include hidden and ignored | fd -HI pattern |
| Changed in the last day | fd --changed-within 1d |
| Bigger than 100 MB | fd -S +100M |
| Run a command per result | fd -e txt -x gzip |
| Exclude a directory | fd -E node_modules pattern |
| Debian: it is called | fdfind |
Related reading
- find — the classic, including the predicates fd cannot match
- ripgrep vs grep — the same trade-off, for file contents
- ncdu and dust vs du — finding what filled the disk
- File permissions — what
-permis actually testing - The Linux filesystem — and where to put your
fdsymlink - Package management — installing it on your distribution
