Skip to content

Shell Quoting Rules


This chapter has been promised since the very beginning: quoting, covered in full. You’ve already seen its consequences repeatedly — a touch command splitting one filename into two, a [ ] test collapsing entirely when a variable was empty, a for loop mangling a real filename with a space in it. Every one of those was the same underlying mechanism showing up in a different place. This chapter finally covers that mechanism directly: exactly what single and double quotes do, how word splitting and pathname expansion combine to make unquoted variables genuinely dangerous, and why quoting causes more real-world Bash bugs than almost anything else in the language.

Single Quotes vs. Double Quotes, Precisely

Single quotes preserve everything, completely literally. No expansion of any kind happens inside them — not variables, not command substitution, not even backslash escaping:

name="Alice"
echo 'Hello, $name — and here is a literal backslash: \n'
Hello, $name — and here is a literal backslash: \n

Nothing was interpreted. $name stayed as four literal characters, and \n stayed as two literal characters, not a newline.

Double quotes preserve most things literally, but still allow a specific set of expansions: variable expansion ($name), command substitution ($(...)), arithmetic expansion ($((...))), and a small set of backslash escapes — \$, \`, \", \\, and a backslash immediately before a newline (for line continuation):

name="Alice"
echo "Hello, $name — today is $(date +%A)"
Hello, Alice — today is Saturday

The rule to hold onto, stated as simply as possible: single quotes turn off everything; double quotes turn off word splitting and pathname expansion, but leave $ and command substitution active.

Escaping With Backslash

Outside of any quotes, a backslash removes the special meaning of the single character right after it:

echo Price: \$5
echo First\ and\ second
Price: $5
First and second

\$ prevented $5 from being read as a (nonexistent) variable expansion. \ (backslash-space) prevented that space from acting as a word separator — without it, First\ and\ second would be three separate words instead of one string containing spaces.

This example makes the space escaping more concrete:

for name in alice bob mary; do echo $name; done

Since names are not space escaped, they are three different words as output:

alice
bob
mary

But, now with the escaped space:

for name in alice\ bob\ mary; do echo $name; done

No more word splitting:

alice bob mary

Inside double quotes, only that small set of characters mentioned above ($, `, ", \, and newline) can be escaped at all — a backslash before anything else inside double quotes is just a literal backslash, passed through unchanged. Inside single quotes, backslash has no special meaning whatsoever — it’s just a literal character, full stop.

In practice, reaching for a quote is almost always clearer than reaching for a string of backslash escapes — 'Price: $5' reads more clearly than Price:\ \$5, and scales far better once more than one or two characters need protecting.

Word Splitting In Full

You’ve seen this mechanism repeatedly since Chapter 2, and Chapter 8 formally introduced IFS as the variable controlling it. The precise rule: word splitting applies only to the result of an unquoted expansion — a variable, a command substitution, an arithmetic expansion — never to literal text you type directly, and never to anything inside quotes:

value="one two three"

for word in $value; do
    echo "[$word]"
done

for word in "$value"; do
    echo "[$word]"
done
[one]
[two]
[three]
[one two three]

Unquoted, $value’s content gets split into three separate words at each space (matching IFS’s default). Quoted, the entire value is preserved as a single word, spaces and all — exactly the behavior every earlier chapter’s fixes relied on.

Pathname Expansion Interacting With Unquoted Variables

This is the part earlier chapters only hinted at, and it compounds directly on top of word splitting: after an unquoted expansion is split into words, each resulting word is then also checked for pathname expansion (globbing) — and if it contains unescaped *, ?, or [...] and happens to match files in the current directory, it gets silently replaced with those matching filenames instead.

mkdir -p /tmp/shell-course-demo
cd /tmp/shell-course-demo
touch report.txt notes.txt secrets.txt

label=*.txt
echo Files matching pattern: $label
Files matching pattern: notes.txt report.txt secrets.txt

label was assigned the literal five characters *, ., t, x, t — assignment itself doesn’t perform pathname expansion, only later usage of an unquoted variable does. The moment $label was used unquoted in the echo command, Bash expanded it as a glob pattern against the current directory’s actual contents.

Empty Values And Quoting

This is the exact mechanism behind the [ $status == "active" ] failure from the earlier chapter, generalized: an unquoted empty variable disappears entirely — it contributes zero words, not one empty word — while a quoted empty variable is preserved as a single, empty-string word.

empty_value=""

count_words() {
    echo "Received $# word(s)"
}

count_words $empty_value
count_words "$empty_value"
Received 0 word(s)
Received 1 word(s)

Unquoted, the empty variable vanished before count_words ever saw it — zero arguments arrived. Quoted, it arrived as one genuine (if empty) argument. This distinction is exactly what broke [ $status == "active" ]: the empty, unquoted $status contributed zero words instead of one, leaving [ with a missing operand instead of an empty string to compare.

Nested Quoting

You can’t nest the same quote type inside itself — a single quote inside a single-quoted string ends that string, full stop, regardless of intent:

echo 'It's a test'
bash: unexpected EOF while looking for matching quote

or you will get this multiline completion:

echo 'It's a test'
>

You need extra ' to end the quoting.

Two common ways around this: switch quote types, since a literal single quote is perfectly fine inside double quotes (and vice versa) —

echo "It's a test"
It's a test

— or, if you specifically need single-quote semantics (no expansion at all) around text that itself contains a single quote, close the string, escape a literal quote, then reopen it:

echo 'It'\''s a test'
It's a test

That reads as three concatenated pieces with no space between them: 'It' (literal), \' (one escaped literal single quote), 's a test' (literal) — Bash concatenates adjacent quoted (and unquoted) strings with no separator automatically, which is what makes this idiom work.

Note

A single quote appearing inside a double-quoted string, or vice versa, isn’t “nesting” in any special sense — it’s just a literal character with no quoting significance at all in that context. "It's fine" works because the ' inside double quotes is just a regular character, not a quote delimiter.

Best Practices

  • Quote every variable expansion by default. Leaving one unquoted should be a deliberate choice made for a specific reason — deliberate glob matching (Chapter 7), or "${array[*]}"-style joining (Chapter 9) — never a default habit.
  • Prefer single quotes for anything with no expansion needs, and double quotes the moment you need a variable, command substitution, or arithmetic expansion inside the string.
  • Reach for a quote before reaching for a backslash escape — it’s more readable and scales better than a string peppered with individual escaped characters.
  • Treat any variable holding external or user-controlled input with extra suspicion — file contents, command-line arguments, anything not fully under your own script’s control could contain spaces, glob characters, or other content that behaves unexpectedly if left unquoted.

Shell-Safety Considerations

The *.txt example above wasn’t just a curiosity — the same mechanism, applied to a destructive command instead of echo, is one of the most well-known ways to lose data with a shell script. This section fixes the safe version directly, and explains the higher-stakes version in full without actually running it.

Naive (broken) version, continuing the example from earlier in this chapter:

cd /tmp/shell-course-demo
label=*.txt
echo Files matching pattern: $label
Files matching pattern: notes.txt report.txt secrets.txt

Corrected version — quote the expansion:

echo "Files matching pattern: $label"
Files matching pattern: *.txt

Quoting suppresses both word splitting and pathname expansion at once, so the literal text label was actually assigned comes through exactly as stored.

Warning

The same mechanism behind this harmless echo example is the reason rm -rf $some_dir/* is treated as a well-known danger sign in Bash scripting circles. If $some_dir is ever accidentally empty or unset when that line runs, the unquoted expansion vanishes (as covered under Empty Values And Quoting above), and the shell is left evaluating rm -rf /* against the filesystem root instead of the intended subdirectory — with no error, no confirmation, and no way back. This isn’t a demonstration to run, even in a throwaway environment — it’s a reason to treat every unquoted variable placed next to rm, mv, or any other destructive command as a serious red flag, and to double- and triple-check quoting specifically around anything that deletes or overwrites data.

Hands-On: Quoting, Escaping, And More

1. Set up a working directory.

mkdir -p ~/shell-course/ch11
cd ~/shell-course/ch11

2. Compare single and double quotes directly.

name="Alice"
echo 'Literal: $name'
echo "Expanded: $name"

3. Practice backslash escaping outside of quotes.

echo Price: \$5
echo First\ and\ second

4. Reproduce the empty-value word-count difference.

empty_value=""

count_words() {
    echo "Received $# word(s)"
}

count_words $empty_value
count_words "$empty_value"

5. Reproduce the combined word-splitting-plus-globbing understanding

mkdir -p /tmp/shell-course-demo
cd /tmp/shell-course-demo
touch report.txt notes.txt secrets.txt

label=*.txt
echo "--- * performing glob expansion ---"
echo Files matching pattern: $label

echo "--- * preserving it's literal meaning ---"
echo "Literal meaning: $label"

6. Practice nested quoting.

echo "It's a test"
echo 'It'\''s a test'

7. Clean up.

cd ~
rm -rf ~/shell-course/ch11
rm -rf /tmp/shell-course-demo

You’ve now seen precisely what single and double quotes each protect against, escaped characters deliberately instead of quoting, confirmed the empty-value word-count difference that broke [ ] example in earlier chapter, and reproduced — safely — the exact combined word-splitting-and-globbing mechanism behind one of Bash’s most well-known real-world data-loss patterns. The next chapter goes further into expansion itself: brace expansion, tilde expansion, parameter expansion defaults and substrings, command and arithmetic substitution, process substitution, and the order Bash actually performs all of it in.

Last updated on