Linux find and xargs Tutorial
Master find and xargs to locate files and run bulk operations safely. Includes -exec vs xargs, null-delimited pipelines, parallelism, and how to avoid destroying directories.
What you'll learn
- ✓How find evaluates expressions and prunes trees
- ✓When to use -exec vs piping to xargs
- ✓Null-delimited pipelines for filenames with spaces
- ✓Running operations in parallel with xargs -P
- ✓How a missing flag can wipe a directory
Prerequisites
- •Basic familiarity with bash and Unix commands
What and Why
find walks a directory tree and tests each file against expressions you supply. xargs reads items from stdin and turns them into command-line arguments for another program. Together they are the duct tape of the shell: cleanup scripts, bulk renames, log pruning, code searches, mass chmod, and ad-hoc migrations all start here.
People reach for them because they are everywhere, scriptable, and predictable. They also bite hard when filenames contain spaces, newlines, or quotes, which is most of the reason this tutorial exists.
Mental Model
find is a tiny language. Each argument after the starting path is either an option (-maxdepth, -xdev), a test (-name, -mtime, -size), or an action (-print, -delete, -exec). Tests return true or false; expressions combine with implicit AND, or explicit -o (OR) and ! (NOT). The default action is -print.
xargs is the bridge from “lines on stdin” to “arguments on a command line”. It batches inputs into as few command invocations as it can, which is much faster than running one command per file.
The single most important habit: never trust whitespace in filenames. Use find -print0 paired with xargs -0, or pass \; vs + to -exec deliberately.
Hands-on Example
Find all .log files modified more than 7 days ago under /var/log and show their size:
find /var/log -type f -name '*.log' -mtime +7 -printf '%s %p\n'
Delete those logs safely. Two equivalent forms:
# Using -exec with + batches calls like xargs
find /var/log -type f -name '*.log' -mtime +7 -exec rm -f {} +
# Using xargs with null delimiters
find /var/log -type f -name '*.log' -mtime +7 -print0 \
| xargs -0 rm -f
Convert a tree of PNGs to WebP in parallel using all cores:
find ./images -type f -name '*.png' -print0 \
| xargs -0 -P "$(nproc)" -I {} cwebp -q 80 {} -o {}.webp
Search for a string in source files but skip node_modules and .git:
find . \( -path ./node_modules -o -path ./.git \) -prune \
-o -type f -name '*.ts' -print0 \
| xargs -0 grep -nH 'TODO'
find . -type f -name '*.log' -print0
|
v stream of NUL-terminated paths
|
xargs -0 -P 4 -n 50 gzip
|
v builds command lines:
gzip file1 file2 ... file50 <- worker 1
gzip file51 ... file100 <- worker 2
gzip file101 ... file150 <- worker 3
gzip file151 ... file200 <- worker 4 -exec ... + already batches like xargs. Use xargs when you need parallelism (-P), a custom replacement string (-I), or to feed a pipeline.
Common Pitfalls
- Spaces and newlines in filenames:
find ... | xargs rmwill tear apartMy Photo.jpgintoMyandPhoto.jpg. Always pair-print0with-0. -execwithout quoting:-exec mv {} /tmp \;is fine;-exec sh -c 'mv $1 /tmp' _ {} \;is required if you need shell features.- Forgetting
-type f:find . -name 'build'matches directories too, andrm -rflater regrets it. - Pruning order:
-prunemust come before-o -print, and the parenthesized group needs escaping. xargswith empty input: by default it still runs the command once with no args. Pass-r(GNU) or--no-run-if-emptyto skip.- Cross-mounting:
find /happily descends into NFS or/proc. Use-xdevto stay on one filesystem.
Practical Tips
- Build commands in stages: replace
-deleteorrmwith-printfirst, eyeball the list, then swap back. -mtime,-mmin,-newer, and-sizeaccept ranges; combine them to scope dangerous operations.- Use
-I {}only when you must; it forces one invocation per item and is slow on large sets. - For complex matches, prefer
fdandripgrepfor daily use, but keepfindandxargsin your toolbox for portability. - Pipe through
sort -z | uniq -zwhen you need deduplication with null delimiters.
Wrap-up
find plus xargs is the ultimate Swiss army knife: a tiny language for selecting files and a tiny language for running commands on them. Treat filenames as untrusted bytes, use null-delimited pipelines, and dry-run anything destructive. Get those habits in and you will reach for these tools without flinching.
Related articles
- Linux 20 Essential Linux Commands Every Developer Should Know
Twenty commands that cover the overwhelming majority of day-to-day Linux work — navigation, file management, inspection, search, and processes — with runnable examples for each.
- Linux The Linux Terminal: A Beginner's Guide
Open a terminal, read the prompt, and learn the handful of conventions that make every command-line tool consistent. The foundation for everything else you will do on Linux.
- Linux Linux cgroups Explained: How Containers Get Their Limits
A practical introduction to Linux control groups. Learn what cgroups do, how v1 and v2 differ, and how Docker and Kubernetes use them to cap CPU and memory.
- Linux Linux Cron and systemd Timers: A Practical Comparison
Run scheduled jobs on Linux with cron or systemd timers. How they differ, when to choose each, and recipes that survive reboots and log rotations.