xargs reads items from standard input and executes a command with those items as arguments. It's the glue between commands that produce output and commands that need arguments — and once you internalize a few patterns, you'll reach for it constantly.
In short: command1 | xargs command2 takes the output of command1 and passes it as arguments to command2. The key options are -0 for safe filename handling, -I {} for argument placement, -n for batching, and -P for parallelism.
Why xargs Exists
Some commands accept input from stdin naturally — grep, sort, wc. Others don't. Try piping a list of files to rm:
# This does NOT work — rm doesn't read from stdin
find . -name "*.tmp" | rm
rm expects arguments, not stdin. That's where xargs comes in:
# This works — xargs converts stdin to arguments
find . -name "*.tmp" | xargs rm
xargs takes each line (or whitespace-separated token) from stdin and appends it as arguments to the command you specify. Simple concept, but the implications are powerful.
The Basics: How xargs Splits Input
By default, xargs splits input on whitespace (spaces, tabs, newlines) and passes everything as arguments in one batch:
echo "file1.txt file2.txt file3.txt" | xargs touch
# Equivalent to: touch file1.txt file2.txt file3.txt
You can control how many arguments go to each invocation with -n:
echo "a b c d e f" | xargs -n 2 echo
# echo a b
# echo c d
# echo e f
And you can see exactly what xargs is executing with -t (trace mode):
echo "file1 file2" | xargs -t rm
# rm file1 file2 ← printed to stderr before execution
Handling Filenames with Spaces: -0 and -print0
This is the single most important xargs pattern. Default whitespace splitting breaks on filenames containing spaces:
# BROKEN: "my file.txt" becomes two arguments: "my" and "file.txt"
find . -name "*.txt" | xargs rm
The fix is null-byte delimiting — use find -print0 paired with xargs -0:
# SAFE: null bytes separate filenames, spaces are preserved
find . -name "*.txt" -print0 | xargs -0 rm
Null bytes (\0) cannot appear in filenames (POSIX guarantee), making them the only truly safe delimiter. Always use -print0 | xargs -0 when processing filenames from find.
If your input isn't from find, you can set a custom delimiter with -d:
# Split on newlines instead of all whitespace
echo -e "path with spaces\nanother path" | xargs -d '\n' ls -la
Controlling Argument Placement: -I
By default, xargs appends arguments at the end of the command. When you need them somewhere else, use -I:
# Rename each .log file to .log.bak
find /var/log -name "*.log" -print0 | xargs -0 -I {} cp {} {}.bak
-I {} tells xargs to replace every {} in the command with the current input item. It also implies -n 1 — each item is processed individually.
A practical example — downloading a list of URLs:
cat urls.txt | xargs -I {} curl -sS -o /dev/null -w "%{http_code} {}\n" {}
# 200 https://example.com
# 404 https://example.com/broken
Parallel Execution: -P
xargs can run multiple processes simultaneously with -P:
# Compress 4 files at a time in parallel
find . -name "*.log" -print0 | xargs -0 -P 4 -n 1 gzip
-P 4 means up to 4 concurrent processes. -n 1 ensures each process gets one file. Without -n 1, xargs might batch all files into a single gzip call, defeating the parallelism.
Use -P 0 to let xargs run as many processes as possible:
# Convert all PNGs to WebP using all available cores
find images/ -name "*.png" -print0 | xargs -0 -P 0 -I {} \
cwebp -q 80 {} -o {}.webp
xargs -P vs find -exec
A common question: when to use find -exec vs find | xargs?
# find -exec: spawns a new process for EVERY file
find . -name "*.tmp" -exec rm {} \;
# find -exec +: batches like xargs (preferred over \;)
find . -name "*.tmp" -exec rm {} +
# xargs: batches by default, supports -P for parallelism
find . -name "*.tmp" -print0 | xargs -0 rm
find -exec {} + and xargs are similar in performance for serial execution. But when you need parallel execution, only xargs offers -P. For CPU-bound tasks (compression, image conversion), -P makes a real difference.
Real-World Patterns
Pattern 1: Find and grep across files
# Search for "TODO" in all Python files
find . -name "*.py" -print0 | xargs -0 grep -n "TODO"
This is faster than find -exec grep because xargs batches filenames into fewer grep invocations.
Pattern 2: Batch rename files
# Add .bak extension to all config files
find /etc/myapp -name "*.conf" -print0 | xargs -0 -I {} mv {} {}.bak
Pattern 3: Parallel image processing
# Resize all JPEGs to max 1920px width, 8 at a time
find photos/ -name "*.jpg" -print0 | \
xargs -0 -P 8 -I {} convert {} -resize "1920>" {}
Pattern 4: Delete files older than 30 days
find /tmp -type f -mtime +30 -print0 | xargs -0 rm -f
Pattern 5: Run a command for each line in a file
# Ping each host in a list (3 at a time)
cat hosts.txt | xargs -P 3 -I {} ping -c 1 -W 2 {}
Pattern 6: Git — stage files matching a pattern
git diff --name-only | grep "\.tsx$" | xargs git add
Pattern 7: Bulk API calls
# Check HTTP status of URLs from a file (10 concurrent)
cat endpoints.txt | xargs -P 10 -I {} \
curl -sS -o /dev/null -w "%{http_code} {}\n" {}
Common Mistakes
Forgetting -0 with find
# Wrong — breaks on spaces in filenames
find . -name "*.txt" | xargs wc -l
# Right
find . -name "*.txt" -print0 | xargs -0 wc -l
Using -I when not needed
# Slow — processes one file at a time
find . -name "*.log" | xargs -I {} gzip {}
# Fast — batches all files into one gzip call
find . -name "*.log" -print0 | xargs -0 gzip
Ignoring ARG_MAX limits
Extremely long file lists can exceed the OS argument limit. xargs handles this automatically by splitting into multiple invocations — but only if you're not using -I (which already implies -n 1). For very large file lists, xargs is actually safer than manual $() expansion:
# Can fail with "Argument list too long"
rm $(find . -name "*.tmp")
# Safe — xargs auto-splits if needed
find . -name "*.tmp" -print0 | xargs -0 rm
FAQ
What does xargs do that pipes can't?
Pipes send stdout to stdin. But many commands (rm, mv, cp, mkdir, chmod) expect arguments, not stdin input. xargs bridges that gap by converting stdin into command-line arguments.
When should I use xargs -0?
Always when processing filenames from find. The -print0 / -0 combination is the only safe way to handle filenames containing spaces, newlines, or special characters.
How does xargs -P compare to GNU Parallel?
xargs -P is simpler and available everywhere (it's part of findutils). GNU Parallel offers more features: job logging, resume support, remote execution, and progress bars. For simple local parallelism, xargs -P is enough. For complex job distribution, consider GNU Parallel.
Can xargs handle empty input?
By default, xargs still executes the command once with no arguments if stdin is empty. Use --no-run-if-empty (or -r on GNU xargs) to prevent this:
find . -name "*.nonexistent" -print0 | xargs -0 --no-run-if-empty rm
Should I use find -exec or find | xargs?
Use find -exec {} + for simple cases — it batches like xargs with less syntax. Use find | xargs when you need -P (parallelism), -n (custom batching), or when chaining with other filters between find and the final command.
Does xargs work on macOS?
Yes, but macOS ships BSD xargs which has slightly different flags. Notable difference: BSD xargs requires -0 with no space before arguments, and --no-run-if-empty is the default behavior. Most patterns in this article work on both.
How do I debug what xargs is running?
Use -t to print each command before execution, or -p to prompt for confirmation. Both are invaluable when building complex pipelines.
Can I use xargs with multiple commands?
Not directly, but you can invoke a shell:
find . -name "*.md" -print0 | xargs -0 -I {} sh -c 'echo "Processing {}"; wc -l {}'
Wrapping Up
xargs is one of those commands that seems simple but dramatically changes how you compose pipelines. The patterns worth memorizing:
find -print0 | xargs -0— safe filename processing, use it every timexargs -I {}— when you need argument placement controlxargs -P N -n 1— parallel execution for CPU-bound tasksxargs -t— debug what's actually running
If you're already using grep and ripgrep and sed/awk in your workflow, xargs is the piece that ties them together. Combine it with fzf for interactive selection, and you've got a toolkit that handles most file processing tasks without leaving the terminal.