Why find, and not ls and your eyes
"Which files are older than 30 days?", "is anything world-writable?", "delete the old session files but keep this week's" - questions about thousands of files at once. find answers them precisely, and it is also the tool that deletes them, so it pays to know exactly how it reads what you type.
What you need to know already: 4.5 (modes), 4.9 (find -perm -4000), 4.3 (userdel leaves files with a bare UID), 1.7 (pipes).
The shape of every find
find [start points...] [expression]
find /var/log -type f -name '*.gz' -mtime +30 -print
└──┬───┘ └──────────────┬──────────────┘ └─┬──┘
where tests, ANDed together action
find walks every entry under each start point (the directories you name) and evaluates the expression against it, left to right. Tests are true or false; actions (-print, -delete, -exec) do something and are also true or false. With no action at all, find adds -print for you.
Tests you will use every week
-type f / d / l regular file / directory / symlink
-name '*.log' the NAME matches a glob (case-sensitive; -iname ignores case)
-path '*/cache/*' the whole PATH matches a glob
-size +100M bigger than 100 MiB (c bytes, k KiB, M MiB, G GiB)
-mtime +7 modified more than 7 days ago
-mtime -1 modified less than 1 day ago
-mmin -30 in the last 30 minutes
-newer ref modified more recently than the file ref
-user appuser / -group ops
-nouser / -nogroup owned by a UID/GID with no name - leftovers of a userdel
-perm ... mode tests, below
-empty empty file or empty directory
-links +1 more than one hard link
A glob is a shell-style pattern: * any characters, ? one character. -maxdepth 1 means "do not go deeper than the start directory itself"; -printf '%s %p\n' prints size in bytes and path (more under Actions); sort -rn sorts numerically, biggest first.
$ find /etc -maxdepth 1 -name 'host*'
/etc/hostname
/etc/hosts
$ sudo find /var/log -type f -printf '%s %p\n' | sort -rn | head -3
45837 /var/log/nginx/access.log
3292 /var/log/nginx/error.log
190 /var/log/apt/history.log
Quote the glob. Always.
The shell expands an unquoted *.conf before find runs. In a directory with no .conf files it happens to pass through untouched, which is why this bug hides for months:
$ mkdir -p /tmp/g && touch /tmp/g/a.conf /tmp/g/b.conf
$ cd /tmp/g && ls
a.conf b.conf
$ find /etc -name *.conf
find: paths must precede expression: `b.conf'
find: possible unquoted pattern after predicate `-name'?
(cd /tmp/g && ls: && runs the second command only if the first worked, 1.7.) bash turned it into find /etc -name a.conf b.conf. With exactly one match it is worse: find /etc -name a.conf runs, succeeds, and silently searches for the wrong thing. -name '*.conf' with quotes, every time.
-size and -mtime round, and that bites
find rounds up to the unit before comparing. -size -1M means "rounds up to less than 1 MiB" - and every file from 1 byte to 1 MiB rounds up to 1, so -size -1M only matches empty files. For small thresholds, use a smaller unit:
-size -1M only empty files (surprise)
-size -1024k under 1 MiB, what you meant
-size +1G strictly bigger than 1 GiB
-mtime counts whole 24-hour periods, discarding the fraction:
-mtime 0 modified within the last 24 hours
-mtime +7 at least 8 full days ago (age of 7 days 23 hours does NOT match)
-mtime -7 less than 7 days ago
That off-by-one is why retention jobs written as -mtime +7 keep eight days of files. It is fine once you know it; write -mtime +6 if you mean "older than a week" and say so in a comment.
-perm: three forms
-perm 644 mode is EXACTLY 644
-perm -4000 ALL of these bits are set (setuid, whatever else)
-perm -o+w same thing, symbolic (world-writable)
-perm /111 ANY of these bits is set (executable by someone)
The audit one-liners:
sudo find / -xdev -perm -4000 -type f setuid binaries
sudo find / -xdev -perm -o+w -type f world-writable files
sudo find / -xdev \( -nouser -o -nogroup \) orphaned ownership
Operators, and the precedence trap
Tests next to each other are joined by an implicit -a (AND). -o is OR, ! or -not negates, and \( \) groups (escaped, because the shell owns parentheses). AND binds tighter than OR:
$ find /etc -maxdepth 1 -type d -name 's*' -o -name 'host*'
/etc/hostname <- files! -type d only applied to the left side
/etc/hosts
/etc/ssh
/etc/sudoers.d
...
$ find /etc -maxdepth 1 -type d \( -name 's*' -o -name 'host*' \)
/etc/ssh
/etc/sudoers.d
...
The first reads as (-type d AND -name s*) OR (-name host*). When you mix -o with anything else, parenthesise.
Global options go first
-maxdepth, -mindepth and -xdev are not tests - they change the walk. Put them after a test and find warns (and applies them anyway):
$ find /etc -name '*.conf' -maxdepth 1
find: warning: you have specified the global option -maxdepth after the argument -name, but global options are not positional, ...
-xdev is the one that matters in an incident: without it, find / walks into /proc, /sys, NFS mounts and every other filesystem.
Actions
-print the default
-printf '%s %p\n' custom: %s size, %p path, %f name, %u user, %m octal mode,
%TY-%Tm-%Td modification date
-ls ls -dils style
-delete delete what matched (implies -depth: children before parents)
-exec cmd {} \; run cmd once PER FILE, {} replaced by the path
-exec cmd {} + run cmd with MANY paths at once - much faster
(\; is backslashed so the shell passes ; to find)
$ find /home -name .bashrc -exec ls -l {} \;
-rw-r--r-- 1 learner learner 615 Sep 14 17:43 /home/learner/.bashrc
$ sudo find /var/log -name '*.gz' -mtime +30 -exec rm {} +
Twelve thousand files with \; is twelve thousand processes; with + it is a handful. Use \; only when the command can take one path at a time and you need that.
-delete: print first, then delete
sudo find /srv/cache/sessions -type f -mtime +7 # read the list
sudo find /srv/cache/sessions -type f -mtime +7 | wc -l # how many
sudo find /srv/cache/sessions -type f -mtime +7 -delete # then, and only then
Two ways -delete ruins a day:
- Order.
find . -delete -name '*.tmp'deletes everything - the action runs before the test is ever evaluated. Actions go last. - Start point.
find / tmp -name ... -delete(a stray space) searches/.
Errors, and silencing only the ones you mean
$ find / -name passwd
/usr/bin/passwd
/etc/passwd
find: '/etc/sudoers.d': Permission denied
find: '/var/lib/app': Permission denied
2>/dev/null hides the noise, and also hides real errors. Running as root (sudo find) is usually the better fix. And note that any unreadable directory makes find exit 1 even though it printed results - check that before using find's exit code in a script.
What you can now do
- Build a find expression from a plain-words question (type, name, age, size, mode, owner).
- Avoid the classic traps: unquoted globs,
-owithout parentheses,-deletetoo early. - Preview with
-print, then act with-deleteor-exec ... +.