Guides

xargs command in Linux: -0, -n, -I {}, -P and when to use find -exec instead

xargs turns input into arguments for another command: -0 with find -print0, -n per run, -I {} placeholder, -P in parallel, -r for empty input.

Published ~8 min read
On this page

Short answer: xargs reads a list of items from standard input and turns it into arguments for another command, so a command that does not read from a pipe (rm, gzip, chmod, ls) can still work on the output of one that writes to it. find . -name '*.log' -print0 | xargs -0 gzip compresses every log in as few gzip runs as fit. The options you actually need are -0 (safe file names), -n (items per run), -I {} (a placeholder), -P (parallel runs) and -r (do nothing on empty input).

any Linux
find . -name '*.log' -print0 | xargs -0 gzip         # every .log, names with spaces included
grep -rlZ 'oldname' /etc/nginx | xargs -0 -r sed -i 's/oldname/newname/g'
xargs -n 1 -P 4 curl -sO < urls.txt                   # four downloads at a time

Every output block below was produced with GNU xargs (findutils 4.9.0) on Debian 12, in a scratch directory; the other examples follow the GNU xargs man page. macOS and BSD xargs share the core options; the differences are noted where they matter.

Cheat sheet: xargs options

TaskCommandOption
Split input on null bytes (pair with find -print0)find . -print0 | xargs -0 ls -ld-0
At most N items per commandxargs -n 2 echo-n N
Put each item somewhere other than the endxargs -I {} mv {} {}.bak-I {}
Run N commands in parallelxargs -n 1 -P 4 gzip-P N
Do not run at all on empty inputxargs -r rm ---r
Print each command before running itxargs -t rm-t
Ask before each commandxargs -p rm-p
Split on newlines only (GNU)xargs -d '\n' rm ---d
Read items from a file, not stdinxargs -a list.txt echo-a FILE
Show the argument length limitsxargs --show-limits < /dev/null--show-limits

What xargs actually does

A pipe hands text to a command’s standard input. Many commands ignore their input and only act on arguments: ls | rm does nothing useful, because rm never reads stdin. xargs sits in between, collects the words it reads and runs the command with them appended:

Output
$ printf 'a\nb\nc\n' | xargs echo
a b c
$ echo a b c | xargs
a b c

Three items, one echo, not three. With no command at all, xargs runs echo. It packs in as many items as fit under the system’s argument size limit and starts another run only when the list is longer than that, which is the point: rm *.log on a directory with hundreds of thousands of files fails with Argument list too long, while find . -name '*.log' -print0 | xargs -0 rm splits the work into as many runs as needed. The limits on the Debian 12 test system:

Output
$ xargs --show-limits < /dev/null
Your environment variables take up 2203 bytes
POSIX upper limit on argument length (this system): 2092901
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2090698
Size of command buffer we are actually using: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647

By default xargs splits input on spaces, tabs and newlines, and treats quotes and backslashes as special. That default is behind almost every xargs bug, so the next section comes first for a reason.

File names with spaces: -0 with find -print0

In a directory holding a.txt, b.txt and c d.txt:

Output
$ ls | xargs ls -l
ls: cannot access 'c': No such file or directory
ls: cannot access 'd.txt': No such file or directory
...
$ printf '%s\0' *.txt | xargs -0 ls -l
-rw-r--r-- 1 root root 0 Oct 21 12:20 a.txt
-rw-r--r-- 1 root root 0 Oct 21 12:20 b.txt
-rw-r--r-- 1 root root 0 Oct 21 12:20 c d.txt

-0 makes xargs split on the null byte only, and treat everything else, spaces, quotes and backslashes included, as part of the name. The null byte is the one character a Linux path cannot contain, so this is the only fully safe way to pass file names. The producer has to write null bytes too:

ProducerNull-separated output
findfind . -name '*.tmp' -print0
grep (files that match)grep -rlZ PATTERN DIR
gitgit ls-files -z
printf in the shellprintf '%s\0' *.txt

grep -rlZ into xargs -0 sed -i is the classic search and replace across a whole tree. Run the grep -rl half alone first to see which files would be touched; the grep guide covers the flags, and the sed guide covers the expression and why to test it without -i. A single quote in the input shows the other half of the problem:

Output
$ echo "it's" | xargs echo
xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option

Dry run first: echo, -t and -p

Put echo in front of the command to see what xargs would run without running it. Here it also shows the space problem from above in plain sight:

Output
$ ls *.txt | xargs echo rm
rm a.txt b.txt c d.txt

-t prints each command to standard error just before running it, which is the right setting for a script whose log you want to read later. -p prints the command and waits for y before each run; slow, but a good fit for a one-off cleanup on a production server.

Output
$ printf 'a\nb\n' | xargs -t -n 1 echo
echo a
a
echo b
b

-n and -L: how many items per run

Output
$ cat list
one
two
three
four
five
$ cat list | xargs -n 2 echo
one two
three four
five

-n N caps each run at N items. -n 1 runs the command once per item, which you need for commands that take exactly one argument, and with -P. -L N counts input lines instead of words, so a line like x y stays together as one run’s arguments (but is still split into two arguments).

-I {}: put the item anywhere in the command

Output
$ printf 'one.txt\ntwo.txt\n' | xargs -I {} echo mv {} {}.bak
mv one.txt one.txt.bak
mv two.txt two.txt.bak

-I {} replaces every {} in the command with the current item, which is how you get an item into the middle of a command, or into it twice. It changes three other things, per the man page and visible in the output: the command runs once per input line, spaces inside a line no longer split it, and leading blanks on the line are dropped. Any string works as the placeholder; {} is a convention borrowed from find -exec.

-P: run commands in parallel

Output
$ seq 1 6 | xargs -n 1 -P 3 sh -c 'sleep 0.2; echo done $0'
done 1
done 2
done 3
done 5
done 4
done 6

-P N keeps up to N commands running at once. The man page says to combine it with -n or -L: without them xargs may pack everything into one run and there is nothing left to parallelise. The output above also shows the cost: results come back in whatever order the jobs finish (5 before 4), and lines from parallel jobs can interleave. Eight jobs of half a second each took 4.0 seconds with -P 1 and 1.0 second with -P 4 on the test machine.

any Linux
# compress old logs, one gzip per vCPU
find /var/log/myapp -name '*.log' -mtime +7 -print0 | xargs -0 -r -n 1 -P "$(nproc)" gzip

# copy top-level directories to a backup host, four rsync processes at a time
find /srv/data -mindepth 1 -maxdepth 1 -print0 | xargs -0 -I {} -P 4 rsync -a {} backup:/srv/data/

For CPU-heavy work like gzip, more parallel jobs than vCPUs only adds overhead, so -P "$(nproc)" is the sensible ceiling on a VPS. For work that mostly waits on the network, such as downloads or several rsync transfers, a higher number can help, until the disk or the other end becomes the bottleneck. A big parallel job on a production server competes with the services it hosts; start it with nice -n 19 in front of xargs and lower -P if the site slows down.

-r: do nothing when there is no input

Output
$ true | xargs echo "ran anyway"
ran anyway
$ true | xargs -r echo "ran anyway"
$ true | xargs rm
rm: missing operand
Try 'rm --help' for more information.
$ true | xargs du -sh
8.0K	.

GNU xargs runs the command once even when the input is empty. For rm that is just an error message, but commands that default to the current directory when given no arguments, like du and ls, act on the wrong place without a word. Add -r (--no-run-if-empty) to anything that runs from cron or a script. BSD and macOS xargs already skip the run on empty input and accept -r for compatibility.

xargs vs find -exec

find -exec ... \;find -exec ... +find -print0 | xargs -0
Processes startedOne per fileAs few as fitAs few as fit (or per -n)
5,000 files, command trueAbout 1.8 sAbout 3 msAbout 3 ms
Names with spacesSafeSafeSafe with -0 only
Parallel runsNoNoYes, -P
Item in mid-commandYesNo, {} must be lastYes, -I {}
Input from grep, git, a listNoNoYes

The timings come from the same Debian 12 scratch directory. The rule that falls out of the table: when find alone produces the list and one command handles it, use find ... -exec cmd {} +; when the list comes from somewhere else, or you want -P, use xargs -0. Avoid -exec ... \; unless the command takes exactly one file.

Quoting pitfalls, and how to avoid each

  • Spaces, quotes and backslashes in names. The default splitting breaks them. Use -0 with a null-separated producer, or -d '\n' when the input is one item per line and you cannot change the producer.
  • Names that start with a dash. A file called -v.txt reaches rm as a bundle of options, and rm stops with an invalid option error instead of deleting it. End the options with a double dash: xargs rm --. find output is safe here because every name starts with the path, such as ./-v.txt.
  • ls | xargs. Parsing ls output is fragile for every reason above. Use a glob with printf '%s\0', or find.
  • Interactive commands. xargs connects the command’s standard input to /dev/null, so find . -name '*.conf' | xargs vim misbehaves. GNU xargs has -o to reopen the terminal for it: xargs -o vim.

Exit status in scripts

CodeMeaning
0Every run succeeded
123At least one run exited with a status from 1 to 125
124A run exited with 255; xargs stops reading input at that point
125A run was killed by a signal
126The command was found but could not be run
127The command was not found

A failing grep or rm in one batch therefore shows up as 123, not as the command’s own code. To make a script stop early on a bad item, have the command exit 255; xargs then prints xargs: sh: exited with status 255; aborting and returns 124.

Worked examples for a server

keep the newest 14 backups, delete the rest
ls -1t /srv/backups/world-*.tar.gz | tail -n +15 | xargs -r rm --

ls -1t lists newest first, tail -n +15 skips the first 14 lines, and xargs removes what is left. In a test with 16 dated archives, -t showed one rm with the two oldest names and 14 archives remained. This is safe only because the script names its own files without spaces; the Minecraft server guide uses the same line in its nightly backup script.

more one-liners
# hand back to the deploy user everything a deploy left owned by root
find /var/www/site -user root -print0 | xargs -0 -r chown deploy:deploy

# check which of your own hosts (one per line in hosts.txt) answer on port 22, eight at a time
xargs -a hosts.txt -I {} -P 8 sh -c 'nc -z -w 3 "$1" 22 && echo "$1 open"' sh {}

# count lines of code across a git repository, names with spaces included
git ls-files -z '*.py' | xargs -0 cat | wc -l

Frequently asked

What does xargs do in Linux?

xargs reads items from standard input and passes them as arguments to a command, running it as few times as the system's argument limit allows. It connects a command that prints a list, such as find or grep -l, to one that only takes arguments, such as rm, gzip or chmod. Given no command, it runs echo.

How do I use xargs with file names that contain spaces?

Have the producer separate names with null bytes and tell xargs to split on them: find . -name '*.log' -print0 | xargs -0 gzip, or grep -rlZ PATTERN . | xargs -0 followed by the command. Without -0, xargs splits on any whitespace and treats quotes as special, so a name like my file.txt arrives as two arguments.

What does xargs -I {} do?

It replaces every {} in the command with the current input item and runs the command once per input line, so the item can sit anywhere, as in xargs -I {} mv {} {}.bak. Spaces inside a line no longer split it. Do not put {} inside a sh -c script string; pass it to the script as an argument instead.

How do I run commands in parallel with xargs?

Add -P with the number of jobs and -n 1 so each item gets its own run: find . -name '*.log' -print0 | xargs -0 -n 1 -P 4 gzip. Output from parallel jobs comes back in completion order and can interleave. For CPU-heavy work, more jobs than nproc reports only adds overhead.

Related articles

All guides