Quoting Rules You Actually Need
Bash has three kinds of quoting: single, double, and none. Each has a specific job. Picking the wrong one is the source of most "but this worked yesterday" bugs.
This lesson is the short, practical reference: what each quoting style does, when to reach for which, and the real-world scenarios where the difference matters.
Single quotes: literal. Double quotes: interpolate variables and command substitution. No quotes: subject to everything. Three sentences covers 95% of the rules.
The three styles
name="World"
echo 'Hello, $name' # Hello, $name (single — literal)
echo "Hello, $name" # Hello, World (double — interpolate)
echo Hello, $name # Hello, World (none — interpolate + split)
The first two produce one argument each; the third gets word-split.
Single quotes — everything is literal
Inside '...', every character is literal. Even the backslash. Even the dollar sign. Even a newline.
echo '$PATH is $PATH' # $PATH is $PATH
echo 'backslash: \n' # backslash: \n (no newline interpretation)
echo 'one
two' # one (literal newline preserved)
# two
The only thing single quotes cannot contain is another single quote. You cannot escape it:
echo 'can\'t' # Parse error
echo 'can'\''t' # "can't" — close, escape, reopen
echo "can't" # "can't" — just use double quotes instead
When to use single quotes
- SQL queries and JSON bodies with lots of
$characters. - Regular expressions that use
$anchors. - Passing literal strings to
sed,awk,jqwhere you don't want Bash variables to interpolate. - Writing config snippets where
$is part of the target syntax.
# SQL with column references that look like Bash variables
query='SELECT $1, $2 FROM users WHERE id = $3'
psql -c "$query"
# sed with a $ anchor (end of line)
sed 's/foo$/bar/' file.txt
# jq with string literals
echo '{"a":1}' | jq '.a'
Double quotes — most of the time
Inside "...":
- Variable expansion happens:
"$var"and"${var}". - Command substitution happens:
"$(cmd)". - Arithmetic happens:
"$((expr))". - Word splitting does NOT happen.
- Pathname expansion does NOT happen.
- Escapes like
\$,\",\\, ``` work.
name="Alice"
echo "Hello, $name!" # Hello, Alice!
echo "today is $(date +%Y)" # today is 2026
echo "next: $((count + 1))"
echo "literal dollar: \$42" # literal dollar: $42
When to use double quotes
- Any string that contains a variable. Always.
- Any string that contains a command substitution.
- Any argument to a command where spaces in the value matter.
cp "$src" "$dst"
grep "$pattern" "$file"
echo "Processing $count files from $dir..."
The default for everything should be double quotes unless you have a specific reason to use single quotes or no quotes.
If you are ever unsure, reach for double quotes. They are almost never wrong. Single quotes are for the specific case where you want literal $/\. No quotes is for the specific case where you want word splitting or globbing.
Mixed quoting — concatenation without concatenation
In Bash, adjacent strings are concatenated, with no operator between them:
first="Alice"
last="Smith"
echo "$first"' and '"$last"
# Alice and Smith
Breaking it down:
"$first"— double-quoted, interpolates toAlice' and '— single-quoted, literal"$last"— double-quoted, interpolates toSmith
Result: Alice and Smith — one single argument to echo.
This technique is the idiomatic way to mix quoting inside a single argument. You will use it most often for embedding single quotes inside strings:
# A string containing a literal single quote
echo 'it'"'"'s' # it's
# A SQL query with an embedded variable
query='SELECT * FROM users WHERE name = '"'$name'"
# SELECT * FROM users WHERE name = 'Alice'
# (but please use a parameterized query instead!)
The $'...' form — escape sequences in a literal
A Bash extension: $'...' is like single quotes but processes C-style escapes:
echo $'line1\nline2' # line1
# line2
echo $'tab\there' # tab here
echo $'\x41' # A (hex escape)
echo $'\u2603' # ☃ (unicode escape)
Useful for literals that need a tab, newline, or non-printable character. Much cleaner than embedding raw characters into a string.
Quoting in specific contexts
Conditionals — [[ ... ]]
Inside [[ ]], variable expansion does NOT word-split (this is a Bash-specific exception):
file="my report.txt"
if [[ -f $file ]]; then # works — no word splitting inside [[]]
echo "exists"
fi
if [ -f $file ]; then # BROKEN — word splits to [, -f, my, report.txt, ]
echo "exists"
fi
Inside [[ ]], you can omit quotes on variables. In [ ] (the POSIX test), you cannot. This is one of many reasons to prefer [[ ]] over [ ].
case statements
case does not word-split either:
case $var in # unquoted fine
foo) ... ;;
bar) ... ;;
esac
Though, quoting the pattern matters — it disables glob matching if you want a literal:
case $file in
*.txt) echo "text" ;; # unquoted pattern — glob match
"*.txt") echo "literal star" ;; # quoted — literal match only
esac
Arithmetic — (( ))
Inside (( )), you don't need $ on variable names, and no quoting needed for variable references:
count=0
(( count++ ))
(( count += 5 ))
if (( count > 10 )); then echo "big"; fi
Quotes inside (( )) are rarely needed.
Command substitution
Both inside and around the substitution:
# Quote the whole substitution to prevent word splitting of its output
file="$(ls -t | head -1)"
# Inside, quote variables as usual
echo "Latest: $(ls -t "$DIR" | head -1)"
The real-world mistakes
1. Not quoting in a loop over filenames
# Broken if any filename has a space
for f in $(ls /tmp); do
rm $f
done
# Correct
for f in /tmp/*; do # prefer glob over ls
rm "$f"
done
2. Interpolating in a single-quoted context
# Does not do what you think
echo 'Running as $USER' # prints: Running as $USER (literal)
# Use double quotes
echo "Running as $USER" # prints: Running as alice
3. Missing quotes on $* or $@
# BROKEN: word-splits each argument
echo $@
# CORRECT: preserves each argument
echo "$@"
4. Trying to store a full command in a string
# Tempting but broken
cmd="grep 'hello world' myfile.txt"
$cmd # word-splits into [grep, 'hello, world', myfile.txt]
# Use an array
cmd=(grep "hello world" myfile.txt)
"${cmd[@]}" # runs: grep "hello world" myfile.txt
SSH, Docker, and other "remote" quoting
Commands that run a shell on the other side (ssh, docker exec, kubectl exec -- sh -c) have TWO layers of quoting: your local shell, and the remote shell.
# Run `ls -la /tmp` remotely
ssh server "ls -la /tmp" # OK
# Run `ls -la $HOME` remotely (remote $HOME)
ssh server 'ls -la $HOME' # OK: single quotes pass through literal
# Run `ls -la $HOME` remotely (LOCAL $HOME)
ssh server "ls -la $HOME" # OK: double quotes expand locally first
# Remote `grep "my pattern" file.txt`
ssh server "grep 'my pattern' file.txt" # quotes survive the trip
The first question to ask: which side's variables do I want?
ssh joins all remote arguments back into a single string and re-parses them with the remote shell. This double-parsing is why remote command quoting is so error-prone. When in doubt, single-quote the whole remote command and inject variables carefully.
A worked example — building a safe ssh wrapper
A wrapper that runs a command on a remote host, safely passing arguments:
run_remote() {
local host="$1"
shift
# "$@" now holds the command and its arguments
# Build a remote command safely.
# printf '%q ' quotes each argument for safe shell parsing.
local remote_cmd
remote_cmd=$(printf '%q ' "$@")
ssh "$host" "$remote_cmd"
}
run_remote server.example.com ls -la "/path with spaces"
# Sends: ssh server.example.com "ls -la /path\ with\ spaces"
# Remote shell parses this safely.
printf '%q' is Bash's "shell-quote this string" operator. It produces output that, when parsed by a shell, yields the original string back. Useful when you genuinely have to build a shell command.
Quoting rules summary
Quiz
Which of these correctly prints the literal dollar-PATH label followed by the actual value of PATH?
What to take away
- Single quotes: everything is literal. Use for SQL, sed, regex with
$anchors. - Double quotes: interpolate variables and
$(), but no word splitting or globbing. Use for everything else. - No quotes: interpolate AND word split AND glob. Use only when you deliberately want splitting or globbing.
- Default to double quotes. When in doubt, double-quote.
- Concatenate by adjacency:
"$a"'literal'"$b". $'...'processes escape sequences (\n,\t,\x41,\u2603) inside a literal.[[ ]],case, arithmetic(( )), and assignments do not word-split — quoting is less critical there, but quoting never hurts.printf '%q'safely shell-quotes a string for later reuse — useful for building remote commands.
Next module: control flow done right — conditionals, loops, and functions.