Bash can do most everyday string work with parameter expansion, without calling sed, awk, cut or basename. That’s faster in loops and avoids quoting trouble. Here’s the toolbox, with examples from real server scripts.

Length

s="example.com"
echo "${#s}"        # 11

Substrings

s="2026-10-14T09:12:00"
echo "${s:0:10}"    # 2026-10-14  (start 0, length 10)
echo "${s:11}"      # 09:12:00    (from index 11 to the end)
echo "${s: -8}"     # 09:12:00    (last 8 characters; note the space)
echo "${s:0:-3}"    # drop the last 3 characters (Bash 4.2+)

Remove a prefix or suffix

Syntax Removes
${s#pattern} shortest match from the start
${s##pattern} longest match from the start
${s%pattern} shortest match from the end
${s%%pattern} longest match from the end

The patterns are globs (*, ?, […]), not regular expressions.

path="/var/www/site/backup.tar.gz"

echo "${path##*/}"      # backup.tar.gz         (like basename)
echo "${path%/*}"       # /var/www/site         (like dirname)
echo "${path%.gz}"      # /var/www/site/backup.tar
echo "${path%%.*}"      # /var/www/site/backup  (strip all extensions)
f="${path##*/}"; echo "${f#*.}"    # tar.gz    (all extensions)

url="https://example.com/login?next=/admin"
host="${url#*://}"; host="${host%%/*}"
echo "$host"            # example.com

A memory aid: # is on the left of $ on a US keyboard, so it trims the left. % is on the right, so it trims the right.

Search and replace

s="hello world, hello bash"
echo "${s/hello/hi}"       # hi world, hello bash   (first match)
echo "${s//hello/hi}"      # hi world, hi bash      (all matches)
echo "${s/#hello/hi}"      # replace only at the start
echo "${s/%bash/zsh}"      # replace only at the end
echo "${s//o/}"            # delete all "o"
echo "${s// /_}"           # spaces to underscores

Case conversion (Bash 4+)

name="Web Server"
echo "${name^^}"      # WEB SERVER
echo "${name,,}"      # web server
echo "${name^}"       # capitalise the first letter

That’s useful for normalising user input: [[ "${answer,,}" == y* ]].

Contains, starts with, ends with

if [[ "$s" == *"world"* ]]; then echo "contains"; fi
if [[ "$s" == hello* ]];   then echo "starts with hello"; fi
if [[ "$file" == *.sql.gz ]]; then echo "compressed dump"; fi

Regular expressions with captures:

if [[ "$version" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
  major="${BASH_REMATCH[1]}"; minor="${BASH_REMATCH[2]}"
fi

Put complex regexes in a variable, as in re='^…$'; [[ $s =~ $re ]]. That avoids quoting issues.

Split a string

On a delimiter, into variables:

IFS=: read -r user pass uid gid _ <<< "deploy:x:1001:1001:Deploy:/home/deploy:/bin/bash"

Into an array:

IFS=, read -ra hosts <<< "web1,web2,db1"
echo "${hosts[1]}"       # web2

Key/value pairs:

line="DB_HOST=10.0.0.5"
key="${line%%=*}"        # DB_HOST
value="${line#*=}"       # 10.0.0.5 (keeps any further '=' in the value)

Trim whitespace

s="   padded value   "
s="${s#"${s%%[![:space:]]*}"}"   # trim leading
s="${s%"${s##*[![:space:]]}"}"   # trim trailing
echo "[$s]"                      # [padded value]

It’s cryptic, but pure Bash. A more readable alternative uses xargs. It trims and collapses whitespace, though it mangles quotes:

trimmed=$(echo "$s" | xargs)

Join an array

hosts=(web1 web2 db1)
joined=$(IFS=,; echo "${hosts[*]}")
echo "$joined"           # web1,web2,db1

Repeat, pad and format

printf -v line '%*s' 40 ''; echo "${line// /-}"    # 40 dashes
printf '%-15s|%8s\n' "Disk" "Used"                 # aligned columns
printf -v padded '%05d' 42; echo "$padded"         # 00042

printf -v var stores the result in a variable without a subshell.

When to reach for other tools

Parameter expansion works on one string at a time. For processing whole files or streams, sed, awk and grep are faster and clearer:

  • multi-line edits, or editing in place → sed
  • fields and columns in many lines → awk
  • character-level translation → tr

Quick reference

Task Syntax
Length ${#s}
Substring ${s:start:len}
Basename ${p##*/}
Dirname ${p%/*}
Strip extension ${f%.*}
Replace first / all ${s/a/b} / ${s//a/b}
Upper / lower ${s^^} / ${s,,}
Contains [[ $s == *sub* ]]
Split IFS=, read -ra arr <<< "$s"
Key and value ${l%%=*} / ${l#*=}

Related: Bash variables and Bash arrays.

  • Bash
  • Scripting
  • Linux