32blogby StudioMitsu

xargs Practical Guide: Turn Any Output into Commands

Master xargs to chain commands effectively. Covers -0 for safe filenames, -P for parallel execution, -I for placement control, and real-world patterns with find, grep, and curl.

9 min read
On this page

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:

bash
# 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:

bash
# 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:

bash
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:

bash
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):

bash
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:

bash
# 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:

bash
# 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:

bash
# 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:

bash
# 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:

bash
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:

bash
# 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:

bash
# 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?

bash
# 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

bash
# 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

bash
# Add .bak extension to all config files
find /etc/myapp -name "*.conf" -print0 | xargs -0 -I {} mv {} {}.bak

Pattern 3: Parallel image processing

bash
# 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

bash
find /tmp -type f -mtime +30 -print0 | xargs -0 rm -f

Pattern 5: Run a command for each line in a file

bash
# 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

bash
git diff --name-only | grep "\.tsx$" | xargs git add

Pattern 7: Bulk API calls

bash
# 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

bash
# 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

bash
# 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:

bash
# 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:

bash
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:

bash
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 time
  • xargs -I {} — when you need argument placement control
  • xargs -P N -n 1 — parallel execution for CPU-bound tasks
  • xargs -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.