xargs

Build command lines from standard input

Updated 2026-08-23

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
62 outputs, collapsed by default

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 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

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$
$

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