Bash & Shell Scripting for Engineers

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.

KEY CONCEPT

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, jq where 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.

PRO TIP

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 to Alice
  • ' and ' — single-quoted, literal
  • "$last" — double-quoted, interpolates to Smith

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?

WARNING

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

'single quotes'everything literal$var: literal\n: literal*: literalno word splitno globUSE FOR:SQL, sed scripts,regex with $"double quotes"interpolate, no splitting$var: expanded$(cmd): expanded\n: still literalno word splitno globUSE FOR:the default.almost every variable.no quoteseverything enabled$var: expanded$(cmd): expanded\n: literalWORD SPLITGLOBUSE FOR:deliberate splittingor globbing only

Quiz

KNOWLEDGE CHECK

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.