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).
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 timeEvery 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
| Task | Command | Option |
|---|---|---|
| Split input on null bytes (pair with find -print0) | find . -print0 | xargs -0 ls -ld | -0 |
| At most N items per command | xargs -n 2 echo | -n N |
| Put each item somewhere other than the end | xargs -I {} mv {} {}.bak | -I {} |
| Run N commands in parallel | xargs -n 1 -P 4 gzip | -P N |
| Do not run at all on empty input | xargs -r rm -- | -r |
| Print each command before running it | xargs -t rm | -t |
| Ask before each command | xargs -p rm | -p |
| Split on newlines only (GNU) | xargs -d '\n' rm -- | -d |
| Read items from a file, not stdin | xargs -a list.txt echo | -a FILE |
| Show the argument length limits | xargs --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:
$ printf 'a\nb\nc\n' | xargs echo
a b c
$ echo a b c | xargs
a b cThree 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:
$ 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): 2147483647By 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:
$ 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:
| Producer | Null-separated output |
|---|---|
| find | find . -name '*.tmp' -print0 |
| grep (files that match) | grep -rlZ PATTERN DIR |
| git | git ls-files -z |
| printf in the shell | printf '%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:
$ echo "it's" | xargs echo
xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 optionDry 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:
$ 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.
$ printf 'a\nb\n' | xargs -t -n 1 echo
echo a
a
echo b
b-n and -L: how many items per run
$ 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
$ 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
$ 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.
# 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
$ 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 started | One per file | As few as fit | As few as fit (or per -n) |
| 5,000 files, command true | About 1.8 s | About 3 ms | About 3 ms |
| Names with spaces | Safe | Safe | Safe with -0 only |
| Parallel runs | No | No | Yes, -P |
| Item in mid-command | Yes | No, {} must be last | Yes, -I {} |
| Input from grep, git, a list | No | No | Yes |
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
-0with 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.txtreachesrmas a bundle of options, and rm stops with aninvalid optionerror 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. Parsinglsoutput is fragile for every reason above. Use a glob withprintf '%s\0', or find.- Interactive commands. xargs connects the command’s standard input to
/dev/null, sofind . -name '*.conf' | xargs vimmisbehaves. GNU xargs has-oto reopen the terminal for it:xargs -o vim.
Exit status in scripts
| Code | Meaning |
|---|---|
| 0 | Every run succeeded |
| 123 | At least one run exited with a status from 1 to 125 |
| 124 | A run exited with 255; xargs stops reading input at that point |
| 125 | A run was killed by a signal |
| 126 | The command was found but could not be run |
| 127 | The 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
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.
# 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 -lFrequently 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.