Bash Parameter Expansion Tricks

September 27, 2026

scripts system-administration

Parameter expansion is one of the most useful features in bash that many scripters underuse. Instead of shelling out to sed, awk, or cut for basic string operations, parameter expansion handles them directly in the shell. It is faster, more readable, and avoids spawning extra processes.

Default Values

These are essential for writing scripts that handle missing variables gracefully.

Use a Default (:-)

Return a default if the variable is unset or empty. The variable itself is not modified.

echo "${name:-anonymous}"      # prints "anonymous" if $name is unset/empty
echo "${PORT:-8080}"           # common pattern for configurable ports

Assign a Default (:=)

Same as above, but also assigns the default to the variable.

echo "${tmpdir:=/tmp}"         # sets $tmpdir to /tmp if it was unset/empty
echo "$tmpdir"                 # /tmp

Error If Unset (:?)

Terminate the script with an error message if the variable is missing. Useful for required configuration.

: "${DATABASE_URL:?DATABASE_URL must be set}"
: "${API_KEY:?API_KEY is required}"

The : command is a no-op. It evaluates the expansion (triggering the error if needed) without doing anything else.

Use Alternate Value (:+)

Return a value only if the variable IS set. Useful for conditional flags.

verbose=""
curl ${verbose:+--verbose} https://example.com

verbose="yes"
curl ${verbose:+--verbose} https://example.com    # adds --verbose flag

String Length

Get the length of a string without wc or expr.

path="/var/log/syslog"
echo "${#path}"                # 15

Works with arrays too, returning the number of elements.

files=("a.txt" "b.txt" "c.txt")
echo "${#files[@]}"            # 3

Substring Extraction

Extract parts of a string by position with ${var:offset:length}.

timestamp="2026-09-27T14:30:00"

echo "${timestamp:0:10}"       # 2026-09-27  (first 10 chars)
echo "${timestamp:11}"         # 14:30:00    (from position 11 to end)
echo "${timestamp:5:2}"        # 09          (2 chars starting at position 5)

Negative offsets count from the end. Note the space before the minus sign to distinguish from :- (default value).

filename="backup-2026-09-27.tar.gz"
echo "${filename: -6}"         # tar.gz (last 6 characters)

Prefix and Suffix Removal

These are probably the most frequently used parameter expansions. They strip matching patterns from the beginning or end of a string.

Remove Shortest Prefix (#)

path="/home/user/documents/report.txt"
echo "${path#*/}"              # home/user/documents/report.txt

Remove Longest Prefix (##)

path="/home/user/documents/report.txt"
echo "${path##*/}"             # report.txt  (like basename)

Remove Shortest Suffix (%)

filename="report.txt"
echo "${filename%.*}"          # report

Remove Longest Suffix (%%)

filename="archive.tar.gz"
echo "${filename%%.*}"         # archive

Practical: Path Manipulation Without basename/dirname

These expansions replace basename and dirname entirely.

filepath="/var/log/nginx/access.log"

# Get filename (like basename)
echo "${filepath##*/}"         # access.log

# Get directory (like dirname)
echo "${filepath%/*}"          # /var/log/nginx

# Get extension
echo "${filepath##*.}"         # log

# Get filename without extension
name="${filepath##*/}"
echo "${name%.*}"              # access

Practical: Batch Rename Files

Rename all .txt files to .md without a loop over external commands.

for f in *.txt; do
    mv "$f" "${f%.txt}.md"
done

Change a prefix on backup files.

for f in old_*.sql; do
    mv "$f" "new_${f#old_}"
done

Search and Replace

Replace First Match (/)

msg="hello world hello"
echo "${msg/hello/goodbye}"    # goodbye world hello

Replace All Matches (//)

msg="hello world hello"
echo "${msg//hello/goodbye}"   # goodbye world goodbye

Replace at Beginning (/#)

path="src/main.rs"
echo "${path/#src/lib}"        # lib/main.rs

Replace at End (/%))

file="data.csv"
echo "${file/%.csv/.tsv}"      # data.tsv

Delete Matches

Omit the replacement string to delete the match.

version="v1.2.3"
echo "${version/v/}"           # 1.2.3

spaces="  lots   of   spaces  "
echo "${spaces// /}"           # lotsofspaces

Case Conversion

Available in bash 4.0 and later.

name="john doe"

echo "${name^}"                # John doe    (capitalize first char)
echo "${name^^}"               # JOHN DOE    (all uppercase)

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

This is useful for normalizing input.

read -rp "Continue? [y/N] " answer
if [[ "${answer,,}" == "y" ]]; then
    echo "Proceeding..."
fi

Indirect Expansion

Reference a variable whose name is stored in another variable.

env="production"
production_db="prod-db.example.com"
staging_db="staging-db.example.com"

db_var="${env}_db"
echo "${!db_var}"              # prod-db.example.com

Practical Example: Log Rotation Script

Here is a script that uses several parameter expansion techniques.

#!/bin/bash
set -euo pipefail

log_dir="${1:?Usage: $0 <log_directory>}"
max_age="${2:-30}"
archive_dir="${log_dir}/archive"

mkdir -p "$archive_dir"

for log_file in "$log_dir"/*.log; do
    [[ -f "$log_file" ]] || continue

    # Get just the filename
    name="${log_file##*/}"

    # Get name without extension
    base="${name%.*}"

    # Create dated archive name
    archive="${archive_dir}/${base}-$(date +%Y%m%d).log.gz"

    echo "Archiving ${name} -> ${archive##*/}"
    gzip -c "$log_file" > "$archive"
    : > "$log_file"   # truncate original
done

# Clean old archives
find "$archive_dir" -name "*.gz" -mtime +"$max_age" -delete

echo "Rotation complete. Archives older than ${max_age} days removed."

Quick Reference

Syntax Purpose
${var:-default} Default if unset/empty
${var:=default} Assign default if unset/empty
${var:?error} Error if unset/empty
${var:+alt} Alternate if set
${#var} String length
${var:offset:len} Substring
${var#pattern} Remove shortest prefix
${var##pattern} Remove longest prefix
${var%pattern} Remove shortest suffix
${var%%pattern} Remove longest suffix
${var/old/new} Replace first match
${var//old/new} Replace all matches
${var^} Uppercase first char
${var^^} Uppercase all
${var,} Lowercase first char
${var,,} Lowercase all

Once these expansions become second nature, you will find yourself writing cleaner and faster scripts. Every parameter expansion that replaces a call to sed or basename is one fewer subprocess spawned, which adds up in loops processing thousands of files.


Other Recent Posts