iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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.

Example

Example
name="Ada"
echo "Hello, $name"
echo "Date: $(date +%F)"
Try it Yourself »

Discussion

Loading…