Skip to content

xargs splits on the wrong things

Level: 201 · for anyone who has piped find into something

One line: xargs splits its input on spaces and tabs as well as newlines, and processes ', " and \ as quoting before it hands anything over — so a file called O'Brien.txt stops the pipeline, and find … -print0 | xargs -0 is not a refinement but the only correct form.

Where this page starts

The find page establishes that a filename is a bag of bytes with two forbidden values, 0x00 and /, so a newline in a name is legal and find … | wc -l counts the wrong thing. This page is about what happens next — because the tool on the other end of that pipe makes the problem worse in two ways find cannot warn you about.

In the terminal

Verified output of xargs_separator_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

1. SEVEN FILES. COUNT THEM THREE WAYS AND GET THREE ANSWERS
   ls files | wc -l                              : 8
   find files -type f | wc -l                    : 8
   find files -type f -print0 | tr -dc "\\0" | wc -c : 7
   Eight, eight, seven. One of the names contains a newline, so any count
   of LINES is a count of the wrong thing. Only the NUL-separated form
   answers 'how many files', because NUL is the one byte a filename
   cannot contain.

2. THE DEFAULT SEPARATOR IS WHITESPACE, NOT NEWLINE
$ printf "with space.txt\n" | xargs -n99 echo COUNT:
COUNT: with space.txt
$ printf "with space.txt\n" | xargs -n1 echo ARG:
ARG: with
ARG: space.txt
   One filename went in and TWO arguments came out. xargs splits on
   spaces and tabs as well as newlines, so 'with space.txt' is 'with' and
   'space.txt' — two files that do not exist.

3. AND THE QUOTE CHARACTERS ARE SPECIAL
$ printf "O%sBrien.txt\\n" "'" | xargs echo
   (refused — exit 1)
   An apostrophe in a filename is enough. No spaces, no newline, nothing
   exotic — xargs processes ' and " and backslash as quoting before it
   hands anything over, so a perfectly ordinary Irish surname stops the
   pipeline. Both xargs refuse; they word it differently, so only the
   status is shown here. GNU's message names the fix, BSD's does not.

4. -print0 AND -0: THE PAIR THAT TURNS ALL OF IT OFF
$ find files -type f -print0 | xargs -0 -n1 echo FILE: | wc -l
8
   Eight lines for seven files, and this time that is honest: echo is
   printing a name that genuinely contains a newline. With -0 there is no
   splitting on spaces, no quote processing and no escape processing —
   the only separator is the one byte that cannot appear in a name.

5. THE SIZE LIMIT IS COUNTED IN BYTES, SO THE ENCODING DECIDES
   THE NUMBER OF BATCHES
   Two directories, ten files each, every name SEVEN CHARACTERS long:
     ascii/aaaaa01   bytes: 7
     accent/ééééé01  bytes: 12
   ascii  at -s 200 : 1
   accent at -s 200 : 2
   batch(es). Same number of files, same number of characters in every
   name, and the accented directory needs more runs — because -s is a
   BYTE budget and é costs two. How many times your command runs is a
   function of how your filenames are spelled.

6. WHY THE BATCH COUNT IS NOT A DETAIL
   xargs runs the command once PER BATCH. That is invisible for rm and
   chmod, and it is a bug for anything that starts fresh each time:
     find . -print0 | xargs -0 tar -cf out.tar      <- batch 2 OVERWRITES
     find . -print0 | xargs -0 sort > sorted        <- sorted per batch
   Neither reports anything. The archive is simply short, and whether it
   is short depends on whether the filenames were ASCII.
   The fix is not a bigger -s. It is to hand the LIST to a command that
   reads a list, instead of expanding it onto a command line:
     find . -print0 | tar --null -cf out.tar -T -
   -T - reads names from stdin and --null says they are NUL-separated, so
   there is one tar, no batching, and no separator to get wrong. Verified
   on bsdtar 3.5.3 and GNU tar 1.35.

The three defaults, and why each is wrong for filenames

Default What it does Why it breaks
splits on whitespace space and tab end an argument, as well as newline with space.txt becomes two arguments naming two files that do not exist
processes quotes ', " and \ are quoting characters O'Brien.txt is an unterminated quote and xargs refuses — no spaces required
batches by byte budget runs the command once per ARG_MAX-sized batch how many times your command runs depends on how your filenames are spelled

-0 turns off the first two completely. Nothing turns off the third.

The quote row is the one people do not expect, and it is worth dwelling on: it is not an edge case about weird filenames. Apostrophes are in ordinary names, in every language that has them, and a single one in a single file will stop a find … | xargs pipeline dead. Both xargs implementations refuse — GNU's message names the fix ("by default quotes are special to xargs unless you use the -0 option") and BSD's does not ("unterminated quote"), which is why the recorded example above shows the exit status rather than the wording.

The byte budget is the character-data part

Section 5 of the example is the reason this page is in this chapter rather than a shell-scripting one. Two directories, ten files each, every filename exactly seven characters long. The ASCII one takes one batch; the accented one takes two, because -s is a budget in bytes and each é costs two of them.

That is a property of the text, changing the control flow of a pipeline. For rm or chmod it makes no difference. For anything that starts fresh on each run it is a silent data-loss bug:

find . -print0 | xargs -0 tar -cf out.tar     # batch 2 OVERWRITES batch 1's archive
find . -print0 | xargs -0 sort > sorted       # sorted within each batch, not overall

Nothing reports it. The archive is simply short — and whether it is short depends on whether the filenames happened to be ASCII, which is why it works in testing and fails on the customer with the accented surname.

The fix is not a bigger -s. It is to stop using a command line as the transport:

find . -print0 | tar --null -cf out.tar -T -

-T - reads the names from stdin and --null says they are NUL-separated, so there is one tar, no batching, and no separator to get wrong. Verified on bsdtar 3.5.3 and GNU tar 1.35.

The part that is not the same on both machines

Measured 2026-09-06 — macOS 26.6 (BSD xargs) and ubuntu:24.04 (GNU findutils 4.9.0). Not machine-checked: no key can hold both.
                                      macOS (BSD)        Ubuntu (GNU)
: | xargs echo RAN                    (nothing)          RAN
: | xargs -r echo RAN                 (nothing)          (nothing)

find files -type f | xargs ls -d      0 of 7 survived    2 of 7 survived
  (the seven awkward names, no -print0)

getconf ARG_MAX                       1048576            2097152
xargs --show-limits                   unrecognized       prints the budget

Empty input is the classic one. Hand GNU xargs nothing and it runs your command anyway, once, with no arguments — find /nonexistent | xargs rm -rf /tmp/build is the shape of accident that follows. BSD xargs does not. -r (--no-run-if-empty) makes GNU behave like BSD and is accepted by BSD too, so write -r and it is portable; omit it and your script has two behaviours.

The second row is the same catastrophe with two different numbers, which is worth seeing precisely because neither is "safe": the default pipeline mangles the names on both platforms, it just mangles a different number of them.

If you are coming from Python or ABAP

Python: subprocess.run([cmd, *names]) is xargs with none of its problems — the list is passed as a list, so there is no separator, no quoting pass, and no re-parsing. That is the general lesson rather than a Python one: xargs exists because a pipe carries bytes and a command line wants a list, and every bug on this page is in the conversion between the two. When you must shell out, pass a list and never a string. os.scandir() entries go straight into that list, so the round trip through text never happens. If you hit ARG_MAX for real, that is subprocess raising OSError: [Errno 7] Argument list too long — the same budget, reported honestly instead of silently split.

ABAP (Not machine-checked — CI cannot run ABAP.) There is no xargs, and the nearest equivalent is building a command string for SXPG_COMMAND_EXECUTE or an external OS command — which is exactly the unsafe direction this page argues against, because you are constructing a command line by concatenation and every quoting rule above is now yours to get right. Prefer passing a file of names and having the external program read it, which is the tar -T - shape; and remember that the additional-parameters string is bytes on the far side, so the code page of what you concatenate matters.

Try it

  1. touch "O'Brien.txt" in an empty directory, then find . -type f | xargs ls. Read the error.
  2. The same with -print0 | xargs -0 ls.
  3. : | xargs echo RAN on a Mac and on a Linux box. One of them prints RAN.
  4. Make twenty files with accented names and twenty with ASCII names of the same character length, then find -print0 | xargs -0 -s 200 echo | wc -l on each. The difference is the encoding deciding your control flow.

Practice

Four filenames, and how many survive. Create files called plain.txt, two words.txt, O'Brien.txt and quote".txt. Predict how many lines come out of find files -type f | xargs -I{} echo {} — and what xargs's exit status is.

Then say what xargs splits its input on (it is more than newlines), why -print0 | xargs -0 is not a refinement but the only correct form, and what the second correct form is.

Answers

Verified output of xargs_kata_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

THE FOUR FILES
   files/O'Brien.txt
   files/plain.txt
   files/quote".txt
   files/two words.txt

1. NAIVE: find | xargs
   lines out: 1 of 4        xargs exit: 1
   The apostrophe opens a quote that never closes, so xargs consumes the
   rest of the input looking for the end of it and then reports an
   unterminated quote. One filename broke the whole pipeline -- not just
   its own line.

2. WHY: xargs SPLITS ON MORE THAN NEWLINES
      a
      b
      c
      d
   One line in, four arguments out. xargs splits on spaces and tabs as
   well as newlines, so 'two words.txt' was already two files before the
   quote problem started.

3. THE FIX, AND IT IS NOT A REFINEMENT
   find -print0 | xargs -0    lines out: 4 of 4
   NUL is the one byte a filename cannot contain -- the kernel's own API
   cannot express it -- so it is the only delimiter that can never occur
   in the data. Every other separator is a guess about what people do not
   name their files.

4. THE OTHER CORRECT FORM, WITH NO xargs AT ALL
   find -exec ... \;          lines out: 4 of 4
   find hands the name to exec directly, so nothing ever parses it as
   text. Use -exec ... + when you want them batched; it is the same
   safety with one process instead of four.

WHAT THIS IS REALLY ABOUT
   A filename is a bag of bytes with exactly two forbidden values: 00 and
   2f. Everything else -- spaces, quotes, newlines, escapes -- is legal,
   and every tool that treats a list of filenames as TEXT has to invent a
   convention the filesystem never agreed to.

See also