find and xargs are two of the most useful tools on any Linux box. find walks a directory tree and selects files by almost any property; xargs turns a list of names into arguments for another command. Together they handle cleanup jobs, audits and bulk edits that would otherwise need a script.
They also have a few traps, mostly around filenames with spaces. This post is a set of recipes I actually use, all tested with GNU findutils on Debian, plus the reasoning behind each one.
How find expressions work
The basic shape is:
find [where] [tests] [actions]
Tests are joined with an implicit AND and evaluated left to right. If you give no action, -print is assumed. Some everyday tests:
find /var/www -type f -name '*.php' # files by name (quote the pattern!) find . -type d -name node_modules # directories by name find . -iname '*.jpg' # case-insensitive find /etc -type f -newer /etc/hostname # modified more recently than a file find . -type d -empty # empty directories
Always quote patterns like '*.php'. Unquoted, the shell may expand them against the current directory before find ever sees them.
Finding files by age
-mtime counts in 24-hour periods. +30 means "more than 30 days ago", -1 means "within the last day":
find /var/log/myapp -type f -name '*.log' -mtime +30 find ~/projects -type f -mmin -60 # changed in the last hour
The rounding is easy to misread: -mtime +30 actually matches files at least 31 full days old, because fractional days are discarded. For cleanup jobs that's usually what you want anyway.
Finding files by size and permissions
Hunting down what's filling a disk:
find / -xdev -type f -size +100M -exec du -h {} + 2>/dev/null | sort -h
-xdev keeps find on one filesystem so it doesn't wander into /proc or network mounts.
Security audits:
find /var/www -type f -perm -o+w # world-writable files find / -xdev -perm -4000 -type f # setuid binaries find /home -nouser -o -nogroup # files owned by deleted users
World-writable files in a web root are a red flag, so that check is worth running now and then. It fits well alongside the server hardening in Hardening Your SSH Server.
Skipping directories with -prune
To search a project but ignore vendor and node_modules:
find . \( -path ./vendor -o -path ./node_modules \) -prune -o -type f -name '*.php' -print
Read it as: "if the path is vendor or node_modules, prune it (don't descend); otherwise, if it's a PHP file, print it." The explicit -print at the end matters. Without it, the pruned directories get printed too.
Running commands: -exec
find can run a command on what it finds without xargs at all:
find src -name '*.php' -exec grep -l 'TODO' {} +
find . -name '*.sh' -exec chmod +x {} \;
The terminator makes a big difference:
{} \;runs the command once per file. That's simple, but slow for thousands of files.{} +packs as many filenames as fit onto one command line, likexargsdoes. Use it whenever the command accepts multiple files.
Both forms pass filenames safely, spaces and all, because no shell is involved.
Deleting safely
find has a built-in -delete, but always run the same command with -print first and read the output:
find /var/log/myapp -type f -name '*.log' -mtime +30 -print find /var/log/myapp -type f -name '*.log' -mtime +30 -delete
Put -delete last. Since tests are evaluated in order, find . -delete -name '*.log' deletes everything before it gets to the name test.
xargs and the space problem
Here's what goes wrong with the "obvious" pipeline when a directory is called old app:
$ find logs -name '*.log' -mtime +30 | xargs ls ls: cannot access 'logs/old': No such file or directory ls: cannot access 'app/error': No such file or directory ls: cannot access '1.log': No such file or directory
xargs splits on whitespace by default, so one path became three. The fix is to separate names with null bytes, the one character that can't appear in a filename:
find logs -name '*.log' -mtime +30 -print0 | xargs -0 ls -l
Make -print0 | xargs -0 a reflex. Many other tools speak the same format: grep -lZ, sort -z, and git ls-files -z.
Useful xargs options
xargs -0 -r ... # -r: don't run at all if there's no input
xargs -0 -n 1 ... # one argument per command
xargs -I{} cp {} {}.bak # place the argument anywhere (implies one per line)
xargs -0 -P 4 ... # run up to 4 commands in parallel
-r (--no-run-if-empty) is important in scripts. Without it, GNU xargs runs the command once with no arguments when the list is empty, and some commands do surprising things when given no files.
Parallel jobs with -P
-P turns xargs into a simple job runner. For example, compressing images or linting PHP files across 4 CPU cores:
find src -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l find photos -name '*.png' -print0 | xargs -0 -n 1 -P 4 optipng -quiet
If each job needs more than one command, hand the filename to a small inline shell script. Pass it as $1, never by pasting {} into the script text, so odd filenames can't inject commands:
find uploads -name '*.jpg' -print0 |
xargs -0 -n 1 -P 4 sh -c 'convert "$1" -resize 1600x1600\> "resized/${1##*/}"' _
The _ fills $0, so the filename lands in $1.
A few combined recipes
# Total size of all .log files under /var/log
find /var/log -type f -name '*.log' -print0 | du -ch --files0-from=- | tail -1
# Ten most recently modified files in a project
find . -type f -not -path './.git/*' -printf '%T@ %p\n' | sort -nr | head -10
# Fix permissions on a web root: dirs 755, files 644
find /var/www/site -type d -exec chmod 755 {} +
find /var/www/site -type f -exec chmod 644 {} +
The last pair is one I run after copying a site onto a new server. Combined with a proper deploy script (see Writing Robust Bash Scripts), it avoids most "permission denied" surprises.
Conclusion
Three habits cover most of what can go wrong: quote your patterns, use -print0 | xargs -0 (or -exec ... +) for anything involving filenames, and preview with -print before you -delete. Beyond that, find's man page is long but well organised. Search it for -printf when you need custom output.
These snippets work well inside systemd timers for scheduled cleanups; see Systemd Units Explained. And if you'd rather have someone set up and maintain the server behind your website, that's something I do.
Comments (0)
No comments yet. Be the first to leave one!
Leave a Comment