You have a list of things — files from find, lines from a file, output from another command — and you want to run something against each of them. Piping does not do it, because most commands read arguments, not standard input:

# does nothing useful: rm reads arguments, not stdin
find . -name '*.tmp' | rm

xargs is the adapter between the two. It reads items from standard input and builds command lines out of them.

find . -name '*.tmp' | xargs rm

That works right up until a filename contains a space, at which point it deletes the wrong things.

The one flag that matters

By default xargs splits on whitespace, so My Holiday Photos.zip becomes three arguments. Paired with rm, that is a command trying to delete a file called My, one called Holiday, and one called Photos.zip. It also treats quotes and backslashes as syntax, which mangles anything containing them. The fix is to separate items with a null byte instead — the one character a filename cannot contain: find -print0 paired with xargs -0. Use that pairing every single time, not only when you expect awkward names.

find . -name '*.tmp' -print0 | xargs -0 rm

Other producers have their own null-output flag: grep -lZ, sort -z, rg --null, fd -0. If a command has no such option, its output is not safe to pipe into xargs without thinking about what could be in it.

Why xargs exists at all

The other half of its job is the Argument list too long error. There is a kernel limit on the total size of a command line, and a glob that expands to fifty thousand filenames exceeds it:

rm /var/spool/old/*          # bash: /usr/bin/rm: Argument list too long

find /var/spool/old -type f -print0 | xargs -0 rm

xargs batches automatically: it fills each command line to just under the limit, runs it, and starts another. Fifty thousand files might become four rm calls. This is why it is faster than a shell loop as well as safer — four processes instead of fifty thousand.

Controlling how it builds the command

FlagDoes
-0Input items are null-separated
-n 1One item per command run, rather than as many as fit
-I {}Substitute each item at {} — implies one at a time
-P 4Run four commands at once
-rDo not run at all if input is empty (GNU; default on BSD)
-pAsk before each command
-tPrint each command as it runs

-I {} is what you need when the item is not the last argument:

# wrong: puts every filename after the destination
find . -name '*.log' | xargs cp /backup/

# right
find . -name '*.log' -print0 | xargs -0 -I {} cp {} /backup/

# rename, using the item twice
find . -name '*.txt' -print0 | xargs -0 -I {} mv {} {}.bak

Note the trade-off: -I runs the command once per item, so you lose the batching that made xargs fast. For a hundred files that does not matter; for a hundred thousand it does.

-r is worth adding to anything in a script. Without it, empty input still runs the command once with no arguments, and xargs rm with nothing to delete is harmless but xargs docker rm with nothing to remove prints a confusing error at 3am.

Look before you leap

Before running anything destructive, swap the real command for echo and read what it would have done:

find . -name '*.tmp' -print0 | xargs -0 echo rm

# or keep the command and confirm each one
find . -name '*.tmp' -print0 | xargs -0 -p rm

That habit costs two seconds and has saved a great many home directories.

Running things in parallel

# four at a time
find . -name '*.png' -print0 | xargs -0 -P 4 -I {} optipng {}

# one per CPU core
find . -name '*.wav' -print0 | xargs -0 -P "$(nproc)" -I {} flac {}

-P is a genuine speed-up on anything CPU-bound, and free — no extra tools. The catch is output: several commands write to the same terminal simultaneously and their lines interleave, sometimes mid-line. That is fine when you are ignoring the output and fatal when you are collecting it.

For I/O-bound work, more parallelism is not better. Four concurrent jobs on a spinning disk are usually slower than one, because the head spends its time seeking. Match -P to the resource that is actually the bottleneck.

Where GNU parallel earns its place

parallel does the same job with better ergonomics for the harder cases.

sudo apt install parallel
You wantUse
Batching, and the safe handling of odd filenamesxargs -0
Simple parallelism with output you ignorexargs -0 -P
Parallel output kept in order, one job at a timeparallel
The filename without its extension, or its directoryparallel
Distributing work to other machines over SSHparallel
A progress bar and a resumable job logparallel
# output stays grouped per job, not interleaved
find . -name '*.log' -print0 | parallel -0 grep -c ERROR {}

# the placeholders that save writing basename and dirname
parallel convert {} {.}.webp ::: *.png
parallel echo {/} ::: /some/long/path/*.txt      # basename only

# resume a long run after an interruption
parallel --joblog run.log --resume ./process.sh ::: input/*

::: supplies arguments directly rather than through a pipe, and {.} is the item with its extension removed — the two features people reach for most.

Two practical notes. On Debian and Ubuntu, parallel may be the moreutils version, which is a different and much simpler program; the package above installs GNU parallel and parallel --version tells you which you have. And GNU parallel prints a citation notice on first use — parallel --citation acknowledges it once and silences it.

When not to use either

find can run commands itself, and for the batching case it is simpler:

find . -name '*.tmp' -delete           # no pipe at all
find . -name '*.tmp' -exec rm {} +      # batched, like xargs
find . -name '*.tmp' -exec rm {} \;     # one process per file, slow

-exec ... + is the batched form and needs no null handling at all, because nothing is ever converted to text. If find is already your source of items and you do not need parallelism, prefer it. Reach for xargs when the list comes from somewhere else, or when you want -P.

And if the work per item is complex, a plain loop is more readable than a clever one-liner:

while IFS= read -r -d '' f; do
    echo "processing $f"
done < <(find . -name '*.log' -print0)

Common problems

SymptomCauseFix
Acts on filenames split at spacesWhitespace separation-print0 and -0
unmatched single quoteA quote character in a filenameSame fix
Runs once with no arguments on empty inputDefault behaviourAdd -r
Items appended after the wrong argumentItem is not last-I {}
Suddenly very slow-I disabled batchingDrop -I, or add -P
Parallel output interleaved and unreadableConcurrent writers to one terminalUse parallel, or write per-item files
Interactive command gets no inputstdin is the item list-o (GNU), or restructure
Half the files processed, then it stoppedOne command returned 255xargs aborts on 255 — check that command’s exit codes

Quick reference

find . -name '*.tmp' -print0 | xargs -0 rm            # the safe default
find . -print0 | xargs -0 -r -I {} cp {} /backup/     # item not last
find . -print0 | xargs -0 -P "$(nproc)" -I {} gzip {} # one per core
cat urls.txt | xargs -n 1 -P 8 curl -sO               # batch of one, eight at a time
find . -name '*.tmp' -print0 | xargs -0 echo rm       # dry run

parallel convert {} {.}.webp ::: *.png                # extension stripping
parallel --joblog run.log --resume ./go.sh ::: in/*   # resumable

find . -name '*.tmp' -delete                          # when find can do it alone
find . -name '*.tmp' -exec rm {} +                    # batched, no pipe

Related reading

  • find — the usual source of the list, and -exec in full
  • The text toolkit — sort, uniq, cut and the rest of the pipeline
  • Command line basics — pipes, redirection and quoting
  • fd vs find — a producer with -0 built in
  • Processes — what all those parallel jobs are doing to the machine