Bash Safe Filenames
Bash safe filenames are filenames handled so spaces, globs, and newlines cannot break a script. Quoting and null-delimited reads keep each name whole.
Quoting keeps a spaced name whole
The quoted loop sees one name. The unquoted loop sees two words, because the space split the name.
printf 'one\ntwo\n' > "my file.txt"
for f in "my file.txt"; do echo "quoted: $f"; done
for f in my file.txt; do echo "split: $f"; doneQuoting stops a star from becoming a glob
The quoted name prints as itself. The unquoted pattern expands to every matching file, including the literal star file.
printf 'x\n' > data1.txt
printf 'x\n' > data2.txt
printf 'x\n' > 'data*.txt'
echo "quoted: data*.txt"
printf '%s\n' data*.txtA newline name needs a null-delimited read
The line-based read counts the newline name twice, once per line. The null-delimited read counts it once, because null bytes never appear in names.
: > split.count
: > whole.count
printf 'x\n' > $'weird\nname.txt'
find . -maxdepth 1 -name '*.txt' -print | while IFS= read -r f; do echo hit >> split.count; done
find . -maxdepth 1 -name '*.txt' -print0 | while IFS= read -r -d '' f; do echo hit >> whole.count; done
echo "newline split count: $(wc -l < split.count)"
echo "null-delimited count: $(wc -l < whole.count)"Example
printf 'a\n' > "my notes.txt"
printf 'b\n' > $'odd\nname.txt'
find . -maxdepth 1 -name '*.txt' -print0 | while IFS= read -r -d '' f; do cp "$f" "$f.bak"; done
echo "backups made: $(find . -maxdepth 1 -name '*.bak' -print0 | tr -cd '\0' | wc -c)"Key points
- Quote every expansion: "$file" keeps a name with spaces as one word.
- Unquoted names split: an unquoted $file breaks at each space.
- Unquoted globs expand: an unquoted name containing * matches files instead of naming one.
- Newlines hide in names: a filename can contain a newline, so line-based reads split it.
- Null-delimited reads: find -print0 with read -d '' splits names only at null bytes.
- Filenames are data: never let a name be re-parsed as a pattern or a command.
In this example, the script creates two awkward names, one with a space and one with a newline. A null-delimited find loop copies each name, quoted, to a backup. The final count proves both backups exist.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Which read handles a filename containing a newline correctly?