Parameter expansion and string manipulation

Default values and assignment forms, substring and length expansion, prefix and suffix removal with patterns, case conversion, and building paths and names safely.

Defaults, errors and alternatives

: "${API_URL:?API_URL must be set}"     # abort with your own message
name=${1:-guest}                          # use the default if unset OR empty
count=${COUNT:=10}                        # default, and assign it for later
quiet=${VERBOSE:+no}                      # 'no' when VERBOSE is set and non-empty
raw=${MAYBE_BLANK-unchanged}              # '-' form: only an UNSET variable triggers it

# the ':' forms also treat the empty string as missing; without it they do not
echo "${EMPTY:-fallback}"     # fallback
echo "${EMPTY-fallback}"      # (empty line: the variable exists, so it is used)
FormEffect
${var:-default}Value, or the default if unset or empty
${var:=default}Same, and assigns the default back to the variable
${var:?message}Exit with the message if unset or empty
${var:+alt}The alternative only when the variable is set and non-empty
${var-} / ${var+}Same, but an empty value counts as present
  • ${VAR:?} is the cheapest argument validation in bash: it turns a missing setting into an immediate, named failure.
  • Avoid := for configuration you only read; the surprise mutation makes a later default look like it came from the environment.

Substrings, patterns and case

file=/var/log/app/current.log

echo "${#file}"        # 22      length in characters
echo "${file:0:4}"     # /var    offset 0, 4 characters
echo "${file: -3}"     # log     negative offset needs the leading space
echo "${file:9}"       # app/current.log

echo "${file##*/}"     # current.log      strip longest  prefix matching */
echo "${file#*/}"      # var/log/...      strip shortest prefix matching */
echo "${file%/*}"      # /var/log/app     strip shortest suffix matching /*
echo "${file%%.*}"     # /var/log/app/current  strip longest suffix matching .*

name="Deploy Failed (retry)"
echo "${name,,}"       # deploy failed (retry)
echo "${name^^}"       # DEPLOY FAILED (RETRY)
echo "${name// /_}"    # Deploy_Failed_(retry)   replace every match
echo "${name/ /_}"     # Deploy_Failed (retry)   replace the first match
echo "${name//[()]/}"  # Deploy Failed retry     classes work, like globs

declare -u upper="prod" ; echo "$upper"   # PROD, via an attribute not an operator
OperatorMeaning
${#var}Length
${var:off:len}Substring by offset
${var#pat} / ##Remove the shortest / longest matching prefix
${var%pat} / %%Remove the shortest / longest matching suffix
${var/a/b} / //Replace the first / every occurrence
${var,,} / ${var^^}Lowercase / uppercase (bash 4)

The patterns are globs, not regular expressions: *, ? and [a-z]. Use #/% for path surgery and reach for sed only when you truly need a regex.

Building names and paths safely

# derive names from a single source of truth
input=/data/invoices/2026-08.csv
base=${input##*/}                 # 2026-08.csv
stem=${base%.*}                   # 2026-08
ext=${base##*.}                   # csv
dir=${input%/*}                   # /data/invoices

out="${dir%/}/processed/${stem}.done.${ext}"
printf '%s
' "$out"               # /data/invoices/processed/2026-08.done.csv

# normalise an optional trailing slash before joining
root=${TARGET_DIR:-/srv/app}
root=${root%/}
path="$root/logs/app.log"

# guard the case that breaks naive code
case "$input" in
    */*) : ;;                      # has a directory
    *)   die "expected a path, got '$input'" ;;
esac
⚠️
Never build a path by concatenating a variable straight onto a slash without removing a possible trailing slash: $dir/logs with dir=/srv/app/ quietly becomes /srv/app//logs. Strip it once with ${dir%/} and the problem disappears.

FAQ

Why did ${var:-x} not apply my default?
Because the variable was set but empty, and you used a form without the colon. Use the colon forms for "unset or empty" and the bare forms when empty is a meaningful value.
How do I strip a file extension?
${file##*.} gives the extension and ${file%.*} removes the shortest suffix starting at a dot. Handle names with no dot explicitly — ${file%.*} would otherwise remove the whole name.

Functions, arrays and associative arrays Text processing: grep, sed and awk in scripts

Last refreshed 2026-09-18.