Skip to content

Safely process filenames containing spaces and newlines with find and xargs

On Linux, filenames can contain not only spaces but also newline characters. If you treat the output of find as text split on newlines or spaces, a single filename can therefore be split incorrectly into multiple arguments.

The basic rule for safely using GNU find and xargs is not to reinterpret filenames as strings. find -print0 and xargs -0 use the NUL character as a delimiter. find -exec can also pass search results directly as command arguments.

This article creates files containing spaces and newlines and verifies one-at-a-time processing, batch processing, and parallel processing.

Step 1: Create test files containing spaces and newlines

Section titled “Step 1: Create test files containing spaces and newlines”

First, create a normal filename, a filename containing a space, a filename containing a newline, and additional files for checking batch processing.

In Bash, the $'...' form interprets \n as a newline character, so you can create a filename that contains an actual newline between line and break.txt.

Terminal window
set -euo pipefail
rm -rf /tmp/find-xargs-example
mkdir -p /tmp/find-xargs-example/input
printf 'alpha\n' > '/tmp/find-xargs-example/input/plain.txt'
printf 'bravo\n' > '/tmp/find-xargs-example/input/two words.txt'
printf 'charlie\n' > $'/tmp/find-xargs-example/input/line\nbreak.txt'
for n in $(seq -w 01 12); do
printf 'batch-%s\n' "$n" > "/tmp/find-xargs-example/input/batch file ${n}.txt"
done
printf 'setup-ok\n'

This creates a total of 15 files.

It is not safe to pass plain find output directly to processing that splits on spaces or newlines. In particular, with a filename containing a newline, one visible line does not correspond to one file.

Step 2: Pass filenames safely with -print0 and xargs -0

Section titled “Step 2: Pass filenames safely with -print0 and xargs -0”

find -print0 terminates each pathname with a NUL character instead of a newline. The corresponding xargs -0 treats only NUL as the input delimiter, so spaces and newlines inside filenames are preserved.

In the following example, the shell first changes to /tmp, which is accessible to a regular user, and then runs find. This avoids depending on permission to return to the original working directory. Each file is then passed to bash individually, and the command checks that the received path refers to an existing file.

Terminal window
set -euo pipefail
cd /tmp
find /tmp/find-xargs-example/input -maxdepth 1 -type f -print0 |
xargs -0 -r -n 1 bash -c 'test -f "$1"' _
test "$(find /tmp/find-xargs-example/input -maxdepth 1 -type f -printf x | wc -c)" -eq 15
printf 'nul-pipeline-safe\n'

The _ after bash -c supplies a value for $0. The first pathname appended by xargs is received as $1.

When using -print0, the receiving side must also use the matching -0 option. Specifying only one side does not make the pipeline safe.

Step 3: Process files one at a time with find -exec

Section titled “Step 3: Process files one at a time with find -exec”

When xargs is unnecessary, find -exec is another option.

With the {} \; form, the command is run once for each pathname found. Because the pathname is passed from find directly as a command argument, it does not need to be reparsed using spaces or newlines as delimiters.

If a regular user cannot access the directory from which the command was started, find can fail when it tries to return there after executing a command. The next example therefore changes to /tmp before processing.

Terminal window
set -euo pipefail
cd /tmp
proof=/tmp/find-xargs-example/exec.count
: > "$proof"
find /tmp/find-xargs-example/input -maxdepth 1 -type f \
-exec bash -c 'test -f "$1"; printf x >> "$2"' _ {} "$proof" \;
test "$(wc -c < "$proof")" -eq 15
printf 'exec-safe\n'

For a small number of targets, or when a command must be run separately for each file, {} \; is straightforward. For a large number of files, however, starting an external command for every file increases process creation overhead.

For large numbers of files, -exec ... {} + can pass multiple pathnames to one command invocation.

To avoid depending on permissions for the original working directory, this example also changes to /tmp before running find.

The example processes every argument supplied to each command invocation with for path do. Quoting it as "$path" prevents a pathname that has already been received as one argument from being split again.

Terminal window
set -euo pipefail
cd /tmp
result=$(
find /tmp/find-xargs-example/input -maxdepth 1 -type f \
-exec bash -c '
for path do
test -f "$path"
printf x
done
' _ {} +
)
test "${#result}" -eq 15
printf 'batch-safe\n'

With {} +, find does not unconditionally put every file into a single command. It divides the pathnames across invocations according to the usable argument size, which can reduce the number of external process launches compared with {} \;.

Step 5: Run processing in parallel with xargs -P

Section titled “Step 5: Run processing in parallel with xargs -P”

If each file can be processed independently, xargs -P can run multiple commands in parallel.

To avoid depending on permissions for the original working directory, this example again changes to /tmp before running find.

The example uses -n 1 to pass one file per invocation and -P 4 to limit concurrency to at most four processes. Input still flows through -print0 and -0, so parallel execution does not change how filenames containing spaces or newlines are delimited.

Terminal window
set -euo pipefail
cd /tmp
result=$(
find /tmp/find-xargs-example/input -maxdepth 1 -type f -print0 |
xargs -0 -r -n 1 -P 4 bash -c 'test -f "$1"; printf x' _
)
test "${#result}" -eq 15
printf 'parallel-safe\n'

Parallel execution does not guarantee completion order. Do not simply add -P when output must remain in input order or when multiple processes may update the same file.

Parallelism can also increase CPU, memory, storage I/O, and load on connected services, so choose a concurrency level appropriate for the workload.

For safe filename processing, choose the method that fits the task.

  • To pass pathnames through a pipeline to another command, combine find -print0 with xargs -0
  • To run a command directly once per file, use find -exec ... {} \;
  • To process multiple files in batches, use find -exec ... {} +
  • To parallelize independent work, combine find -print0, xargs -0, and xargs -P
  • In shell code, always quote received pathnames such as "$path" and "$1"

Even when filenames are passed safely, expanding variables without quotes afterward can reintroduce word splitting and pathname expansion. It is important to preserve argument boundaries not only with NUL delimiters but throughout the receiving shell code.

After verification, remove the temporary files that were created.

Terminal window
set -euo pipefail
rm -rf /tmp/find-xargs-example
printf 'cleanup-ok\n'

When processing large numbers of filenames that may contain spaces or newlines, the fundamental rule is not to treat the pathname list as newline-delimited text. Preserve argument boundaries with NUL delimiters or find -exec, and keep pathnames quoted in subsequent shell processing to build safe handling for unusual filenames.

Category: Linux