Mastering `find` and `xargs` for Powerful File Operations

By Eddie Power Aug 20, 2025 in Commands & Scripting 6 min read
bash man-pages

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, like xargs does. 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.

Share this article

E
Eddie Power admin

Linux enthusiast and open source advocate.

Comments (0)

No comments yet. Be the first to leave one!

Leave a Comment

Comments are moderated and will appear after approval.