Why unquoted variables break, and what to do instead
Shell quoting is not a style preference. An unquoted variable is a word-splitting instruction, and a filename with a space in it is enough to turn a working script into a broken one or an exploitable one.
By Enrique M. · 9 min read · Last reviewed 2026-10-18
The default behaviour is to split
An unquoted variable is expanded and then split on whitespace, and each resulting piece is treated as a separate argument. A filename with a space, a glob character or a newline in it therefore does not survive.
The same expansion also expands globs, so a variable containing `*` turns into a list of files, and a variable containing `~` expands to a home directory. Neither is usually what the author intended.
file="my report.txt"
cat $file # cat: 'my' 'report.txt' — two arguments
cat "$file" # cat: my report.txt — one argument, correct Single quotes, double quotes, and the difference
Single quotes protect everything: no expansion of variables, no command substitution, no globbing, no backslash escapes except a literal quote. The contents are completely literal.
Double quotes protect the *structure* — the value stays one argument — but the content is still expanded: variables, command substitution and backslash escapes are all processed inside them.
That difference is the one that bites. A value that contains a dollar sign is safe in single quotes and dangerous in double quotes, because in double quotes the shell will try to expand whatever follows the dollar sign.
# Literal, always safe
name='$HOME and `whoami`'
echo "$name" # $HOME and `whoami`
# Expanded, not what you wanted
name='$HOME'
echo "$name" # /root Command substitution runs code
Backticks and `$(...)` run the command inside them and substitute the output. In a double-quoted string the substitution still happens, which means untrusted data can end up being executed.
This is the mechanism behind a whole family of injection bugs. A parameter that contains `$(rm -rf ...)` and is interpolated into a command inside double quotes will be executed.
input='$(touch /tmp/pwned)'
eval "echo $input" # creates the file
echo "$input" # safe, prints the literal text Passing a value to another command safely
The correct way to hand a value to another program is as a separate argument, quoted, with the command called directly. That is the shape that avoids the shell entirely and is immune to every problem above.
If you truly must build a command as a string, the safe way is to use `printf %q`, which quotes a value so that the shell will read it as exactly one argument, or to stop building strings and use an array.
# Correct: separate argument, quoted
rm -- "$file"
# Correct: an array, no re-parsing
cmd=(rm -- "$file")
"${cmd[@]}"
# Correct: a format string, never interpolation
printf 'Processing %s\n' "$file" Things that are not quoting
A backslash before a space works in an interactive shell, but it is fragile, unreadable and does not survive being put in a variable. It also does nothing inside single quotes, and inside double quotes it only escapes a small set of characters.
Braces, `${var}` and `$var` are not quoting. Brace expansion happens before any other expansion and turns `{a,b}` into two words. `${var}` and `$var` are the same expansion, and both of them split and glob the result unless they are inside double quotes.
- `\ ` escapes a space, but not inside single quotes, and not reliably inside double quotes.
- `{a,b}` expands to two words, before variables are even looked at.
- `${var}` and `$var` behave identically. Neither of them quotes.
What to take away
- Always double-quote a variable expansion unless you specifically want splitting and globbing.
- Single quotes are fully literal; double quotes still expand variables and command substitutions.
- Never build a command by interpolating untrusted data into a string.
- Pass values as separate arguments, and use an array or `printf %q` when a string is unavoidable.