bash Parameter Expansion — Understanding ${var:-x} and Related Syntax

bash Parameter Expansion — Understanding ${var:-x} and Related Syntax

What Is Parameter Expansion?

bash parameter expansion (${var}) goes far beyond simple variable substitution. You can set defaults, extract substrings, strip prefixes and suffixes by pattern, perform replacements, and change case — all without spawning external processes like sed or awk.

Full Cheat Sheet

Syntax Behavior
${var} Expand variable (braces avoid ambiguity)
${var:-val} Return val if unset or empty (no assignment)
${var:=val} Assign and return val if unset or empty
${var:+val} Return val if set and non-empty
${var:?msg} Print msg to stderr and exit if unset or empty
${#var} Length of the variable's value
${var:N} Substring from offset N to end
${var:N:L} Substring from offset N with length L
${var#pat} Remove shortest prefix matching pat
${var##pat} Remove longest prefix matching pat
${var%pat} Remove shortest suffix matching pat
${var%%pat} Remove longest suffix matching pat
${var/pat/rep} Replace first match of pat with rep
${var//pat/rep} Replace all matches of pat with rep
${var/#pat/rep} Replace pat only if anchored at start
${var/%pat/rep} Replace pat only if anchored at end
${var^} Uppercase first character (Bash 4+)
${var^^} Uppercase all characters (Bash 4+)
${var,} Lowercase first character (Bash 4+)
${var,,} Lowercase all characters (Bash 4+)

Default Value Patterns (:- / := / :+ / :?)

These four operators handle the "what if the variable isn't set?" problem cleanly.

${var:-val} — Return val if unset or empty

The most common operator. Returns val when var is unset or empty. Does not modify var.

name=${1:-"world"}
echo "Hello, ${name}!"
# No argument → Hello, world!
# Argument "Alice" → Hello, Alice!
LOG_DIR=${LOG_DIR:-/var/log/myapp}
echo "Logging to: ${LOG_DIR}"

${var:=val} — Assign val if unset or empty

Like :-, but also assigns val to var. The variable retains the value for subsequent use.

: ${TMPDIR:=/tmp}
echo "${TMPDIR}"   # /tmp (if it was unset)

:= does not work with positional parameters ($1, $2, etc.). Use :- for those.

${var:+val} — Return val if set and non-empty

The inverse of :-. Returns val only when var is set and non-empty.

VERBOSE=1
msg="Done${VERBOSE:+ (verbose mode)}"
echo "$msg"   # Done (verbose mode)

unset VERBOSE
msg="Done${VERBOSE:+ (verbose mode)}"
echo "$msg"   # Done

${var:?msg} — Exit with error if unset or empty

Enforces required variables. Prints msg to stderr and exits with status 1.

#!/bin/bash
set -e
DB_HOST=${DB_HOST:?"DB_HOST is not set"}

Combine with set -u for strictest variable discipline; :? adds a custom message that set -u alone cannot provide.

String Length and Substring Extraction

${#var} — String Length

str="Hello, World"
echo ${#str}   # 12

${var:N:L} — Substring

Extract L characters starting at offset N. Omit L to go to the end.

date_str="2026-06-02"
echo ${date_str:0:4}   # 2026  (year)
echo ${date_str:5:2}   # 06    (month)
echo ${date_str:8}     # 02    (day)

For negative offsets (count from end), write ${var: -2} with a space, or ${var:(-2)}. Without the space, ${var:-2} is the "default value" operator, not a negative offset.

str="Hello"
echo ${str: -3}   # llo (last 3 characters)

Pattern Removal (# / ## / % / %%)

Ideal for stripping path components and file extensions without calling basename or dirname.

Remove prefix (# / ##)

  • ${var#pat} — remove shortest prefix matching pat
  • ${var##pat} — remove longest prefix matching pat
path="/home/user/docs/report.txt"

echo ${path#*/}    # home/user/docs/report.txt  (removes leading /)
echo ${path##*/}   # report.txt                 (removes everything up to last /)

Portable basename alternative:

file="/home/user/docs/report.tar.gz"
echo ${file##*/}   # report.tar.gz

Remove suffix (% / %%)

  • ${var%pat} — remove shortest suffix matching pat
  • ${var%%pat} — remove longest suffix matching pat
file="report.tar.gz"

echo ${file%.*}    # report.tar  (removes last extension)
echo ${file%%.*}   # report      (removes all extensions)

Portable dirname alternative:

path="/home/user/docs/report.txt"
echo ${path%/*}    # /home/user/docs

Patterns support globs. ${var#*_} removes up to and including the first _; ${var##*_} removes up to and including the last _.

Pattern Replacement (/ / //)

${var/pat/rep} — Replace first match

msg="foo bar foo"
echo ${msg/foo/baz}    # baz bar foo

${var//pat/rep} — Replace all matches

msg="foo bar foo"
echo ${msg//foo/baz}   # baz bar baz

Anchored replacement

str="foo-bar-foo"
echo ${str/#foo/baz}   # baz-bar-foo  (only at start)
echo ${str/%foo/baz}   # foo-bar-baz  (only at end)

Patterns are glob patterns, not regular expressions. For regex matching, use [[ $str =~ pattern ]].

Case Conversion (Bash 4+)

These operators require Bash 4.0 or later. macOS ships with Bash 3.x by default; use brew install bash or zsh to access these features.

Syntax Behavior Example
${var^} Uppercase first char helloHello
${var^^} Uppercase all helloHELLO
${var,} Lowercase first char HELLOhELLO
${var,,} Lowercase all HELLOhello
name="alice"
echo ${name^}    # Alice
echo ${name^^}   # ALICE

Practical Recipes

Get and swap a file extension

file="image.png"
ext=${file##*.}        # png
base=${file%.*}        # image
new="${base}.webp"     # image.webp

Script argument defaults

#!/bin/bash
ENV=${1:-production}
PORT=${2:-8080}
echo "Starting: env=${ENV} port=${PORT}"

Enforce required environment variables

#!/bin/bash
: ${API_KEY:?"API_KEY is not set. Run: export API_KEY=..."}
: ${DB_URL:?"DB_URL is not set"}

Extract domain from a URL

url="https://example.com/path/to/page"
no_proto=${url#*://}     # example.com/path/to/page
domain=${no_proto%%/*}   # example.com

Normalize log level to lowercase

level="WARNING"
echo ${level,,}   # warning

Safe filename (replace spaces with underscores)

filename="my report 2026.txt"
safe=${filename// /_}   # my_report_2026.txt

Next Reading