LinuxXXXV · Shell Scripting for SysadminsVariables
Variables and quoting - the foundation of safe shell scripts
What you'll learn
- Use variables safely
- Quote every variable expansion
- Recognise common quoting pitfalls
- Use special variables correctly
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09
Variables and quoting are the foundation of safe shell scripts. Almost every shell bug is a variable or quoting bug. This lesson covers the rules.
Variable basics
NAME="alice"
echo "$NAME" # alice
echo ${NAME} # alice (with braces)
Variables are case-sensitive. Convention: lowercase for variables, uppercase for constants and environment variables.
Special variables
| Variable | Meaning |
|---|---|
$0 | Script name |
$1, $2, … | Positional parameters |
$# | Number of positional parameters |
$@ | All positional parameters (each quoted separately) |
$* | All positional parameters (single string) |
$? | Exit status of last command |
$$ | PID of current shell |
$! | PID of last background job |
$- | Current shell options |
Use $@ for arguments, $? for exit status, $$ for the
PID.
Quoting rules
- Double quotes (
"..."): allow variable expansion and command substitution. - Single quotes (
'...'): literal; no expansion. - No quotes: subject to word splitting and globbing.
FILE="my file.txt"
cat $FILE # WRONG: word-split, becomes "cat my file.txt"
cat "$FILE" # CORRECT: "cat my file.txt"
Always quote variable expansions. The only exceptions are specific cases where you want word splitting (rare).
Common pitfalls
# Word splitting: a scalar cannot be iterated safely at all
LIST="a b c"
for arg in $LIST; do echo "$arg"; done # splits on IFS - breaks on spaces
for arg in "$LIST"; do echo "$arg"; done # ALSO WRONG - runs exactly once
read -r -a items <<<"$LIST" # scalar -> array, explicitly
for arg in "${items[@]}"; do echo "$arg"; done # RIGHT
# Unquoted: glob expansion
[ -f $FILE ] # WRONG: glob expands
[ -f "$FILE" ] # RIGHT
# Remote execution: quoting locally does not protect the remote shell
dir="/srv/my data"
ssh user@host "rm -rf $dir" # WRONG: remote shell re-splits it
ssh user@host "rm -rf -- $(printf '%q' "$dir")" # RIGHT: quoted for both shells
Three traps in that block are worth spelling out.
Quoting does not fix a for-loop. for arg in "$LIST"
does not iterate the words - it iterates a single item that
happens to be the whole string, so the body runs exactly
once. shellcheck flags it as SC2066. The fix is not a
quote, it is a different data type: build an array.
"$arr" is element zero, never the whole array. If
LIST really is an array, "$LIST" expands to ${LIST[0]}
and silently discards the rest. Always "${arr[@]}".
A command sent over SSH is parsed twice. The local quotes
decide what string is transmitted; the remote login shell
then parses that string again. Spaces, $, backticks and
; in a variable are live metacharacters on the far end.
printf '%q' produces a form that survives the second parse.
Arrays
# Define
NAMES=("alice" "bob" "carol")
NAMES+=("dave") # append
# Iterate
for name in "${NAMES[@]}"; do
echo "$name"
done
# Access
echo "${NAMES[0]}" # alice
echo "${NAMES[@]}" # all elements
echo "${#NAMES[@]}" # count
"${NAMES[@]}" (quoted with @) expands each element
separately - safe.
The @ and the quotes are both load-bearing:
NAMES=("alice" "bob smith" "carol")
for n in "${NAMES[@]}"; do echo "[$n]"; done # RIGHT: 3 iterations
# [alice] [bob smith] [carol]
for n in ${NAMES[@]}; do echo "[$n]"; done # WRONG: unquoted, word-split
# [alice] [bob] [smith] [carol]
for n in "${NAMES[*]}"; do echo "[$n]"; done # WRONG: joins into one word
# [alice bob smith carol]
Same rule as "$@" versus "$*". Reach for "${array[@]}"
every time; the other two forms are for the rare case where
you actually want splitting or a single joined string.
Default values
${VAR:-default} # use default if VAR is unset or empty
${VAR:=default} # set default if VAR is unset or empty
${VAR:?error} # error if VAR is unset or empty
${VAR:+alt} # use alt if VAR is set and non-empty
Command substitution
DATE=$(date)
COUNT=$(ls | wc -l)
Modern bash: $(command) is preferred over backticks.
Knowledge check
Knowledge check · 3 questions
Q1. What is the difference between "$@" and "$*"?
Q2. Unquoted variable expansion is safe in modern bash.
Q3. Which of the following are valid bash quoting patterns? Select all that apply.
Passing score: 75%. Answers are checked in this browser.