If you only ever properly learn one thing about bash, make it this module. Quoting is not style. It is the difference between a script that works and a script that deletes the wrong directory when someone puts a space in a filename. Nearly every shell bug you will ever hit — and a large share of the shell-related CVEs ever filed — is a quoting bug wearing a disguise.
The shell is a word-splitting machine
A command line is not a string that gets passed to a program. It is a list of words, and bash decides where the boundaries are. Programs receive an array of arguments; the shell's job is to build that array. Everything in this module is about controlling how it does that.
file="my report.txt"
rm $file # TWO words: rm "my" "report.txt" -- deletes the wrong things
rm "$file" # ONE word: rm "my report.txt" -- correctRead that until it is boring. The variable holds one value in both cases. What differs is whether bash was allowed to split it after substituting it.
The expansion order, precisely
When bash processes a word it performs these steps in this order:
- Brace expansion —
{a,b}becomesa b - Tilde expansion —
~becomes your home directory - Parameter expansion —
$varbecomes its value - Command substitution —
$(cmd)becomes cmd's output - Arithmetic expansion —
$((1+1))becomes 2 - Word splitting — the result is chopped on
IFScharacters - Filename expansion (globbing) —
*.txtbecomes matching filenames
Steps 6 and 7 happen after your variable's value has been pasted in, and double quotes are how you switch them off. That is the entire mechanism.
Two consequences people find surprising, both of which follow directly from the ordering:
- Brace expansion is first, so it cannot use variables:
{1..$n}does not work, because$nhas not been substituted yet when braces are processed. - Globbing is last and operates on the result of substitution, so a variable containing
*will expand to filenames when used unquoted. If a filename can contain an asterisk, an unquoted variable is a landmine.
IFS: the thing doing the splitting
IFS (Internal Field Separator) is the variable holding the characters bash splits on. Its default is space, tab, and newline. Change it and you change what "a word" means:
line="root:x:0:0:root:/root:/bin/bash"
IFS=':' read -r user pass uid gid gecos home shell <<< "$line"
echo "$user" # root
echo "$shell" # /bin/bashSetting IFS on the same line as the command scopes the change to that one command only — a very useful idiom, and much safer than assigning it globally and forgetting to restore it.
The three kinds of quoting
| Form | Substitution? | Splitting / globbing? | Use for |
|---|---|---|---|
'single' | No — everything literal | No | Literal text, regexes, awk programs |
"double" | Yes — $var, $(cmd) still expand | No | Almost everything else |
bare | Yes | Yes — split and globbed | Almost nothing, deliberately |
Inside single quotes there is no escape mechanism at all — you cannot backslash-escape a single quote. To include one, end the string, add an escaped quote, and start again: 'it'\''s'. It looks absurd and it is the standard idiom.
"$@" versus "$*" versus $@
This is the highest-value distinction in the language, and it is worth memorising outright:
show() { printf '[%s]' "$@"; echo; }
set -- "one two" "three" # two arguments
show "$@" # [one two][three] <- correct: each arg preserved
show $@ # [one][two][three] <- split on whitespace, now three args
show "$*" # [one two three] <- joined into ONE arg"$@"expands to each argument as a separate, intact word. This is what you want, essentially always."$*"joins all arguments into a single word, separated by IFS's first character. Useful when you deliberately want one string.- Unquoted
$@or$*re-splits and globs every argument. Almost always a bug.
The same distinction applies verbatim to arrays in Module 07: "${arr[@]}" versus "${arr[*]}".
Where quoting actually bites
# 1. Command substitution splits too
files=$(ls) # any filename with a space is now several words
# 2. Empty variables VANISH when unquoted
name=""
[ -n $name ] # becomes: [ -n ] -- which is TRUE. Bug.
[ -n "$name" ] # becomes: [ -n "" ] -- correctly false
# 3. A leading dash makes your value look like a flag
user_input="-rf"
rm "$user_input" # quoted, and STILL parsed as a flag
rm -- "$user_input" # -- ends option parsing. Now it is a filename.
# 4. Globs inside variables expand on use
pattern="*.txt"
echo $pattern # lists your .txt files
echo "$pattern" # prints the literal string *.txtCase 3 deserves emphasis because quoting alone does not save you. Quoting controls word splitting; it does not stop a program from interpreting a leading dash as an option. Any time a value could begin with -, terminate the option list with --. This is a real attack surface, not a theoretical one.
The rule
Quote every expansion, every time, unless you have a specific reason not to. Not "when it might contain a space" — always, because you will be wrong about which values can contain spaces. The two legitimate reasons to leave something unquoted are: you genuinely want globbing, or you genuinely want splitting. Both are rare, and both deserve a comment saying so. Run shellcheck; SC2086 is exactly this warning and it is right far more often than you are.
Takeaways
- The shell builds an argument list. Quoting is how you control where the word boundaries land.
- Word splitting and globbing happen after substitution — which is precisely what double quotes suppress.
"$@"preserves each argument;"$*"joins them into one; unquoted re-splits both. Use"$@".- Unquoted empty variables disappear entirely, silently changing the meaning of a test.
- Quoting does not stop a leading dash being read as a flag — use
--to end option parsing.
Try it: survive hostile arguments
The graders here pass arguments containing spaces, glob characters, empty strings and leading dashes — the exact inputs that break unquoted code. A naive implementation passes the easy test and fails the rest, which is precisely the point.
Loading starter code…
Checkpoint · Quoting & Expansion
5 questions · pass at 70%
f="my report.txt". Why does rm $f try to delete two files?[ -n $name ] true even when name is the empty string?rm "$input". Are you safe?for i in {1..$n} fail to produce a range?Finished Module 01?
Passing the quiz marks it complete automatically. Or mark it yourself.