Bash Quoting Explained: The Single Biggest Source of Shell Bugs

Bash Quoting Explained: The Single Biggest Source of Shell Bugs

More shell scripts break on quoting than on anything else. The rule is short: quote every expansion unless you have a specific reason not to. Understanding why makes it stick.

What happens without quotes

The shell performs several expansions on an unquoted variable, and two of them cause trouble.

Word splitting. The value is split on whitespace into separate words.

file="my report.pdf"
rm $file          # tries to remove "my" and "report.pdf"
rm "$file"        # correct

Glob expansion. Characters like * and ? are expanded against the filesystem.

pattern="*.txt"
echo $pattern      # lists matching files
echo "$pattern"    # prints *.txt

Neither produces an error. The command runs and does the wrong thing, which is why these bugs survive testing on filenames without spaces and then fail on real data.

The three quoting styles

Double quotes prevent word splitting and globbing while allowing variable and command substitution. This is what you want almost always.

echo "Home is $HOME, today is $(date +%A)"

Single quotes are fully literal. Nothing is expanded.

echo 'Costs $5 and uses $HOME literally'
grep 'error.*failed' file.log       # regex safe from the shell

Use single quotes for regexes, awk and sed programs, and anything containing dollar signs you want preserved.

Backslash escapes one character.

echo "The price is \$5"

Where it bites

# Command substitution needs quoting too
files=$(find . -name '*.log')      # loses filenames with spaces on use
mapfile -d '' files < <(find . -name '*.log' -print0)   # correct

# Test expressions
if [ $var = "value" ]; then        # breaks if var is empty or has spaces
if [[ "$var" == "value" ]]; then   # correct

# Array expansion
for f in ${files[@]}; do           # splits again
for f in "${files[@]}"; do         # correct

The [[ ]] form is safer than [ ] in bash because it does not word split its operands, though quoting is still the right habit. Our arrays and parameter expansion guide covers the array side.

Nested quoting

The case that confuses everyone:

ssh host "echo \"$LOCAL_VAR\" > /tmp/out"     # LOCAL expands here
ssh host 'echo "$REMOTE_VAR" > /tmp/out'      # REMOTE expands there

Which shell expands the variable depends entirely on the quoting. Getting this backwards is why remote commands so often behave strangely.

For anything complex, a heredoc is clearer:

ssh host bash <<'REMOTE'
  echo "$HOSTNAME"     # expands on the remote machine
REMOTE

Our heredocs guide covers the quoted delimiter, which is the same distinction.

The double dash

rm -- "$file"
git checkout -- "$path"

-- marks the end of options. Without it, a file named -rf or --force is interpreted as a flag rather than an operand.

This sounds theoretical. It is how a script processing user-supplied filenames turns into a very bad afternoon.

IFS

Word splitting uses IFS, which defaults to space, tab, and newline. Restricting it is a common hardening step:

IFS=$'\n\t'

Now splitting happens only on newlines and tabs, so a filename with a space survives even an unquoted expansion. It is a safety net rather than a substitute for quoting, and it appears in the template in our bash error handling guide.

Let a tool find them

shellcheck myscript.sh

ShellCheck flags unquoted expansions, missing --, useless cat, and most of the other standard mistakes, with an explanation for each. Running it over an existing script is genuinely educational and takes seconds.

Most editors integrate it. If you write shell scripts and use one tool from this article, use this one.

Frequently Asked Questions

Why do I need to quote variables?

Because the shell performs word splitting and glob expansion on unquoted expansions. A variable holding a filename with a space becomes two arguments, and one containing an asterisk expands against the current directory, both silently.

What is the difference between single and double quotes?

Single quotes are literal and prevent every expansion, so nothing inside is interpreted. Double quotes allow variable and command substitution while still preventing word splitting and globbing, which is what you want most of the time.

When should I not quote a variable?

Almost never for a plain expansion. The genuine exception is when you deliberately want word splitting, such as expanding a string of options into separate arguments, and even then an array is the better tool.

Why does my command fail on filenames with spaces?

Because an unquoted expansion split the filename into several arguments at the space. Quoting the expansion keeps it as one argument, which is the fix in essentially every case of this problem.

What does the double dash do in a command?

It marks the end of options, so anything after it is treated as an operand even if it starts with a dash. Without it, a file literally named minus-rf can be interpreted as a flag, which is how some spectacular accidents happen.

How do I find quoting bugs I have not hit yet?

Run shellcheck over your scripts. It flags unquoted expansions, missing double dashes, and most other common mistakes, and it is the single highest value tool for shell scripting.