Quoting and Pitfalls
Stages 1 and 3 compressed for lookup. Lesson 1 covers quoting itself; lesson 4 covers the footguns that survive even careful quoting. This sheet is the quoting rule and the specific pitfalls it doesn't fix on its own.
Quoting
An unquoted $var or $(command) expansion undergoes two more steps before the shell uses it: word splitting (breaking the result on $IFS characters, space/tab/newline by default) and pathname expansion / globbing (replacing a word containing *, ?, or [ with matching filenames).
| Form | Word splitting | Globbing | Variable/command expansion | Use for |
|---|---|---|---|---|
Unquoted $var | Yes | Yes | Yes | Almost never, for a value that might hold whitespace or a glob character |
"$var" | No | No | Yes | The default: quote every expansion unless you specifically want splitting or globbing |
'$var' | No | No | No | The literal text $var itself, not its value (an awk script, documenting a variable name) |
Where it bites. file="notes final.txt"; rm $file splits into two words, notes and final.txt, deleting neither the intended file nor necessarily anything sensible. A variable holding a value with * in it, expanded unquoted, can silently expand to every matching file in the directory: rm $pattern with an attacker- or accident-influenced $pattern is a well-known way to delete far more than intended.
Tooling catches this class of bug automatically. ShellCheck's SC2086 flags a missing quote around an expansion before the script ever runs.
Pitfalls that survive careful quoting
| Pitfall | Looks fine because | Actually breaks because | Fix |
|---|---|---|---|
for f in $(ls *.txt) | No obviously unquoted variable | $(ls *.txt) is itself an unquoted command substitution; its whole output is word-split regardless of individual filenames | for f in *.txt; do ...; done, letting the shell's own globbing produce the file list directly |
An unmatched glob (*.txt in an empty directory) | "For each matching file" reads as "skip if none match" | Bash passes the literal, unmatched pattern through as one word by default; the loop runs once with f set to the literal string *.txt | shopt -s nullglob, so an unmatched glob expands to nothing |
Iterating a properly quoted capture (for word in $output) | "$(command)" was quoted at capture time | The quoting at capture protects the string's content, but an unquoted loop over it reintroduces word splitting on $IFS | Read line-by-line (while IFS= read -r line) or use an array, instead of splitting the captured string with a bare for |
while read line; do ...; done < file | Looks like the natural way to read a file line by line | No -r means backslashes are interpreted specially; no IFS= means leading/trailing whitespace is stripped per line | while IFS= read -r line; do ...; done < file |
Before trusting a loop or a quoted expansion
- [ ] Every variable and command substitution expansion is double-quoted, unless there is a specific, commented reason for word splitting or globbing.
- [ ] No loop's file list comes from
$(ls ...); a bare glob (for f in *.ext) is used instead. - [ ]
nullglobis set wherever an unmatched glob should mean "nothing," not "the literal pattern." - [ ] Any
while readloop uses both-randIFS=. - [ ] ShellCheck has been run against the script, with
SC2086warnings resolved.