xargs
Build command lines from standard input
Most commands take their input as arguments, not on standard input. rm, cp, chmod, kill
and mkdir all ignore whatever you pipe into them, so find logs -name "*.log" | rm doesn't
delete anything and complains that it is missing an operand. This is because the pipeline
delivered those filenames to rm's standard input, which rm never reads. xargs sits in that
gap. It reads items from standard input and runs a command with those items appended as arguments.
With no command given it runs echo, which is the quickest way to see what it is about to pass
on before you let it near something destructive.
How input becomes arguments
By default xargs splits its input on whitespace (spaces, tabs and newlines alike) and packs as
many items as will fit onto one command line. Four hostnames become a single command with four
arguments rather than four separate commands. -n1 forces one item per run, -n3 takes three at
a time, and -L1 splits on input lines instead of on a count of items.
The packing is worth understanding because it is usually where the speed comes from. One grep
invocation given 500 filenames does far less work than 500 invocations given one filename each,
and on a large tree that difference is minutes rather than milliseconds. It is also why -s
exists: the kernel caps how long a single command line may be, so xargs splits into as many
runs as it needs to stay under that cap. You will not hit the limit by hand, but you will hit it
piping a large find into a command, and xargs handles it silently.
Whitespace separates items, including whitespace inside filenames
A file called last week.log arrives as two items, last and week.log, so the command runs
against two names that do not exist. The failure is a quiet one: wc -l reports
counts for every file it could open, prints an error for the two it could not, and puts a total at
the bottom that is simply wrong. Read quickly, it looks like a successful run.
The fix is to separate items with a byte that cannot appear in a filename. find -print0 ends
each name with a NUL, and xargs -0 splits on NUL instead of whitespace. grep
has -Z, sort has -z, and du has --files0-from, so the whole pipeline can speak the same
protocol end to end.
-0 turns off one other behaviour worth knowing about: without it, xargs treats quotes and
backslashes in its input as syntax. A filename containing an apostrophe makes it fail outright
with unmatched single quote. That failure is at least visible, unlike the silent splitting
above. -0 reads bytes literally and takes neither quotes nor backslashes as special.
Putting the item somewhere other than the end
Everything so far appends items to the end of the command, which is where most commands want their
filenames. cp does not: its destination has to come last, which is exactly
where xargs puts the items. xargs cp builds cp file1 file2 file3, and cp reads that last
name as the destination directory, failing with target 'file3': Not a directory. cp -t and
mv -t exist for this, naming the destination up front.
-I solves that by naming a placeholder. Every occurrence of that placeholder in the command is
replaced with the incoming item, wherever it appears:
xargs -I{} cp {} /backup/{}.bak
{} is only a convention. -I@ works the same way, and so can be a better choice when the
command itself contains braces.
-I changes two other things at the same time, which are both easy to miss. It runs the command
once per line of input rather than packing several items into one command line, so the
batching described above is gone and a thousand items mean a thousand processes. It also overrides
-n. If you give both, xargs will warn that it is ignoring the -n.
Placeholders inside sh -c
Wrapping the command in sh -c is how you get shell features that xargs has none of, such as a
pipeline or a variable. The trap is that -I substitutes text, and it does so before that
shell parses anything. So this:
xargs -0 -I{} sh -c 'cat {}'
builds the command line sh -c 'cat logs/last week.log', so the child shell splits that on the
space into two arguments. The -0 protected the filename all the way through xargs and then
handed it to a shell that undoes the protection.
Pass the item as an argument instead of pasting it into the script, and quote it where it is used:
xargs -0 -I{} sh -c 'echo "== $1"; cat "$1"' _ {}
Everything after the quoted script is passed to that shell as positional parameters. The first one
becomes $0, which is conventionally the program name and not usually wanted, so _ absorbs it
and the real item arrives as $1. Quoting "$1" then keeps it in one piece however many spaces
it contains.
Running several commands at once
-P4 runs up to four of the commands concurrently, and -P0 runs as many as the system will
allow. This is a real speed-up for anything I/O-bound, and it comes with the usual condition:
output from concurrent runs interleaves in whatever order the runs finish, so anything you intend
to read or compare needs sorting afterwards. Commands that append to a shared file are not safe
this way at all.
Exit status
xargs reports on the batch rather than on any one command. If a command it ran exits non-zero,
xargs exits 123 whatever the command's own status was. A command exiting 255 is treated as a
demand to stop immediately: xargs abandons the remaining input and exits 124. A script
checking $? after an xargs pipeline sees those numbers rather than the ones its command
produced.
Empty input is a case worth guarding against. Given nothing at all, xargs still runs the
command once with no arguments, which for rm is harmless and for something like docker rm is
not. -r
(--no-run-if-empty) suppresses that run. It is a GNU extension rather than POSIX, so a script
that has to be portable checks for empty input itself. See
pipes and redirection for how the pipeline delivers that input
in the first place.
Sample files used on this page
Every example below was run against these files. Recreate them to follow along.
hosts.txt four hostnames, one per line
web1.example.com
web2.example.com
db1.example.com
cache1.example.com
packages.txt three package names, one per line
curl
jq
rsync
users.csv a header row plus 6 records
name,age,department
Alice,34,Engineering
Bob,29,Sales
Carol,41,Engineering
Dave,25,Marketing
Erin,38,Sales
Frank,31,Engineering
logs/ five log files, one of them with a space in its name
logs/api.log
logs/app.log
logs/archive/2026-07.log
logs/error.log
logs/last week.log
logs/app.log
boot: starting
boot: ready
logs/error.log
ERROR: disk full
ERROR: retry failed
WARN: slow query
logs/api.log
GET /index.html 200
GET /missing 404
logs/last week.log the file that every separator example on this page depends on
ERROR: old failure
logs/archive/2026-07.log
archived
From standard input to arguments
xargs reads items from standard input and appends them to a command as arguments. Everything else on this page is a variation on how those items are split up and where they are placed.
Watch a pipeline fail because the command ignores standard input
find logs -name "*.log" | rm
rm takes filenames as arguments and never reads standard input, so the pipeline hands it nothing it can use. The error is accurate rather than confusing once you know that: rm really was called with no operands.
Show output
rm: missing operand
Try 'rm --help' for more information.
Turn a file of items into one command line
xargs echo < hosts.txt
The four lines arrive as four arguments to a single echo, which prints them space-separated on one line.
Show output
web1.example.com web2.example.com db1.example.com cache1.example.com
Leave the command out and get echo
xargs < hosts.txt
With no command given, xargs runs echo. Useful for seeing exactly what it will pass on before putting a real command there.
Show output
web1.example.com web2.example.com db1.example.com cache1.example.com
Read from a pipe instead of a file
cat packages.txt | xargs
Standard input is standard input, whatever produced it. See pipes and redirection for the difference between < and | here.
Show output
curl jq rsync
Read from a named file without a redirect
xargs -a hosts.txt
-a names an input file directly, which keeps standard input free for something else.
Show output
web1.example.com web2.example.com db1.example.com cache1.example.com
Use the long form of -a in a script
xargs --arg-file=hosts.txt -n1 echo host:
--arg-file is the same flag spelled out, which reads better in a script someone else will maintain.
Show output
host: web1.example.com
host: web2.example.com
host: db1.example.com
host: cache1.example.com
Pass filenames to a command that counts them
echo "logs/app.log logs/error.log" | xargs wc -l
wc is invoked once with both filenames, so it prints a total as well as a line for each file.
Show output
2 logs/app.log
3 logs/error.log
5 total
Flatten a directory listing onto one line
ls logs | xargs
A quick way to see a listing as a single line. Note that last week.log appears here as two apparent names, which is the subject of a later section.
Show output
api.log app.log archive error.log last week.log
How many items go on each command line
By default xargs packs as many items as will fit onto one command line, which is what makes it fast. These flags override that when the command needs a fixed number.
Run the command once per item
xargs -n1 < hosts.txt
-n1 gives one item per invocation, so this runs four separate echo commands rather than one.
Show output
web1.example.com
web2.example.com
db1.example.com
cache1.example.com
Run the command once per pair of items
xargs -n2 < hosts.txt
-n2 takes two items per invocation. The last run gets whatever is left over, which may be fewer.
Show output
web1.example.com web2.example.com
db1.example.com cache1.example.com
Add a fixed argument before each item
xargs -n1 echo Deploying < hosts.txt
Arguments you write yourself come first, and xargs appends the item after them.
Show output
Deploying web1.example.com
Deploying web2.example.com
Deploying db1.example.com
Deploying cache1.example.com
Batch a numbered list three at a time
seq 1 10 | xargs -n3
-n3 fills a run with three items and starts another, so ten items take four runs and the last is short.
Show output
1 2 3
4 5 6
7 8 9
10
Count how many invocations a batch size produces
seq 1 10 | xargs -n3 | wc -l
Each invocation prints a line, so counting lines counts the runs. Worth checking before pointing a slow command at a long list.
Show output
4
Split on input lines rather than on a count
xargs -L1 echo < hosts.txt
-L1 runs the command once per input line. With one item per line the result matches -n1, and it diverges as soon as a line holds several items.
Show output
web1.example.com
web2.example.com
db1.example.com
cache1.example.com
Keep the items from each line together
printf "a b c\nd e\n" | xargs -L1 echo LINE:
Here -L1 and -n1 differ: the first line carries three items and the second carries two, and each line becomes exactly one invocation.
Show output
LINE: a b c
LINE: d e
Cap the command line by length instead of by count
xargs -s 40 < hosts.txt
-s sets the maximum number of characters per command line. The kernel imposes its own much larger limit, and xargs stays under that one automatically.
Show output
web1.example.com web2.example.com
db1.example.com cache1.example.com
Stop reading at a marker line
printf "web1\nweb2\nEND\nweb3\n" | xargs -E END
-E sets an end-of-file string. Everything after that line is ignored, which lets one file hold both a list and a trailing note.
Show output
web1 web2
Split on a delimiter of your own
printf "one;two;three" | xargs -d";" -n1 echo
-d replaces whitespace splitting with a single delimiter, so spaces inside items no longer break them apart.
Show output
one
two
three
See what a trailing newline does to -d
printf "one;two;three\n" | xargs -d";" -n1 echo | cat -A
With -d, the newline is no longer a separator, so it stays inside the final item. cat -A marks line ends with $, showing the extra one that echo then prints.
Show output
one$
two$
three$
$
Split a quoted string into separate arguments
echo "a b c" | xargs -d" " -n1 echo item:
A space delimiter with -n1 turns one line into one invocation per word.
Show output
item: a
item: b
item: c
Placing each item with -I
-I names a placeholder and replaces it with the incoming item, so the item can sit in the middle of a command or appear more than once. It implies one item per run.
Build a command around each item
xargs -I{} echo "ssh {} uptime" < hosts.txt
The placeholder can go anywhere in the command, which is what -n1 alone cannot do.
Show output
ssh web1.example.com uptime
ssh web2.example.com uptime
ssh db1.example.com uptime
ssh cache1.example.com uptime
Use the same item twice in one command
xargs -I{} echo "{}: backing up to {}.bak" < packages.txt
Every occurrence of the placeholder is replaced, which is the usual way to build a source and destination pair.
Show output
curl: backing up to curl.bak
jq: backing up to jq.bak
rsync: backing up to rsync.bak
Choose a different placeholder
xargs -I@ echo "host @ checked" < hosts.txt
{} is a convention rather than a requirement. Pick another when the command itself contains braces.
Show output
host web1.example.com checked
host web2.example.com checked
host db1.example.com checked
host cache1.example.com checked
Copy a file once per item
xargs -I{} cp logs/app.log {}.copy < packages.txt && ls *.copy
A placeholder in the destination position gives one copy per name in the list.
Show output
curl.copy
jq.copy
rsync.copy
Number a sequence of generated commands
seq 1 3 | xargs -I{} echo "item {} of the batch"
-I accepts any input, not only filenames.
Show output
item 1 of the batch
item 2 of the batch
item 3 of the batch
Do arithmetic on each item
printf "1\n2\n3\n" | xargs -I{} sh -c 'echo $(( {} * 2 ))'
Wrapping the command in sh -c gives you shell features that xargs itself has none of. Beware that the substitution is textual: it is safe here only because the input is known to be numbers.
Show output
2
4
6
Feed the item to sh -c safely
find logs -name "*.log" -print0 | sort -z | xargs -0 -I{} sh -c 'echo "== $1"; cat "$1"' _ {}
Passing the item as a positional parameter and quoting it as "$1" is what keeps a filename with a space intact inside the child shell. The _ supplies $0.
Show output
== logs/api.log
GET /index.html 200
GET /missing 404
== logs/app.log
boot: starting
boot: ready
== logs/archive/2026-07.log
archived
== logs/error.log
ERROR: disk full
ERROR: retry failed
WARN: slow query
== logs/last week.log
ERROR: old failure
Watch the same command break when the item is substituted directly
find logs -name "*.log" -print0 | sort -z | xargs -0 -I{} sh -c 'echo "== {}"; cat {}'
-0 protected the filename on its way into xargs, and the placeholder then pasted it unquoted into a shell command line, where the space splits it again. The header prints correctly because it is inside double quotes; the cat beside it does not.
Show output
== logs/api.log
GET /index.html 200
GET /missing 404
== logs/app.log
boot: starting
boot: ready
== logs/archive/2026-07.log
archived
== logs/error.log
ERROR: disk full
ERROR: retry failed
WARN: slow query
== logs/last week.log
cat: logs/last: No such file or directory
cat: week.log: No such file or directory
Loop over a whole batch inside one shell
find logs -name "*.log" -print0 | sort -z | xargs -0 sh -c 'for f in "$@"; do echo "== $f"; done' _
Without -I, the batch arrives as $@ and the shell loops over it. One shell for many files rather than one shell each, and the quoting stays correct.
Show output
== logs/api.log
== logs/app.log
== logs/archive/2026-07.log
== logs/error.log
== logs/last week.log
Filenames with spaces, and the NUL separator
Whitespace separates items by default, and a filename may contain whitespace. Every example in this section runs against a logs/ directory containing a file called last week.log.
See a filename split into two items
find logs -name "*.log" | sort | xargs -n1 echo
last week.log arrived as two items, logs/last and week.log, neither of which exists, which is why five files produce six lines.
Show output
logs/api.log
logs/app.log
logs/archive/2026-07.log
logs/error.log
logs/last
week.log
Keep the filename intact with -print0 and -0
find logs -name "*.log" -print0 | sort -z | xargs -0 -n1 echo
find -print0 ends each name with a NUL byte, sort -z keeps that framing, and xargs -0 splits on it. A NUL cannot occur inside a filename, so the split is exact.
Show output
logs/api.log
logs/app.log
logs/archive/2026-07.log
logs/error.log
logs/last week.log
Watch a count come out wrong rather than fail
find logs -name "*.log" | sort | xargs wc -l
The two errors are easy to miss in a long run, and the total at the bottom is wrong rather than absent: 8 lines counted across four files, when there are 9 across five.
Show output
2 logs/api.log
2 logs/app.log
1 logs/archive/2026-07.log
3 logs/error.log
wc: logs/last: No such file or directory
wc: week.log: No such file or directory
8 total
Get the right count with NUL separation
find logs -name "*.log" -print0 | sort -z | xargs -0 wc -l
The same pipeline with -print0 and -0 reaches all five files, and the total is 9.
Show output
2 logs/api.log
2 logs/app.log
1 logs/archive/2026-07.log
3 logs/error.log
1 logs/last week.log
9 total
Find which files match a pattern
find logs -name "*.log" -print0 | sort -z | xargs -0 grep -l ERROR
grep -l prints the name of each file containing a match, and the spaced filename is one of them.
Show output
logs/error.log
logs/last week.log
Count matches per file
find logs -name "*.log" -print0 | sort -z | xargs -0 grep -c ERROR
grep -c reports a count for every file it is given, including the zeros.
Show output
logs/api.log:0
logs/app.log:0
logs/archive/2026-07.log:0
logs/error.log:2
logs/last week.log:1
Take NUL-separated input straight from grep
grep -rlZ ERROR logs | sort -z | xargs -0 -n1 basename
grep -rlZ emits NUL-terminated filenames, so find is not needed when the selection is by content.
Show output
error.log
last week.log
Strip directories off a list of paths
find logs -name "*.log" -print0 | sort -z | xargs -0 -n1 basename
basename takes one path at a time, so -n1 is required rather than optional here.
Show output
api.log
app.log
2026-07.log
error.log
last week.log
Collect the distinct directories in a tree
find logs -type f -print0 | sort -z | xargs -0 -n1 dirname | sort -u
dirname per file, then sort -u, gives the set of directories that actually contain files.
Show output
logs
logs/archive
Watch a quote in the input stop xargs outright
echo "it's here" | xargs echo
Without -0, quotes and backslashes in the input are syntax to xargs, and an unbalanced one is fatal. This is the loud version of the splitting problem.
Show output
xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option
Read input literally with -0
printf "it's here\0" | xargs -0 echo
-0 takes every byte literally, so quotes and backslashes lose their special meaning along with whitespace.
Show output
it's here
Parallel runs with -P
-P runs commands concurrently. Output from concurrent runs arrives in whatever order the runs finish, so every example here sorts its output to be reproducible.
Run up to four commands at a time
xargs -P4 -I{} sh -c "echo {} done" < hosts.txt | sort
-P4 allows four concurrent children. The sort is not decoration: without it the order depends on which run finishes first.
Show output
cache1.example.com done
db1.example.com done
web1.example.com done
web2.example.com done
Combine batching with parallelism
seq 1 6 | xargs -n2 -P3 sh -c 'echo pair: $@' _ | sort
-n and -P compose, giving three concurrent runs of two items each, where -I and -n do not.
Show output
pair: 1 2
pair: 3 4
pair: 5 6
Let xargs choose the parallelism
seq 1 5 | xargs -n1 -P0 echo host | sort
-P0 runs as many at once as the system allows. Reasonable for short I/O-bound work, and a good way to overwhelm a machine with anything heavier.
Show output
host 1
host 2
host 3
host 4
host 5
Keep batched output stable
seq 1 4 | xargs -n2 -P2 echo | sort
Each invocation writes one line, and sorting afterwards makes the result independent of which finished first.
Show output
1 2
3 4
Seeing what will run
xargs builds command lines you did not type, so being able to print them before they run is worth the two extra characters.
Print each command before running it
xargs -t -n1 touch < packages.txt
-t writes each command to standard error as it goes. touch prints nothing itself, so the trace is all the output here.
Show output
touch curl
touch jq
touch rsync
Trace a command built with a placeholder
xargs -t -I{} mkdir -p backup/{} < packages.txt
The trace shows the command after substitution, which is what you want to check before pointing the same pipeline at something destructive.
Show output
mkdir -p backup/curl
mkdir -p backup/jq
mkdir -p backup/rsync
Delete files, having seen the command first
touch logs/old.bak logs/stale.bak && find logs -name "*.bak" -print0 | sort -z | xargs -0 -t rm && ls logs
rm with -t prints exactly what it removed. Build any deletion this way, and run it once with echo in place of rm before you trust the file list.
Show output
rm logs/old.bak logs/stale.bak
api.log
app.log
archive
error.log
last week.log
Discard the trace and keep the output
xargs -n1 -t echo < packages.txt 2>/dev/null
The trace goes to standard error and the command's own output to standard output, so the two can be separated when a script only wants one of them.
Show output
curl
jq
rsync
Exit status and empty input
xargs reports on the batch rather than passing through the status of any one command, and it runs the command once even when given nothing at all.
See the batch exit code after a command fails
echo missing.txt | xargs wc -l; echo "exit: $?"
wc exited 1, and xargs reports 123, which is its code for "something in the batch failed".
Show output
wc: missing.txt: No such file or directory
exit: 123
Confirm 123 does not depend on the command's own status
printf "a\nb\n" | xargs -n1 sh -c "exit 1"; echo "exit: $?"
The command exited 1 on both runs and the batch still reports 123. A script testing $? against the command's own codes will not see them.
Show output
exit: 123
Abort the whole batch from a command
printf "a\nb\n" | xargs -n1 sh -c "exit 255"; echo "exit: $?"
Exiting 255 tells xargs to stop immediately rather than continue through the input, and it then exits 124. Useful for a command that knows the remaining work is pointless.
Show output
xargs: sh: exited with status 255; aborting
exit: 124
Watch a command run with no arguments at all
printf "" | xargs echo "nothing to do:"
Empty input still runs the command once. Harmless for echo, and not harmless for anything that treats "no arguments" as "everything".
Show output
nothing to do:
Suppress the run when there is no input
printf "" | xargs -r echo "nothing to do:"; echo "exit: $?"
-r skips the command entirely when input is empty, and still exits 0. It is a GNU extension, so a portable script tests for empty input itself.
Show output
exit: 0
Spell -r out in a script
printf "" | xargs --no-run-if-empty echo "nothing to do:"; echo "exit: $?"
--no-run-if-empty is the same flag, and says what it does to anyone reading the script later.
Show output
exit: 0
Everyday pipelines
Gluing a command that produces a list to one that wants arguments, which is most of what xargs gets used for.
Turn a column of a CSV into one argument list
cut -d, -f1 users.csv | tail -n +2 | xargs
cut selects the column, tail -n +2 drops the header, and xargs collapses what is left onto one line.
Show output
Alice Bob Carol Dave Erin Frank
Run a command once per row
cut -d, -f1 users.csv | tail -n +2 | xargs -n1 echo user:
The same pipeline with -n1 becomes one invocation per name.
Show output
user: Alice
user: Bob
user: Carol
user: Dave
user: Erin
user: Frank
Build an address from each field
awk -F, 'NR>1 {print $1}' users.csv | xargs -I{} echo "mail {}@example.com"
awk does the field selection and the header skip in one step, and -I places the result inside a longer string.
Show output
mail Alice@example.com
mail Bob@example.com
mail Carol@example.com
mail Dave@example.com
mail Erin@example.com
mail Frank@example.com
Install a list of packages in one transaction
xargs -n1 -a packages.txt echo installing
Dropping the -n1 and putting apt install in place of echo is the real form of this, and one transaction handles dependencies better than three do.
Show output
installing curl
installing jq
installing rsync
Strip the newline off a single value
grep -c ERROR logs/error.log | xargs echo "error lines:"
A one-item pipeline into bare xargs is a common way to trim surrounding whitespace from a captured value.
Show output
error lines: 2
Back up every log file beside itself
find logs -name "*.log" -print0 | sort -z | xargs -0 -I{} cp {} {}.bak && ls logs
-I twice in one command gives the source and the destination. The spaced filename survives because cp receives it as a single argument.
Show output
api.log
api.log.bak
app.log
app.log.bak
archive
error.log
error.log.bak
last week.log
last week.log.bak
Count files one at a time rather than in a batch
find logs -name "*.log" -print0 | sort -z | xargs -0 -n1 wc -l
wc pads its counts into a column when given several files and does not when given one, so -n1 changes the shape of the output as well as the number of runs.
Show output
2 logs/api.log
2 logs/app.log
1 logs/archive/2026-07.log
3 logs/error.log
1 logs/last week.log
Count the invocations a pipeline will make
xargs -n1 echo < hosts.txt | wc -l
Piping into wc -l before substituting the real command is a cheap way to find out how many times it is about to run.
Show output
4