Imagine a teacher with a list of names on a piece of paper who needs to call a helper and say the names out loud. Some helpers listen to the paper being read to them (they read stdin, like grep or sort). Others only understand names said directly to them as instructions (they take arguments, like rm or systemctl). xargs is the person who reads the paper and speaks the names to the second kind of helper: "rm this, this and this".
With -I{} it calls the helper once per name and puts the name exactly where {} is. With -0 the names are separated by an invisible marker that can never be part of a name, so "my report.log" is not misheard as two names.
Why this lesson
You have a list - of files from find, of names from jq - and you want to run a command on each item. But rm, chmod and systemctl do not read their list from a pipe; they want it as arguments on their command line. xargs bridges the two.
What you need to know already: stdin and pipes (1.7); arguments and word splitting (6.6); find and its -exec (4.15); NUL separators (6.8); jq -r (7.11).
Turning input into arguments
Some commands read stdin (grep, sort, wc). Many do not - rm, chmod, systemctl all want arguments. xargs CMD reads words from its stdin and runs CMD with those words added as arguments.
echo "a b c" | xargs echo -> runs: echo a b c
cat hosts.txt | xargs -n1 ping -c1
The second runs ping -c1 HOST (send one test packet, 1.13) once per host: -n1 = one argument per run.
-n N at most N arguments per invocation
-I {} replace {} with the input LINE - one invocation per line
-0 input is NUL-separated (pairs with find -print0)
-r do nothing if the input is empty (--no-run-if-empty)
-P N run N invocations in parallel
-t print each command before running it
The safe idiom
find . -name '*.log' -print0 | xargs -0 rm
-print0 and -0 separate filenames with a NUL byte, which is the one character that cannot appear in a filename. Without them, a file called my report.log arrives as two arguments and you delete the wrong things - the same failure as unquoted $var, one layer out.
(grep -l prints only the names of the files that match.)
-I{} when the argument goes in the middle of the command (every {} is replaced by one input line); plain xargs when it goes at the end.
xargs vs -exec
find . -name '*.log' -exec rm {} \; one rm per file - slow
find . -name '*.log' -exec rm {} + batched - as fast as xargs, no pipe
find . -name '*.log' -delete best when find can do it itself
-exec ... + has made xargs largely unnecessary for find. xargs still wins when the list comes from somewhere else - a file, a jq query, another command.
-r and -P
grep -l ERROR *.log | xargs -r rm without -r, an empty list still
runs rm with no arguments at all
cat hosts.txt | xargs -P4 -n1 ping -c1 four pings at a time
-P runs several commands in parallel (at the same time). Very effective, and very effective at overwhelming whatever is on the other end - use it deliberately.
What you can now do
Turn a list on stdin into arguments with xargs, or xargs -I{} for one run per line.
Pass filenames safely with find -print0 | xargs -0, and guard empty input with -r.
Why it helps
xargs is the glue between "produce a list" and "act on each item". The list comes from jq (names), grep -l (files containing a deprecated key), a file of hostnames, or git diff --name-only (check only changed files). In an incident you might restart a set of units, delete a batch of stale files, or ping every host in parallel with -P. The danger is the same as unquoted variables: filenames with spaces or newlines split into wrong arguments, and with rm that means deleting the wrong thing. Knowing -print0 | xargs -0, -r and find -exec + is how you write and review these lines without that risk.
rm does not read filenames from stdin; it only takes arguments. find . -name '*.log' | rm gives "missing operand". xargs reads stdin and turns it into arguments: ... | xargs rm. The same goes for kill, chmod, systemctl and most commands that act on things rather than filter text.
What goes wrong with filenames that contain spaces?
By default xargs splits its input on whitespace and also interprets quotes and backslashes. my report.log becomes two arguments, my and report.log, and rm either fails or deletes something else. The fix is NUL-separated input: find ... -print0 | xargs -0 rm. NUL is the one byte that cannot appear in a path. -d '\n' (GNU) splits on newlines only, for lists that are one per line.
What is the difference between xargs and xargs -I{}?
Plain xargs cmd appends as many items as fit to the end of one command line: cmd a b c. That is fast, but items always go last. -I{} runs the command once per input line and substitutes {} wherever you put it: xargs -I{} cp {} /backup/. Use it when the argument goes in the middle, accepting one process per item.
Do I still need xargs if find has -exec?
Often not. find ... -exec cmd {} + batches arguments like xargs, handles any filename safely and needs no pipe; -exec cmd {} \; runs once per file; -delete removes without running rm at all. xargs is still the tool when the list comes from something other than find: jq, grep -l, a file, an API, or when you want -P parallelism.
What does -r protect against?
Without -r (--no-run-if-empty), GNU xargs runs the command once even if the input is empty, with no arguments. For rm that is a harmless error, but for commands whose no-argument behaviour is to act on everything or on the current directory, it is not. In scripts, add -r by default so an empty list means "do nothing". BSD xargs on macOS already behaves that way.
In an interview Junior
What does xargs do, and how do you use it safely with filenames?
Many commands (rm, chmod, systemctl) take their list as arguments, not from stdin. xargs CMD reads words from stdin and runs CMD with them added as arguments: echo a b c | xargs echo runs echo a b c.
The danger: xargs splits on spaces, so a file called my report.log arrives as two arguments. The safe idiom separates names with a NUL byte, the one character that cannot be in a filename:
find . -name '*.log' -print0 | xargs -0 rm
Useful flags: -r do nothing if the input is empty, -n1 one argument per run, -I{} put each line where {} is, -P4 four at a time in parallel (effective, and good at overwhelming whatever is on the other end). For a list that comes from find, -exec rm {} + or -delete does the same without a pipe.
Also asked: What is the difference between find -exec cmd {} \; and -exec cmd {} +? · Why add -r to xargs? · When would you use xargs -I{}?