Code of the Day
AdvancedShell mastery

Parameter Expansion

Master all the ${...} forms — default values, error on unset, string length, prefix/suffix stripping, substitution, and case conversion.

BashAdvanced12 min read
Recommended first
By the end of this lesson you will be able to:
  • Provide default and fallback values with :- and :=
  • Abort with an error message using :?
  • Measure string length with ${#var}
  • Strip prefixes and suffixes with ${var#}, ${var##}, ${var%}, ${var%%}
  • Replace substrings with ${var/old/new} and ${var//old/new}
  • Convert case with ${var^^} and ${var,,}

— the ${...} forms — is Bash's built-in string manipulation layer. Reaching for awk or sed to strip a file extension or substitute a prefix is common in beginner scripts; once you know parameter expansion, most of those one-liners disappear and the remaining script is faster (no subprocess) and more readable.

Default and fallback values

These operators handle unset or empty variables without an explicit if:

# :- use a default if unset or empty
echo "${name:-Alice}"          # prints "Alice" if $name is unset or ""
echo "${count:-0}"             # safe numeric default

# := assign AND use a default (modifies the variable)
: "${tmpdir:=/tmp}"            # set $tmpdir to /tmp if not already set
echo "$tmpdir"                 # /tmp

# :+ substitute a different value if the variable IS set (non-empty)
echo "${debug:+--verbose}"     # prints --verbose only if $debug is set

# :? exit with an error message if unset or empty
: "${REQUIRED_VAR:?REQUIRED_VAR must be set}"

The :? form is the idiomatic way to make a required environment variable explicit — it produces a clear error and exits the script immediately.

Without the colon, these operators treat only unset variables specially, not empty ones. ${var-default} gives the default only if var is unset; ${var:-default} gives it if var is unset or empty. For scripts, the colon versions are almost always what you want.

String length

name="Alice"
echo "${#name}"       # 5

path="/usr/local/bin"
echo "${#path}"       # 14

arr=(a b c d)
echo "${#arr[@]}"     # 4 (array element count, not string length)

Prefix and suffix stripping

# strips from the left (prefix); % strips from the right (suffix). Doubled forms (##, %%) are greedy (strip as much as possible):

path="/home/alice/docs/report.pdf"

# Strip a prefix
echo "${path#/home/}"           # alice/docs/report.pdf  (shortest match)
echo "${path##*/}"              # report.pdf             (longest match — basename)

# Strip a suffix
echo "${path%.pdf}"             # /home/alice/docs/report  (shortest match)
echo "${path%%/*}"              # "" (longest match strips everything after first /)

This covers the two most common use cases: extracting a filename from a path (${path##*/}) and stripping a file extension (${path%.*}):

file="archive.tar.gz"
echo "${file%.*}"     # archive.tar  (strip last extension)
echo "${file%%.*}"    # archive      (strip all extensions)

Substring substitution

path="/usr/local/bin:/usr/bin"
echo "${path/bin/lib}"     # /usr/local/lib:/usr/bin  (first match only)
echo "${path//bin/lib}"    # /usr/local/lib:/usr/lib  (all matches, // is global)

# Delete by replacing with nothing
echo "${path//:/}"         # /usr/local/bin/usr/bin

Case conversion

name="alice"
echo "${name^}"     # Alice  (first character uppercase)
echo "${name^^}"    # ALICE  (all uppercase)

title="HELLO WORLD"
echo "${title,}"    # hELLO WORLD  (first character lowercase)
echo "${title,,}"   # hello world  (all lowercase)

Case conversion requires Bash 4+. Like associative arrays, ^^ and ,, are unavailable on macOS's default Bash 3.2. For portable scripts, use tr '[:lower:]' '[:upper:]' instead.

Combining expansions

Parameter expansions compose naturally:

# Extract extension and convert to lowercase
file="Report.TXT"
ext="${file##*.}"
echo "${ext,,}"    # txt

# Build a backup filename
orig="data.csv"
backup="${orig%.*}.$(date +%Y%m%d).${orig##*.}"
echo "$backup"     # data.20240115.csv

Check your understanding

  1. 1.
    path="/var/log/nginx/access.log". Which expansion gives you "access.log"?
  2. 2.
    LOGFILE="" and you run echo "${LOGFILE:-/var/log/app.log}". What is printed?
  3. 3.
    ${var/old/new} replaces all occurrences of "old" in $var.

Do it yourself

# Basename and extension extraction
path="/home/user/projects/app.tar.gz"
echo "basename: ${path##*/}"
echo "no ext:   ${path%.*}"
echo "all exts: ${path%%.*}"

# Defaults
: "${EDITOR:=nano}"
echo "Editor: $EDITOR"

# Case conversion (Bash 4+)
tag="PRODUCTION_SERVER"
echo "${tag,,}"    # production_server

# Substitute in a path
old_path="/home/olduser/data"
new_path="${old_path/olduser/newuser}"
echo "$new_path"

Where to go next

You now have the full set of ${...} tools for string manipulation without subprocesses. The next lesson — Heredocs and herestrings — covers <<EOF, <<<, and process substitution, which let you pass multi-line text and command output to programs in flexible, readable ways.

Finished reading? Mark it complete to track your progress.

On this page