Safely Processing Filenames with Spaces in Bash Using Null‑Delimited Loops
Why Null‑Delimited Loops Matter
In day‑to‑day shell work I often need to iterate over files that may contain spaces, newlines or other tricky characters. A simple for loop over a glob or a command substitution breaks as soon as a filename has a space because the shell splits on whitespace. The result is hard‑to‑debug bugs and, worse, accidental data loss when the script tries to move or delete the wrong pieces.
The reliable way to handle arbitrary filenames is to work with null‑delimited input. Many utilities—most notably find, grep -Z and sort -z—can produce a stream where each record ends with a NUL byte (ASCII 0). Since NUL never appears in a valid filename, it can be used as a safe separator.
The Problem with Plain Loops
Consider a directory with three files:
$ ls
'photo 1.jpg'
'photo
2.jpg'
'photo 3.jpg'
A naïve approach like:
for f in *; do
echo "$f"
done
produces three separate iterations for the first file because the shell splits on the space, and it treats the newline and tab as delimiters as well. The output is garbled.
Even using while read line fails because read by default strips leading/trailing whitespace and stops at the first newline.
Using find -print0 with while IFS= read -r -d ''
The idiom that has saved me countless headaches looks like this:
# Process every regular file under the current directory, safely
while IFS= read -r -d '' file; do
# Perform whatever action you need—here we just echo the name
echo "Processing: $file"
# Example: copy to a backup directory preserving the full name
cp -- "$file" "/backups/$(basename "$file")"
done < <(find . -type f -print0)
Let’s break down why each piece is important:
find . -type f -print0emits a NUL‑terminated list of files.- The process substitution
<( … )makes that stream appear as a temporary file descriptor. read -r -d ''tellsreadto use NUL as the delimiter (-d '') and to disable backslash escapes (-r) so that filenames containing backslashes are left untouched.- Setting
IFS=preventsreadfrom trimming leading/trailing whitespace. - The loop body receives the exact filename, ready for any command that expects a single argument.
Notice that we quote every expansion of $file. Even though the delimiter guarantees safety, quoting is still a good habit—it protects against empty strings and makes the intent clear to readers.
Alternative: Using mapfile (readarray) with a NUL delimiter
If you prefer to work with an array—for example, to pass the list to another function or to process the files in batches— Bash’s mapfile (also known as readarray) can read NUL‑terminated data directly:
# Read all matching files into an array
mapfile -t -d '' files < <(find . -type f -name '*.log' -print0)
# Now iterate over the array
for f in "${files[@]}"; do
echo "Rotating log: $f"
# Example: compress and move old logs
gzip -c "$f" > "${f}.gz" && rm "$f"
done
The -t option strips the trailing delimiter (the NUL) from each element, leaving clean strings. The -d '' again tells mapfile to use NUL as the separator. This approach is especially handy when you need to check the count (${#files[@]}) or slice the array for parallel processing.
Putting It Into a Reusable Function
To avoid repeating the boilerplate, I keep a small helper in my ~/.bashrc:
# yaml
# Usage: foreachnull
# Example: foreachnull 'echo "Processing: {}"' . -type f
foreachnull() {
local cmd="$1"
shift
# The remaining arguments are passed straight to find
while IFS= read -r -d '' item; do
# Replace {} with the current item, mimicking find -exec
local expanded="${cmd//{}/\"$item\"}"
eval "$expanded"
done < <(find "$@" -print0)
}
Now a one‑liner like foreachnull 'mv "{}" archive/' . -name '*.bak' moves all backup files safely, regardless of spaces or newlines.
When to Reach for This Pattern
- Any script that consumes output from
find,grep -Z,sort -zor similar tools. - Batch renaming, moving, copying or deleting files where names are not under your control.
- Processing logs or data files that may have been generated by users or other systems.
- When you need to pass a list of filenames to another command via an array (use
mapfile -t -d '').
Remember: the shell’s word‑splitting is a feature, not a bug—when you control the input. When the input comes from the outside world, always use a delimiter that cannot appear in the data, and NUL is that delimiter for filenames.