On this page

find, in depth

The Navigating the Filesystem chapter introduced find in a table: a pattern here, an age there, one -exec line at the bottom. That is enough to copy a working command. This guide is the step past copying, because find is one of the few tools where the order of the words changes what the command does, in one case enough to empty a directory by accident.

Everything here was checked on Ubuntu 26.04 and Fedora 44 (findutils 4.10.0) and Arch (4.11.0), with the same behavior on all three. The command ships with the base system on every one of those distro families, so there is nothing to install before the first example.

One filter chain, left to right

find takes a starting directory and, optionally, a chain of tests and actions. With neither, it lists the starting point and everything under it, recursively. Build this small sandbox to search:

mkdir -p ~/logs/cache ~/logs/old ~/logs/sizes
cd ~/logs
touch app.log cache.log debug.log junk1.tmp junk2.tmp
touch "space test.log"
touch cache/session.log
echo "rotate failed on Tuesday" > notes.md
printf 'x' > sizes/one.txt                 # exactly one byte
touch sizes/empty.txt                      # zero bytes
head -c 1572864 /dev/zero > sizes/big.bin  # 1.5 MiB of zeros
touch -d "1 day ago" mark.txt              # -d sets the file's timestamp
touch -d "9 days ago" old/keep.txt         # aged like the logs, but not a .log
ln -s app.log latest.log                   # a symlink (see chapter 3)

Every example below runs against this directory, and every word after the starting path is a test or an action. Tests chain with an implicit AND: find ~/logs -name "*.tmp" -type f requires both the name and the type to match.

Order matters more than it looks, because find evaluates the chain left to right. Watch what a test placed after an action fails to do:

$ find ~/logs -name "*.tmp" -print -name junk1.tmp
/home/user/logs/junk1.tmp
/home/user/logs/junk2.tmp
$ find ~/logs -name junk1.tmp -print
/home/user/logs/junk1.tmp

The second -name sits after -print, so the printing has already happened by the time find reaches it. A test written after an action never affects that action. That sounds like trivia until you meet -delete, which is an action too, and then it is the most important sentence in this guide. One comfort on the way: -print is the default action, so leaving it out changes nothing.

By name and type

-name matches case-sensitively and -iname ignores case: find ~/logs -iname "*.MD" finds notes.md, where -name "*.MD" comes back empty.

Now the single most common find mistake, staged somewhere the glob actually matches. In a directory containing several .txt files, such as your Documents folder, try the pattern without quotes:

$ find . -name *.txt
find: paths must precede expression: `aa.txt'
find: possible unquoted pattern after predicate `-name'?

Exit status 1, no results. The shell expanded the pattern into the matching filenames before find ever started (the shell essentials chapter covers globbing), handing it extra arguments it never asked for. The annoying part: in a directory with no .txt files at all, the same command works, because an unmatched glob reaches find untouched as a literal pattern. Whether it breaks depends on what happens to be lying around. Quote the pattern, find . -name "*.txt", and the problem disappears.

-type filters by what a thing is: -type f for files, -type d for directories, -type l for symlinks. The sandbox has one symlink, and it shows how strictly find draws these lines by default:

$ find ~/logs -name latest.log -type f
$ find ~/logs -name latest.log -type l
/home/user/logs/latest.log
$ find -L ~/logs -name latest.log -type f
/home/user/logs/latest.log

A symlink to a file is not a file as far as find is concerned, so -type f skips it and -type l catches the link itself. (ln -s, which built it, lives in the files and directories chapter.) -L placed before the starting path (it is an option, not a test) makes find follow links, after which the target matches -type f. After the path, it is an unknown-predicate error instead.

By age and size

Make four of the logs look stale with touch -d "9 days ago" cache.log debug.log old/app.log old/error.log, then search:

$ find ~/logs -name "*.log" -type f -mtime +7
/home/user/logs/cache.log
/home/user/logs/debug.log
/home/user/logs/old/app.log
/home/user/logs/old/error.log

-mtime measures age in whole days and rounds up. -mtime 0 catches the last 24 hours, -mtime 7 catches day seven (a file 7 to 8 days old), and -mtime +7 catches anything older than 7 full days, which is what just matched. The same +/- pattern works in minutes too: -mmin -30 matches files modified less than 30 minutes ago, -mmin +30 files older than that.

-newer FILE finds things modified more recently than FILE. The sandbox has mark.txt from yesterday, so the logs younger than yesterday are:

$ find ~/logs -name "*.log" -newer ~/logs/mark.txt -type f
/home/user/logs/app.log
/home/user/logs/cache/session.log
/home/user/logs/space test.log

Pair it with -type f more often than not: the starting directory carries an mtime too, and when files are created inside it, the directory can appear in -newer results alongside the files. The -type f test keeps the list to files.

Size takes fixed units, and all of them round up:

You write find reads it as
n (bare number) 512-byte blocks
c bytes
k kibibytes (1024 bytes)
M mebibytes (1024 KiB)
G gibibytes (1024 MiB)

The rounding produces results that look wrong until you see the rule:

$ find ~/logs/sizes -type f -size 1M
/home/user/logs/sizes/one.txt
$ find ~/logs/sizes -type f -size +1M
/home/user/logs/sizes/big.bin
$ find ~/logs/sizes -type f -size 2M
/home/user/logs/sizes/big.bin

one.txt is one byte long, and find rounds it up to a full mebibyte, which is generous of it. Anything from 1 byte through exactly 1 MiB matches -size 1M, while +1M means strictly larger. big.bin at 1.5 MiB rounds up again, which is why it answers to -size 2M. The zero-byte empty.txt matches neither, since zero rounds to zero. For empty things specifically there is -empty, which matches empty files and, just as usefully, empty directories.

By owner and permission

-user NAME and -group NAME filter by ownership. On a single-user desktop they mostly confirm what you expect, but on a machine with several accounts they partition results cleanly: find /home -user emil hands you Emil’s files and nobody else’s.

Permissions have three spellings, and the difference between them is the whole game. -perm 644 matches only files that are exactly rw-r--r--. -perm -644 matches files with all of those bits set, so a 755 file qualifies too. -perm /700 matches anything where the owner holds at least one of the three bits.

The classic use is auditing for world-writable files with -0002: all bits in 0002 must be set, and 0002 is the write bit for everyone else. Plant one and see:

$ chmod 666 notes.md
$ find ~/logs -type f -perm -0002
/home/user/logs/notes.md
$ chmod 644 notes.md

Anything this search finds is writable by every account on the machine, which is rarely the intent. If you ever run the audit without -type f, add ! -type l to keep symlinks out of the results: their permissions are always wide open, so each link would match the test.

One warning while we are here. Older scripts sometimes carry -perm +644. That form was removed from GNU findutils years ago and now stops find with an invalid-mode error instead of doing anything useful.

Three options control where find looks rather than what it matches. -maxdepth N stops the descent after N levels, and the pairing -mindepth 1 -maxdepth 1 lists a directory’s own entries, without descending and without listing the directory itself:

$ find ~/logs -mindepth 1 -maxdepth 1 -type d
/home/user/logs/cache
/home/user/logs/old
/home/user/logs/sizes

-prune is stronger: it tells find not to walk into a matched directory at all. It almost always appears in this pairing, where the left side matches what to skip and the right side prints the rest:

$ mkdir -p /tmp/prunedemo/node_modules
$ touch /tmp/prunedemo/node_modules/dep.js
$ find /tmp/prunedemo
/tmp/prunedemo
/tmp/prunedemo/node_modules
/tmp/prunedemo/node_modules/dep.js
$ find /tmp/prunedemo -name node_modules -prune -o -print
/tmp/prunedemo

node_modules and everything under it is gone, and the skipped directory itself is not printed either. Anyone who has waited for a backup to crawl through node_modules will recognize the use case.

One combination to refuse outright: prune and -delete do not mix. Ask find for both and it refuses the job before it starts, prints a warning on stderr, exits with status 1, and deletes nothing, not even files a preview had listed:

$ find /tmp/prunedemo -name node_modules -prune -o -type f -mtime +7 -delete
find: The -delete action automatically turns on -depth, but -prune does nothing when -depth is in effect.  If you want to carry on anyway, just explicitly use the -depth option.

The trap is the warning’s own advice. Add an explicit -depth and the refusal vanishes: prune turns into a no-op, find silently deletes every matching file, including the ones inside node_modules, and exits 0 without a complaint. (Fresh files and non-empty directories survive.) One more reason the preview habit matters: the printed list is what tells you a delete is safe.

-xdev tells find not to descend into directories that sit on another filesystem. The starting point and its own filesystem are always searched. That matters for wide searches, because /proc and friends sit on filesystems of their own (the filesystem layout guide tours them). A whole-tree search makes the difference visible. The hit outside /proc is wherever your distro puts it, and the command also prints Permission denied lines on stderr, more on those in a moment:

$ find / -name uptime
/proc/uptime
/usr/bin/uptime
$ find / -xdev -name uptime
/usr/bin/uptime

With -xdev, the search never leaves the root filesystem, so nothing under /proc appears. For wide searches that is exactly what you want, and the disks and storage chapter has more on what counts as one filesystem.

Wide searches also cross directories you cannot read, and find reports each one on stderr. Stage one with mode 000 (mkdir ~/logs/locked; chmod 000 ~/logs/locked):

$ find ~/logs -name "*.tmp" -type f
find: ‘/home/user/logs/locked’: Permission denied
/home/user/logs/junk1.tmp
/home/user/logs/junk2.tmp
$ find ~/logs -name "*.tmp" -type f 2>/dev/null
/home/user/logs/junk1.tmp
/home/user/logs/junk2.tmp

The wording is identical on Ubuntu, Fedora, and Arch, and 2>/dev/null silences the line. Silencing is not fixing: the directory is still unreadable and find still exits with status 1. Remove the roadblock afterward with rmdir ~/logs/locked. The troubleshooting guide covers Permission denied as a symptom in its own right.

Doing things with results

Printing is the default, but find can hand results to other forms. -ls gives an ls -dils-style listing, one line per match:

$ find ~/logs/sizes -type f -ls
  1185073      4 -rw-r--r--   1 user     user            1 Sep  7 10:31 /home/user/logs/sizes/one.txt
  1185074      0 -rw-r--r--   1 user     user            0 Sep  7 10:31 /home/user/logs/sizes/empty.txt
  1185075   1536 -rw-r--r--   1 user     user      1572864 Sep  7 10:31 /home/user/logs/sizes/big.bin

-exec CMD {} \; runs CMD once per match, with {} replaced by the path. The two trailing forms behave very differently:

$ find ~/logs/sizes -type f -exec echo {} \;
/home/user/logs/sizes/one.txt
/home/user/logs/sizes/empty.txt
/home/user/logs/sizes/big.bin
$ find ~/logs/sizes -type f -exec echo {} +
/home/user/logs/sizes/one.txt /home/user/logs/sizes/empty.txt /home/user/logs/sizes/big.bin

With \; find ran echo three times, once per file. With + it batched all three paths into a single invocation, which pays off on large result sets. A close relative, -execdir, runs the command inside each file’s own directory with {} as ./basename, while plain -exec always works in the starting directory: run find ~/logs/old -name "*.log" -exec pwd \; and pwd answers with the starting directory twice, once per match.

When the command is destructive and you want a say in each case, -ok works like -exec \; but asks first:

$ find ~/logs -name "*.tmp" -ok rm {} \;
< rm ... /home/user/logs/junk1.tmp > ? y
< rm ... /home/user/logs/junk2.tmp > ? n

The ... in the prompt is find’s own abbreviation for the command, not a typo. Answering y removed junk1.tmp, and answering n left junk2.tmp untouched, where it still sits.

Deleting without regrets

-delete removes matched files. The safe way to use it is a ritual: run the command with -print first, read the list, then swap -print for -delete. One subtlety: the tests alone are not what you need to preview. The whole command, in its exact order, is what runs.

Build a throwaway directory in /tmp and copy it, so both orderings get tried:

$ mkdir -p /tmp/trap-demo/sub
$ cd /tmp/trap-demo
$ touch a.txt b.txt data.csv keep.md t1.tmp t2.tmp sub/nested.txt sub/photo.jpg
$ cp -r /tmp/trap-demo /tmp/trap-demo2
$ find .
./a.txt
./b.txt
./data.csv
./keep.md
./sub
./sub/nested.txt
./sub/photo.jpg
./t1.tmp
./t2.tmp

Nine files and directories. Now delete “the txt files”, written the way people naturally write it, action first:

$ find . -delete -name "*.txt"
$ find .
.

Everything is gone: the csv, the markdown file, the nested text file, and the photo inside sub/, the two tmp files, and the sub directory itself, leaving only the starting point. The command ran flawlessly. That is the problem. -delete is an action, and this chain reads left to right, so the deleting happened before the -name test was ever reached and every path matched. The directory vanished too because -delete implies -depth: find processes contents before the directory itself, emptying sub/ and then removing it.

The copy survived for contrast:

$ cd /tmp/trap-demo2
$ find . -name "*.txt" -delete
$ find .
.
./data.csv
./keep.md
./sub
./sub/photo.jpg
./t1.tmp
./t2.tmp

Same files, test first, and only the three .txt files are missing. Tests before actions, always. When you preview, preview the entire command and change only the last word.

Pairing find with other commands

Filenames with spaces are where the obvious plumbing breaks, so stage one outside the sandbox:

$ mkdir /tmp/pipe-demo
$ touch "/tmp/pipe-demo/space test.log" /tmp/pipe-demo/plain.log
$ find /tmp/pipe-demo -name "*.log" -print0 | xargs -0 rm
$ find /tmp/pipe-demo -name "*.log"

A plain pipe would have split space test.log into two arguments, rm would have failed on both halves with exit status 123, and the spaced file itself would have survived untouched. -print0 ends each result with a null byte instead of a newline, and xargs -0 reads them back the same way. The pair is needed when filenames contain spaces or newlines, and harmless otherwise.

For counting rather than acting, pipe to wc -l: find ~/logs -name "*.log" -type f | wc -l answers 7 right now. And when what you are looking for lives inside files rather than in their names, that is grep territory: grep -rl "rotate" ~/logs lists files whose contents mention “rotate”, while find only ever examines names and metadata. The text processing chapter covers grep and the rest of that toolbox.

Capstone — clean out an old log directory

The sandbox now looks like a real log directory: nine-day-old logs at the top level, an old/ folder nobody prunes, a cache/ that belongs to some program, and a scattering of personal files. The goal: remove the stale logs, touch nothing else, and be able to prove it.

First an inventory, leaving cache/ out of the count, since whatever is in there is the program’s own business:

$ find ~/logs -name cache -prune -o -type f -print | wc -l
13

Thirteen files outside the cache. Now the candidates: logs older than a week, files only, printed for inspection:

$ find ~/logs -name "*.log" -type f -mtime +7 -print
/home/user/logs/cache.log
/home/user/logs/debug.log
/home/user/logs/old/app.log
/home/user/logs/old/error.log

Four files, none of them under cache/. The fresh session.log in there is far too young to match, and old/keep.txt fails the name test, so neither appears. This printed list is the complete deletion set, because the preview and the delete differ by one word. Read it once, then swap:

$ find ~/logs -name "*.log" -type f -mtime +7 -delete
$ find ~/logs -name "*.log" -type f
/home/user/logs/app.log
/home/user/logs/cache/session.log
/home/user/logs/space test.log

The four stale logs are gone. Everything else is still standing: the fresh logs, keep.txt, the notes file, the cache, the tmp file that -ok spared earlier. Run the inventory count again and it reports 9. The same ritual scales to system log directories when you have the rights for them, and the filesystem layout guide explains what lives where.

Where you are now

You can read any find command as a left-to-right chain of tests and actions, filter by name, type, age, size, owner, and permission, steer the search with -maxdepth, -prune, and -xdev, and act on results with -ls, -exec, and -ok. The habit that ties it together is the preview ritual, and the memory that saves a directory one day is that -delete is an action, subject to the same left-to-right order as everything else.

For the full option surface, the official references are the ones to keep open: