Lesson 2 of 9 · 14m
Quoting and Word Splitting
Why unquoted $var bites, "$@" vs "$*", [ vs [[, and the injection angle.
A backup script that worked in testing deletes the wrong files in production because one directory name contained a space. This is the single most common real-world Bash bug — not race conditions, not permissions, just an unquoted variable. Quoting is not cosmetic; it is the difference between one argument and several.
What unquoted expansion actually does
When Bash sees $var without quotes, it performs two operations you didn't ask for:
- Word splitting: the value is split on
IFS(space, tab, newline) into separate arguments. - Glob expansion:
*,?, and[...]in the result are expanded against the filesystem.
So this:
file="Q3 report.csv"
cp $file /backups/runs cp Q3 report.csv /backups/ — three arguments, one failure. And this is worse:
rm -rf $TARGET_DIRIf TARGET_DIR is " / " or empty, you've got a different bug report than you intended.
The fix is one character class: "$var". Double quotes preserve the value as exactly one argument, no splitting, no globbing. Unquoted $var is almost never what you want — make "$var" the default and justify every exception.
"$@" versus "$*"
Passing arguments through to another command:
run_backup() {
/usr/local/bin/backup-tool "$@" # each arg arrives intact
}"$@"— each positional parameter becomes a separate word."a b" "c"stays two args."$*"— joins all args into one word withIFS[0]. Almost never right.$@unquoted — splitting and globbing. Never.
The [ ] versus [[ ]] trap
x=""
[ -z $x ] && echo empty # WRONG: [ -z ] is a one-arg test, always true
[ -z "$x" ] && echo empty # correct[[ ]] doesn't word-split its left-hand side, so [[ $x = y ]] is safe — but [[ ]] is a Bash extension, not POSIX. Both work if you quote; prefer [[ ]] inside Bash scripts for the pattern matching ([[ $f == *.log ]]) and the saner operator behavior.
Globs that turn into filenames
for f in /var/log/*.log; do
...
doneWhen nothing matches, f becomes the literal string /var/log/*.log — a filename that doesn't exist. Guard it:
shopt -s nullglob # unmatched globs expand to nothing
for f in /var/log/*.log; do ...or check [[ -e $f ]] inside the loop.
Security angle
Unquoted expansion of untrusted input is an injection vector. If any part of your script consumes user-controlled data — a filename from a request, a webhook payload — quote it everywhere, and never interpolate it into a command string you pass to sh -c.
For output, echo interprets -n, -e, and backslashes inconsistently across implementations. For untrusted or arbitrary data:
printf '%s\n' "$untrusted"Try this
touch "my file.txt"
for f in $(ls my*.txt); do echo "got: $f"; done # splits
for f in my*.txt; do echo "got: $f"; done # correctPitfalls
IFSmanipulation is real but rare — you almost never want to change it. If you do, restore it in atrap.readwithout-reats backslashes. Alwaysread -runless you specifically want backslash escapes.- Command substitution
$(cat file)strips trailing newlines — matters for exact byte comparisons.