Variables & Quoting
Bash variables look simple but have rules you must know: scope (export), quoting (single vs double), parameter expansion (${var:-default}), arrays, associative arrays, and the readonly + local modifiers. Get these right and your scripts stop misbehaving on whitespace, empty values, and odd characters.
Quoting, expansions, arrays, scope, and the gotchas
EXAMPLE
#!/usr/bin/env bash
set -Eeuo pipefail
# ===== 1) Assignment + use =====
name=alice # NO spaces around =
echo "$name" # always quote when expanding
# ===== 2) Quoting matters =====
phrase='hello $name' # single quotes -> literal $name
phrase="hello $name" # double quotes -> expansion
phrase="path: $(pwd)" # $(...) command substitution
# Most subtle bug in shell: unquoted expansion + filename with spaces
files=(a.txt 'b c.txt' d.txt)
for f in "${files[@]}"; do # quoted + [@] -> each element as one word
echo "[$f]"
done
# Without quotes: 'b c.txt' becomes two arguments. ALWAYS quote.
# ===== 3) Parameter expansion =====
echo "${name}" # explicit braces (good for clarity)
echo "${name:-anon}" # default if unset OR empty
echo "${name-anon}" # default if unset (empty is fine)
echo "${name:=anon}" # set + use default if unset/empty
echo "${name:?required}" # FAIL with msg if unset or empty
echo "${name:+yes}" # 'yes' if name is set + non-empty
# Length
echo "${#name}" # 5
# Substring
echo "${name:1:3}" # 'lic'
# Substitution
echo "${name/li/LI}" # aLIce (first)
echo "${name//e/X}" # aliceX (all)
# Prefix / suffix trim
path='/var/log/app.log'
echo "${path%.log}" # /var/log/app (remove shortest suffix)
echo "${path%%.*}" # /var/log/app (longest)
echo "${path#/var/}" # log/app.log (shortest prefix)
echo "${path##*/}" # app.log (basename)
# Case
echo "${name^^}" # ALICE
echo "${name,,}" # alice
# ===== 4) Arrays =====
declare -a fruit=('apple' 'pear' 'kiwi')
echo "${fruit[1]}" # pear
echo "${fruit[@]}" # apple pear kiwi
echo "${#fruit[@]}" # 3
fruit+=('mango') # append
unset 'fruit[1]' # remove index 1 (gap remains)
for f in "${fruit[@]}"; do echo "$f"; done
# ===== 5) Associative arrays (bash 4+) =====
declare -A user
user[name]='alice'
user[email]='alice@example.com'
echo "${user[name]} <${user[email]}>"
# Iterate keys
for k in "${!user[@]}"; do
echo "$k = ${user[$k]}"
done
# ===== 6) Scope =====
GLOBAL='outside'
func() {
local LOCAL_VAR='inside' # local to the function
GLOBAL='changed' # mutates the outer GLOBAL
echo "$LOCAL_VAR / $GLOBAL"
}
func
echo "$GLOBAL" # 'changed'
# Always 'local' inside functions to avoid leaking variable names.
# ===== 7) Export — make a variable visible to child processes =====
DB_URL='postgres://localhost/shop'
export DB_URL # now child processes see it
# OR all-in-one
export DB_HOST='localhost'
# Without export, the variable exists ONLY in this shell, not in the
# python/node/ruby script you invoke.
# ===== 8) Readonly =====
readonly PI=3.14
PI=4 # error: readonly variable
# ===== 9) Special variables =====
$0 name of the script
$1..$9 positional arguments
$@ all args as separate quoted words
$* all args as one string
$# argument count
$$ current process PID
$? last exit status
$! last background PID
# ===== 10) Pitfalls =====
# - Unquoted $var around whitespace -> split into multiple words
# - 'rm -rf $dir/' when $dir is empty -> 'rm -rf /' (catastrophic)
# Defence: 'rm -rf -- "${dir:?refusing empty}/"'
# - Forgetting 'local' in functions -> variables leak to outer scope
# - Using 'declare' inside a function without local -> still global
# - assignment with spaces around = (var = value) -> tries to RUN 'var'
# - Using $* when you mean "$@" -> word-splits args
# ===== 11) ShellCheck =====
# Install shellcheck and run it on every script.
# It catches almost every quoting / variable bug above automatically.
shellcheck script.sh
# ===== 12) Decision rules =====
# - 'local' inside every function unless you mean to mutate global state
# - Double-quote every expansion unless you mean to split
# - Use parameter expansion defaults instead of if-checking unset
# - Arrays for lists, not space-separated strings
# - 'set -Eeuo pipefail' at the top of every real script
Why it matters
Always run scripts through `shellcheck`. It is the single tool that catches 90% of the quoting, expansion, and "Im not sure why this hangs on filenames with spaces" bugs that bash punishes hours later. Add it to CI; treat warnings like compile errors.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…