Why this matters
find produces lists of files; many tools want those files as arguments. Joining the two correctly is where scripts traditionally fall apart: a name with a space, a quote or a newline splits into pieces, and the pieces get passed to rm. And when the list is huge, a different failure appears: Argument list too long. This lesson explains both problems down at the kernel level and shows the robust tools: xargs, find -exec +, NUL separators, and bash's NUL-aware read.
Why xargs exists
Programs receive input in two different ways: as arguments (rm a b c) or on stdin (sort < file). Pipes connect stdout to stdin, so they can only feed the second kind:
find . -name '*.tmp' | rm # does nothing useful: rm ignores stdin
xargs bridges the gap: it reads items from stdin and runs a command with those items appended as arguments.
$ printf 'a\nb\nc\n' | xargs echo rm
rm a b c
$ printf 'a\nb\nc\n' | xargs -n 1 echo rm
rm a
rm b
rm c
(echo in front is a good dry run: it prints the command instead of running it.) With no command, xargs runs echo.
Useful options:
| Option | Meaning |
|---|---|
-0 | items are separated by NUL bytes; no quote or backslash processing |
-n N | at most N items per command |
-I {} | run once per item, replacing {} in the command (-I {} mv {} {}.bak) |
-r | do not run the command at all if the input is empty (GNU) |
-P N | run up to N commands in parallel |
-t | print each command to stderr before running it |
Without -r, GNU xargs runs the command once even with empty input: find . -name '*.x' | xargs rm with no matches runs plain rm, which complains rm: missing operand.
The kernel limit: ARG_MAX
When bash runs an external program, it passes the arguments and the environment to the kernel's execve system call, which copies them into the new process's memory. The kernel caps the total:
$ getconf ARG_MAX
2097152
About 2 MB for arguments plus environment together (on Linux it is a quarter of the stack size limit), and each single argument can be at most 128 KiB. Exceed it and execve fails with the error E2BIG, which bash reports as:
bash: /usr/bin/rm: Argument list too long
Note who complains: bash expanded *.tmp into half a million words without trouble; it is the exec of rm that fails. Builtins are never exec'd, so echo *.tmp and for f in *.tmp work with any number of files, and so does printf '%s\0' *.tmp | xargs -0 rm, because printf is a builtin.
xargs knows the limit: it packs as many items into each command as fit, then starts another command for the rest. find -exec cmd {} + does the same. So both handle any number of files.
The separator problem, and NUL
By default xargs splits on blanks and newlines and interprets quotes and backslashes:
$ printf "my file.txt\nit's.txt\n" | xargs echo
xargs: unmatched single quote; by default quotes are special to xargs unless you use the -0 option
And find's normal output separates names with newlines, which are legal in file names. There is exactly one byte that can never appear in a Unix path: the NUL byte (value 0), because the kernel's interfaces use it to mark the end of a string. So the robust protocol is:
find ... -print0 writes name1\0name2\0name\nwith newline\0
xargs -0 cmd splits on \0 only, no quote processing
find . -type f -name '*.log' -print0 | xargs -0 gzip --
find . -name '*.tmp' -print0 | xargs -0 -r rm --
Other tools that speak NUL: sort -z, grep -z (lines end in NUL) and grep -lZ (print matching file names NUL-terminated), du --files0-from, tar --null -T -, and printf '%s\0'.
The -- protects against names starting with -, which find does not produce when started from . (./-g.log), but which you get when the starting point is a bare name like data.
Reading NUL-separated input in bash
When the per-file work is shell code rather than one command, use a loop. read -d '' makes read stop at a NUL byte instead of a newline:
while IFS= read -r -d '' file; do
size=$(stat -c %s -- "$file")
printf '%8d %s\n' "$size" "$file"
done < <(find . -type f -name '*.log' -print0)
Each piece has a purpose:
-d '': the delimiter is the empty string, whose first byte is NUL.-r: backslashes are kept literally.IFS=: leading and trailing spaces are kept.< <(find ...): process substitution, so the loop runs in the current shell and variables set inside it survive (module 13, lesson 4).find ... | while ...also works if you do not need them afterwards.
Choosing between them
- One command per batch of files:
find ... -exec cmd {} + - Need xargs features (parallelism,
-n, a list that comes from elsewhere):find ... -print0 | xargs -0 cmd - Shell logic per file:
while IFS= read -r -d '' f; do ...; done < <(find ... -print0) - Files in one directory: a glob,
for f in *.log
-exec + needs no extra process and is the simplest safe choice; reach for xargs when you need what it adds.
Parallel work with xargs -P
find photos -name '*.png' -print0 | xargs -0 -P 4 -n 10 optipng -quiet --
Up to 4 optipng processes at once, 10 files each. This is the bounded parallelism that module 13 warned you to use instead of an unbounded & loop. Output from parallel commands can interleave, so let each command write to its own file when the output matters.
Common mistakes
- Plain
find | xargs: breaks on spaces, quotes and newlines (xargs: unmatched single quote). Always pair-print0with-0. -print0without-0(or the reverse): one giant argument, or names glued together.- Missing
-r: with empty input the command runs once with no file arguments. xargs -I {}is safe for spaces (it splits on newlines only) but runs one command per item, so it is slow, and still breaks on newlines unless combined with-0.$(find ...)as an argument list: word splitting plus ARG_MAX, both at once.- Forgetting
--when names may start with-.
Exercises
Two quizzes on the kernel limit and xargs's splitting, a task in the sandbox where file names include a newline (look at ls -b data to see it), and a script that reads NUL-separated input.
Recap
- xargs turns stdin items into command arguments, in batches that respect the kernel's ARG_MAX.
Argument list too longis execve's E2BIG; builtins are immune;-exec +and xargs avoid it.- NUL is the only byte not allowed in file names:
find -print0 | xargs -0andwhile IFS= read -r -d ''handle every possible name. xargs -rskips empty input;-n,-I,-Pcontrol batching and parallelism.- Prefer
find -exec ... {} +when one command per batch is enough.
Going further: xargs --help, and getconf -a | grep ARG_MAX. Try printf '%s\0' * | xargs -0 -t -n 2 echo in a directory with a few files to watch how xargs batches.