Variables and quoting look trivial in any other language. In Bash they are the source of more bugs than any other feature. The rules are short and worth memorising.
Assignment
NAME=alice # OK
COUNT=42 # OK
NAME = alice # BROKEN — Bash thinks "NAME" is a command
GREETING="hello" # quotes optional for simple values
PATH_LIST="/usr/bin:/usr/local/bin"
No spaces around =. The shell would otherwise treat the name as a command and fail.
Use
echo $NAME
echo "$NAME" # better — handles spaces and globs
echo "${NAME}" # explicit braces — required when joined
echo "${NAME}_backup" # without braces, shell looks for variable NAME_backup
Single vs Double Quotes
NAME=alice
echo "Hello, $NAME" # Hello, alice
echo 'Hello, $NAME' # Hello, $NAME (literal)
echo "Today: $(date)" # command substitution works
echo 'Today: $(date)' # literal
- Single quotes — everything is literal. No expansion. No escapes.
- Double quotes — variables and command substitutions expand; backslash escapes special chars.
- No quotes — also expands, but also splits on whitespace and globs filenames. Usually wrong.
Why Quote, Always
FILE="my report.txt"
rm $FILE # tries to delete two files: "my" and "report.txt"
rm "$FILE" # correct — one file with a space in its name
The single most important rule in Bash: quote your variable expansions. Always. Even when you think the value is simple. ShellCheck (covered later) catches missing quotes for you.
Command Substitution
NOW=$(date +%Y-%m-%d)
COUNT=$(grep -c error /var/log/app.log)
USER=$(whoami)
LIST="$(ls *.txt)"
echo "Today is $NOW"
Use $(...), not the legacy backticks `...`. The new form nests cleanly and is easier to read.
Arithmetic
N=$((3 + 4))
((N++))
((N += 2))
echo "$N"
if (( N > 5 )); then
echo "big"
fi
Bash arithmetic is integer-only. For floats use bc or awk.
Default Values and Substitutions
echo "${NAME:-anonymous}" # use anonymous if NAME unset/empty
echo "${NAME:=anonymous}" # also assign default
echo "${NAME:?must be set}" # error and exit if unset
echo "${NAME:+is set}" # alt value if NAME is set
echo "${PATH#*:}" # strip shortest prefix matching pattern
echo "${FILE%.txt}" # strip suffix
echo "${FILE//foo/bar}" # replace all foo with bar
These parameter expansions replace many uses of sed and external tools. Worth memorising the most common five: :-, :?, #, %, //.
Arrays
FILES=(a.txt b.txt "c with space.txt")
echo "${FILES[0]}" # first element
echo "${FILES[@]}" # all elements
echo "${#FILES[@]}" # count
for f in "${FILES[@]}"; do # always quote
echo "$f"
done
Use arrays whenever you have a list of values. Storing a list as a space-separated string and later splitting it is a common source of bugs.
Environment vs Shell Variables
FOO=bar # shell variable — only in this shell
export FOO # promote to environment — child processes see it
# or in one line:
export FOO=bar
env # show environment
echo "$FOO"
unset FOO
Configuration that subprocesses need (DATABASE_URL, API keys) must be exported. Pure scripting state need not be.
Special Variables
| Variable | Meaning |
|---|---|
$0 | script name |
$1..$9 | positional arguments |
$# | argument count |
$@ | all args (use as "$@" to keep them separate) |
$* | all args as one string |
$? | exit code of last command |
$$ | PID of the shell |
$! | PID of last background process |
Heredocs
cat <<EOF
Hello, $USER
Today is $(date)
EOF
cat <<'EOF' # quoted EOF — no expansion
This is literal: $USER
EOF
Useful for embedding multi-line strings — config files, SQL, here-document arguments to other commands.
Read
read -p "Name: " NAME
read -s -p "Password: " PASS # silent
echo
echo "Hello, $NAME"
Common Quoting Bugs
# Bug: spaces in path
DIR=/tmp/my data
ls $DIR # tries to list /tmp/my and "data"
# Fix:
DIR="/tmp/my data"
ls "$DIR"
# Bug: empty variable becomes nothing
rm -rf $TARGET/ # if TARGET="", this is rm -rf /
# Fix:
[ -n "$TARGET" ] || { echo "TARGET unset"; exit 1; }
rm -rf "$TARGET"/
Cert Mapping
| Cert | Scope |
|---|---|
| RHCSA / LFCS | Variables, environment, sourcing config files |
| AWS SAA | User data scripts, parameter store environment |
The next lesson covers control flow — the if, while, and for that turn a list of commands into a program.