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
- Always quote glob patterns:
find . -name "*.log", notfind . -name *.log. - If piping find → xargs, use
-print0and-0. - For destructive actions, run with
-printfirst to inspect the list. Only swap to-deleteafter.