backend / linux bash / 04_parameter_expansion.md

Parameter Expansion

6 interview angles 5 min read source

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.”prefix is a glob pattern. ${var#a*} removes everything from start through the first a* 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 (so file.tar.gz becomes file.tar). Use %%.* for everything before the first dot.