open atlas
↑ Back to track
Command line CLI · 08 · 01

Variables and quoting

Variables in bash hold strings; quoting determines whether the shell expands, word-splits, or passes values literally. Unquoted variables are split on whitespace and glob-expanded — the source of most beginner script bugs.

CLI Middle ◷ 20 min
Level
FoundationsJuniorMiddleSenior

A shell script that works perfectly on a directory named logs silently destroys data on a directory named my logs — two words, one space, one catastrophe. The culprit is almost always an unquoted variable. Bash processes every unquoted $VAR through two extra steps: word splitting (split the value on IFS, usually whitespace) and glob expansion (expand any *, ?, [...] patterns). Double quotes suppress both. Single quotes suppress everything including variable expansion itself. Understanding which quoting form to use, and when, is the single biggest jump from “writes shell scripts” to “writes correct shell scripts.”

Goal

After this lesson you can assign and read shell variables correctly, use "$VAR" to prevent word splitting and globbing, use ${VAR} for unambiguous expansion, use $() for command substitution, and choose between single and double quotes deliberately.

1

Assign a variable with no spaces around =. Any space makes bash interpret it as a command.

name="Alice"       # correct
name = "Alice"     # WRONG — bash sees command 'name' with args '=' and '"Alice"'

Read it back with $name or ${name}. The braces are required when the variable name is immediately followed by more text:

prefix="back"
echo "${prefix}up"   # prints: backup
echo "$prefixup"     # prints nothing — shell looks for variable named 'prefixup'

Always prefer ${VAR} in scripts. The braces cost nothing and prevent silent mis-expansion.

2

Unquoted $VAR undergoes word splitting then glob expansion. Word splitting breaks the value on every character in $IFS (default: space, tab, newline). Glob expansion then replaces any *, ?, or [...] tokens with matching filenames.

files="report 2024.txt"
rm $files           # bash splits on space → runs: rm report 2024.txt (two args!)
rm "$files"         # correct — one argument: "report 2024.txt"

The unquoted form tries to remove two files: report and 2024.txt. Neither exists, so you get errors. If a file named report did exist, it would be deleted. The quoted form correctly targets one file.

3

Double quotes "..." suppress word splitting and globbing but still expand variables. They are the right default for script variables.

dir="/home/alice/my projects"
ls "$dir"          # correct — one argument to ls
ls $dir            # WRONG — ls gets two args: /home/alice/my and projects

Inside double quotes: $VAR, ${VAR}, $(cmd), and \ escapes still work. Everything else is literal.

echo "Today is $(date +%F)"   # expands date subshell, prints: Today is 2024-11-15
echo "Cost: \$100"             # \$ escapes the dollar sign — prints: Cost: $100
4

Single quotes '...' suppress everything — no variable expansion, no substitution. The value is taken completely literally.

echo '$HOME'        # prints the literal string: $HOME
echo "$HOME"        # prints: /home/alice

Single quotes are useful when you need a string that contains special characters and you want zero processing:

grep -E '^[0-9]+\.[0-9]+' file.txt     # regex needs no expansion — single quotes safe
pattern='^[0-9]+\.[0-9]+'
grep -E "$pattern" file.txt             # stored in variable — double quotes correct

You cannot embed a single quote inside single quotes. To include one, close the string, write \', and reopen: 'it'\''s' produces it's.

5

$(command) runs a command in a subshell and substitutes its stdout. It replaces the older backtick syntax `command` which is harder to nest and read.

today=$(date +%F)
echo "Backup date: $today"

# Nesting is clean with $()
count=$(wc -l < $(ls -t *.log | head -1))
echo "Latest log has $count lines"

Always quote command substitution results when assigning to a variable that will be used later, or when passing them as arguments: "$(cmd)". The output of $(cmd) undergoes word splitting and globbing just like any unquoted value.

Worked example

Safe file backup with a datestamped name.

#!/usr/bin/env bash
src="$1"
dest_dir="$2"
stamp=$(date +%Y%m%d_%H%M%S)
dest="${dest_dir}/${stamp}_$(basename "$src")"
cp "$src" "$dest"
echo "Backed up: $src$dest"

Every variable is quoted. $(date ...) and $(basename "$src") use command substitution. "$src" inside basename is itself quoted — filenames with spaces survive the entire pipeline. If src were unquoted anywhere, a filename like my report.txt would silently split into two arguments.

Why this works

On macOS, bash is version 3.2 (released 2006) because Apple ships the last GPLv2 version. All quoting rules in this lesson work identically on bash 3.2. The differences only surface in advanced features like associative arrays (declare -A, bash 4+) and mapfile/readarray. For portability, install bash 5 via Homebrew (brew install bash) and use #!/usr/bin/env bash so scripts pick up whichever bash is first in $PATH.

Common mistake

A common mistake is quoting the assignment itself: name="$(whoami)" is correct, but "name=$(whoami)" makes the entire string a literal command name — bash tries to run a command called name=alice. Assignment never needs the outer quotes; only the right-hand side value needs quoting if it contains spaces or special characters. Another trap: export "VAR=value" looks harmless but is non-portable; write export VAR="value" instead.

Check yourself
Quiz

A script contains: f='my file.txt'; rm $f. What does bash actually execute?

Recap

Shell variables hold plain strings. Reading them unquoted triggers word splitting (on $IFS) and glob expansion — the root of most quoting bugs. Double quotes ("$VAR") expand the variable but suppress splitting and globbing; use them as the default for any value that might contain spaces. Single quotes ('literal') suppress all expansion — useful for regex patterns and fixed strings. ${VAR} braces prevent ambiguity when the name is adjacent to other text. $(cmd) runs a command and substitutes its stdout; quote the result when passing it further. The rule is simple: when in doubt, double-quote.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 4 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.