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.gzRemove 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/docsPatterns 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 bazAnchored 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 | hello → Hello |
${var^^} |
Uppercase all | hello → HELLO |
${var,} |
Lowercase first char | HELLO → hELLO |
${var,,} |
Lowercase all | HELLO → hello |
name="alice"
echo ${name^} # Alice
echo ${name^^} # ALICEPractical Recipes
Get and swap a file extension
file="image.png"
ext=${file##*.} # png
base=${file%.*} # image
new="${base}.webp" # image.webpScript 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.comNormalize log level to lowercase
level="WARNING"
echo ${level,,} # warningSafe filename (replace spaces with underscores)
filename="my report 2026.txt"
safe=${filename// /_} # my_report_2026.txt