find, xargs, and the -print0 dance

general / published / guide #35 / ≈ 4 min read

find walks a directory tree and prints out paths that match what you asked for. xargs takes a list of paths and runs a command on each one. Together they're the "do something to every matching file" pair that comes up in shell scripts everywhere.

This guide is the patient introduction — with the obligatory section on -print0, because the moment you have a filename with a space in it, the naive version of these commands silently does the wrong thing.

find: the basic shape

find <where> [conditions] [actions]

Simplest possible call:

find .

That prints every file and directory under the current directory, recursively. Now add a condition:

find . -name "*.log"

Only files whose name matches *.log. The quotes are important — they stop your shell from expanding the * before find sees it.

The most useful conditions

find . -name "*.txt"               # exact glob match, case-sensitive
find . -iname "*.TXT"              # case-insensitive
find . -type f                     # only regular files
find . -type d                     # only directories
find . -type l                     # only symlinks
find . -size +100M                 # bigger than 100 MB
find . -size -1k                   # smaller than 1 kB
find . -mtime -7                   # modified in the last 7 days
find . -mtime +30                  # not modified for more than 30 days
find . -mmin -10                   # modified in the last 10 minutes
find . -newer somefile             # newer than a reference file
find . -user alice                 # owned by alice
find . -perm 644                   # exact permissions
find . -empty                      # zero bytes / empty directory

You can combine them — they're ANDed by default:

find . -type f -name "*.log" -size +10M

For OR, use -o and group with escaped parens:

find . ( -name "*.tmp" -o -name "*.bak" )

Actions: doing something with what you found

By default find prints the matches. You can also act on them:

find . -name "*.log" -delete                # delete each one
find . -name "*.log" -print                 # explicit print (the default)
find . -name "*.log" -ls                    # ls -l style listing
find . -name "*.log" -exec wc -l {} ;      # run wc -l on each (one call per file)
find . -name "*.log" -exec rm {} +          # run rm on as many as fit on a line

About -exec:

  • {} is the placeholder for the filename.
  • ; ends the command, one execution per file.
  • + ends the command, gathering many filenames into a single execution. Much faster on big trees.

Why xargs exists

Sometimes you want to do something fancier than -exec allows — pipe through other tools, use a command that doesn't take filenames last. xargs reads things from stdin and turns them into command-line arguments.

find . -name "*.log" | xargs wc -l

Reads file paths from find, feeds them as arguments to wc -l. Roughly equivalent to find . -name "*.log" -exec wc -l {} +.

The space-in-filename problem (read this twice)

What happens when you have a file called My Holiday Photos.zip?

find . -name "*.zip" | xargs ls

find prints ./My Holiday Photos.zip. xargs sees it as three separate arguments: My, Holiday, and Photos.zip. It runs ls My Holiday Photos.zip and complains that those don't exist.

The fix: tell find to separate paths with null bytes (a character that can't appear in a filename), and tell xargs to expect that:

find . -name "*.zip" -print0 | xargs -0 ls

-print0 on find, -0 on xargs. Always use this pair when piping between them. Always.

If you're using -exec directly inside find, you're fine — that doesn't go through the shell's word-splitting.

Useful xargs tricks

# run in parallel — 4 at a time
find . -name "*.png" -print0 | xargs -0 -P 4 -I{} convert {} -resize 50% {}-small.png

# preview before running — show the command, don't execute
find . -name "*.tmp" -print0 | xargs -0 -p rm

# stop early if any invocation fails
find . -name "*.sh" -print0 | xargs -0 -I{} sh -c 'shellcheck {} || exit 255'
  • -P 4 — run up to 4 in parallel.
  • -I{} — use {} as the placeholder (instead of appending at the end). Lets you put the filename in the middle of the command.
  • -p — ask before each execution.

Realistic one-liners

# find the 10 biggest files under /var
find /var -type f -printf '%s %p
' | sort -rn | head -10

# delete log files older than 30 days
find /var/log -name "*.log" -mtime +30 -delete

# fix permissions: all files 644, all directories 755
find . -type f -exec chmod 644 {} +
find . -type d -exec chmod 755 {} +

# count lines across every .py file
find . -name "*.py" -print0 | xargs -0 wc -l

# every git repo under your code folder
find ~/code -name ".git" -type d -prune -print

The TL;DR for safety

  1. Always quote glob patterns: find . -name "*.log", not find . -name *.log.
  2. If piping find → xargs, use -print0 and -0.
  3. For destructive actions, run with -print first to inspect the list. Only swap to -delete after.

← back to index