Parameter Expansion
${var} is the basic form, but bash has a rich set of ${var:-...}, ${var//pat/repl}, ${var#prefix} modifiers that replace whole sed/awk calls. Memorizing the common ones makes scripts shorter and faster.
Defaults
${var:-default} # if var unset or empty: yield "default" (var unchanged)
${var:=default} # if var unset or empty: ASSIGN var="default", yield it
${var:+alternate} # if var IS set and non-empty: yield "alternate", else empty
${var:?error} # if var unset or empty: print error, exit script
| Form | Empty var |
Unset var |
Set var |
|---|---|---|---|
${var-X} |
yields "" |
yields X |
yields value |
${var:-X} |
yields X |
yields X |
yields value |
${var=X} |
nothing | sets var=X | nothing |
${var:=X} |
sets var=X | sets var=X | nothing |
${var+X} |
yields X |
yields "" |
yields X |
${var:+X} |
yields "" |
yields "" |
yields X |
${var?msg} |
nothing | exit with msg | nothing |
${var:?msg} |
exit with msg | exit with msg | nothing |
The : variant treats empty as unset. Without :, only truly unset triggers the alternative.
Practical:
PORT="${PORT:-8000}" # default 8000
DEBUG="${DEBUG:=false}" # set + default
[[ "${VERBOSE:+yes}" == "yes" ]] && echo "verbose" # if VERBOSE is set
: "${API_KEY:?must set API_KEY}" # require it or die
The last idiom (: "${var:?msg}") is the canonical “require this env var” check at script start.
Length
name="hello"
echo "${#name}" # 5
arr=(a b c d)
echo "${#arr[@]}" # 4 — array length
Substring
str="Hello, World!"
echo "${str:7}" # World! — from index 7 to end
echo "${str:7:5}" # World — from index 7, length 5
echo "${str: -6}" # World! — last 6 chars (note SPACE before -)
echo "${str: -6:5}" # World — last 6, then take 5
Negative offset needs the leading space (or parentheses) so bash doesn’t confuse it with ${var:-default}.
Prefix / suffix removal
file="path/to/myfile.txt.bak"
echo "${file#*/}" # to/myfile.txt.bak — shortest leading match removed
echo "${file##*/}" # myfile.txt.bak — longest leading match (basename!)
echo "${file%.*}" # path/to/myfile.txt — shortest trailing match removed
echo "${file%%.*}" # path/to/myfile — longest trailing match
| Form | Removes |
|---|---|
${var#pat} |
shortest match from start |
${var##pat} |
longest match from start |
${var%pat} |
shortest match from end |
${var%%pat} |
longest match from end |
Mnemonic: # is left of % on the keyboard → start; % is right → end. Doubled = greedy.
These replace basename, dirname, and many sed calls:
# Without parameter expansion:
basename "/path/to/file.txt" # file.txt
dirname "/path/to/file.txt" # /path/to
# With parameter expansion (no subprocess fork):
file="/path/to/file.txt"
echo "${file##*/}" # file.txt
echo "${file%/*}" # /path/to
For tight loops with thousands of files, the no-fork form matters.
Replacement
str="hello world world"
echo "${str/world/EARTH}" # hello EARTH world — first match
echo "${str//world/EARTH}" # hello EARTH EARTH — all matches
echo "${str/#hello/HI}" # HI world world — anchored at start
echo "${str/%world/EARTH}" # hello world EARTH — anchored at end
| Form | Replaces |
|---|---|
${var/pat/repl} |
first match |
${var//pat/repl} |
all matches |
${var/#pat/repl} |
match anchored at start |
${var/%pat/repl} |
match anchored at end |
If repl is omitted, the match is deleted:
str="hello world"
echo "${str// /_}" # hello_world
echo "${str/world/}" # hello (trailing space)
Case modification (bash 4+)
name="Alice"
echo "${name,,}" # alice — all lowercase
echo "${name^^}" # ALICE — all uppercase
echo "${name,}" # alice — first char to lower
echo "${name^}" # Alice — first char to upper
Replaces tr '[:upper:]' '[:lower:]'.
Indirection
var="name"
name="alice"
echo "${!var}" # alice — value of variable named by var
Useful for poor-man’s lookup tables. For real lookups use associative arrays (declare -A).
Array slicing
arr=(a b c d e f)
echo "${arr[@]:2}" # c d e f — from index 2
echo "${arr[@]:2:3}" # c d e — from 2, length 3
echo "${arr[@]: -2}" # e f — last 2 (note space)
A practical example
Parsing a path without forking:
filepath="/var/log/myapp/server.log.2024-01-15"
dir="${filepath%/*}" # /var/log/myapp
filename="${filepath##*/}" # server.log.2024-01-15
basename="${filename%.*}" # server.log
extension="${filename##*.}" # 2024-01-15
date_part="${filename##*.}" # 2024-01-15
Compared to:
dir=$(dirname "$filepath")
filename=$(basename "$filepath")
basename=$(basename "$filepath" | sed 's/\.[^.]*$//')
# ... 4 forks
Use case: trim whitespace
str=" hello world "
trimmed="${str#"${str%%[![:space:]]*}"}" # remove leading
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}" # remove trailing
Ugly. Honestly, just use xargs for trimming or write the script in Python.
Use case: convert env var fallback chain
db_host="${MY_DB_HOST:-${DEFAULT_DB_HOST:-localhost}}"
Cascading defaults — first set wins, ultimately falling back to localhost.
Common interview confusions
- “
${var-default}is the same as${var:-default}.” —:includes empty as “missing”. Without:, only truly unset triggers the default; an empty string yields empty. - “
${var#prefix}removes a literal prefix.” —prefixis a glob pattern.${var#a*}removes everything from start through the firsta*match. - “
${str/pat/repl}replaces all occurrences.” — only the first. Use${str//pat/repl}for all.
Interview angle
- “How do you set a default value for an env var in bash?” —
${VAR:-default}to use a default without setting;${VAR:=default}to default and assign;${VAR:?error}to require it (exit if missing). - “How do you get the basename / dirname without forking?” —
${path##*/}for basename,${path%/*}for dirname. Faster than$(basename ...)in tight loops. - “How do you replace all occurrences of a pattern in a variable?” —
${var//pat/repl}(double slash). Single slash replaces only the first. - “How do you check that an env var is set, otherwise exit?” —
: "${API_KEY:?must be set}"at the top of the script. Idiomatic and one line. - “
${var#pat}vs${var##pat}?” —#removes the shortest match;##removes the longest. Same with%/%%for trailing. - “How do you strip a file extension in bash?” —
${filename%.*}removes the shortest trailing.*match (sofile.tar.gzbecomesfile.tar). Use%%.*for everything before the first dot.