Server

Bash Parameter Expansion Cheatsheet

By Prabath Thalangama· October 24, 2026· 3 min read
#bash#scripting#reference

Introduction

Bash's ${...} parameter expansion does string manipulation without spawning sed, cut, awk, basename, or dirname. It's faster (no fork) and, once learned, more readable. This is the practical subset.

Defaults and presence

${var:-default}     # use "default" if var is unset or empty; var unchanged
${var:=default}     # same, but also ASSIGN default to var
${var:+alt}         # use "alt" if var IS set and non-empty, else nothing
${var:?message}     # print "bash: var: message" to stderr and exit if unset/empty
port="${PORT:-8080}"                       # config with fallback
: "${CONFIG_DIR:=/etc/myapp}"              # ensure it's set for the rest of the script
flags="${VERBOSE:+--verbose}"             # add a flag only when VERBOSE is set
input="${1:?usage: deploy.sh <environment>}"

Drop the colon (${var-default}) to trigger only on unset (an empty value counts as set).

Length

${#var}             # length of the string
${#array[@]}        # number of elements
[ "${#password}" -ge 12 ] || die "password too short"

Substrings

${var:offset}          # from offset to end
${var:offset:length}   # length chars from offset
${var: -4}             # last 4 chars (note the space — ${var:-4} is the default form!)
${var: -4:2}
year="${date_str:0:4}"
last_octet="${ip##*.}"        # see pattern removal below — better for this

Pattern removal (the basename/dirname/extension trio)

${var#pattern}      # remove SHORTEST match from the FRONT
${var##pattern}     # remove LONGEST match from the front
${var%pattern}      # remove SHORTEST match from the END
${var%%pattern}     # remove LONGEST match from the end
path="/var/log/nginx/access.log.1"
echo "${path##*/}"      # access.log.1     (basename)
echo "${path%/*}"       # /var/log/nginx   (dirname)
echo "${path%.*}"       # /var/log/nginx/access.log   (strip last extension)
echo "${path##*.}"      # 1                (last extension only)

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

Mnemonic: # is left of $ on the keyboard (front), % is right (end).

Search and replace

${var/pattern/replacement}     # first match
${var//pattern/replacement}    # all matches
${var/#pattern/replacement}    # only if it matches at the START (anchored)
${var/%pattern/replacement}    # only if it matches at the END
${var//pattern/}               # delete all matches
csv="a,b,c,d"
echo "${csv//,/ }"             # a b c d
branch="feature/JIRA-123-thing"
echo "${branch//\//-}"        # feature-JIRA-123-thing   (escape the / in the pattern)
echo "${filename// /_}"       # spaces to underscores

Case conversion (bash 4+)

${var^}     ${var^^}      # first char / all to UPPER
${var,}     ${var,,}      # first char / all to lower
${var~~}                  # swap case
env="${ENVIRONMENT,,}"                 # normalise "PROD" / "Prod" -> "prod"
header="${key^^}"; header="${header//-/_}"   # http-header -> HTTP_HEADER

Indirection and defaults for arrays

name="PATH"
echo "${!name}"           # value of $PATH  (indirect reference)

${!prefix*}               # names of all variables starting with "prefix"
${array[@]:2:3}           # 3 elements starting at index 2
${array[@]/%/.bak}        # append .bak to every element
# Loop over all vars named MYAPP_*
for v in "${!MYAPP_@}"; do
    printf '%s=%s\n' "$v" "${!v}"
done

Putting it together

# Rotate: access.log -> access.log.1, keeping the extension
for f in *.log; do
    [ -e "$f" ] || continue
    base="${f%.log}"
    mv -- "$f" "${base}.$(date +%F).log"
done

# Normalise a URL to a safe filename
url="https://example.com/path/to/page?x=1"
name="${url#*://}"          # strip scheme
name="${name//[^A-Za-z0-9]/_}"   # non-alnum -> _
echo "$name"                # example_com_path_to_page_x_1

Verification and troubleshooting

set -u                     # make ${var} on an unset var an error (catches typos)
bash -x script.sh          # trace expansions
  • ${var: -4} returns the wrong thing — you wrote ${var:-4} (default value "4"). The substring-from-end form needs the space.
  • Replacement not happening — the pattern is glob, not regex. . is literal, * matches anything. ${s/./_} replaces the first literal dot.
  • bad substitution error — running under sh/dash, not bash. ^^, ,,, ${!x} are bash-only. Add #!/usr/bin/env bash and run it with bash.
  • / in the pattern breaks the expansion — escape it: ${path//\//-}.
  • Case conversion does nothing — bash 3 (old macOS). Needs bash 4+.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.