Bash has a rich set of built-in string operations via parameter expansion — the ${var...} syntax. Mastering these lets you manipulate strings without spawning sed, awk, or tr subprocesses, making scripts significantly faster.
1
Length, substring, and slicing
BASH
str="Hello, World!"
# ── Length ────────────────────────────────────────────────
echo ${#str} # 13
echo ${#str} # Works on any variable
arr=("a" "b" "c")
echo ${#arr[@]} # 3 (array element count)
# ── Substring ${var:offset:length} ───────────────────────
echo "${str:0:5}" # Hello (from index 0, 5 chars)
echo "${str:7}" # World! (from index 7 to end)
echo "${str:7:5}" # World (from index 7, 5 chars)
echo "${str: -6}" # orld! (last 6 chars — note space)
echo "${str: -6:5}" # orld (last 6, then 5 chars)
# ── Practical: extract filename and extension ─────────────
filepath="/var/log/app/error.log.2026"
filename=$(basename "${filepath}") # error.log.2026
ext="${filename##*.}" # 2026 (last extension)
base="${filename%.*}" # error.log (remove last ext)
noext="${filename%%.*}" # error (remove all extensions)
echo "filename : ${filename}"
echo "ext : ${ext}"
echo "base : ${base}"
echo "noext : ${noext}"
2
Prefix and suffix removal — # % ## %%
The # and % operators remove patterns from the beginning or end of a string. Single character = shortest match, doubled = longest match (greedy).
BASH
path="/home/vriddh/projects/myapp/main.sh"
# Remove from the LEFT (prefix removal)
echo "${path#/}" # home/vriddh/projects/myapp/main.sh (shortest)
echo "${path#*/}" # home/vriddh/projects/myapp/main.sh (up to first /)
echo "${path##*/}" # main.sh (greedy — up to LAST /)
# Remove from the RIGHT (suffix removal)
echo "${path%/*}" # /home/vriddh/projects/myapp (remove after last /)
echo "${path%%/*}" # (empty — removes from first / to end)
echo "${path%.sh}" # /home/vriddh/projects/myapp/main (remove .sh)
# Real-world patterns ─────────────────────────────────────
url="https://db.example.com:5432/mydb"
echo "${url#*://}" # db.example.com:5432/mydb (strip protocol)
echo "${url%%:*}" # https (protocol only)
log="[2026-05-01 10:30:45] ERROR: Connection refused"
echo "${log#*] }" # ERROR: Connection refused (strip timestamp)
echo "${log%%]*}" # [2026-05-01 10:30:45 (timestamp only)
3
Search and replace — ${var/pattern/replacement}
BASH
str="the quick brown fox jumps over the lazy dog"
# Replace first occurrence
echo "${str/the/a}" # a quick brown fox jumps over the lazy dog
# Replace ALL occurrences (double //)
echo "${str//the/a}" # a quick brown fox jumps over a lazy dog
# Replace only at start (#) or end (%)
echo "${str/#the/THE}" # THE quick brown fox jumps over the lazy dog
echo "${str/%dog/cat}" # the quick brown fox jumps over the lazy cat
# Delete pattern (empty replacement)
echo "${str// /}" # thequickbrownfoxjumpsoverthelazydog
# Practical — sanitise filename
name="My Report (Final) v2.pdf"
safe="${name// /_}" # Replace spaces with underscores
safe="${safe//[()]/}" # Remove parentheses
echo "${safe}" # My_Report_Final_v2.pdf
# Replace with glob patterns
csv="one,two,,three,"
echo "${csv//,/ }" # one two three (commas to spaces)
4
Case conversion — upper, lower, title
BASH
str="Hello World from Vriddh"
# Bash 4+ parameter expansion for case conversion
echo "${str^^}" # HELLO WORLD FROM VRIDDH (all uppercase)
echo "${str,,}" # hello world from vriddh (all lowercase)
echo "${str^}" # Hello World from Vriddh (capitalise first char)
echo "${str,}" # hELLO WORLD FROM VRIDDH (lowercase first char)
# With pattern — only affects matching chars
echo "${str^^[aeiou]}" # HEllO WOrld frOm VrIddh (uppercase vowels only)
# Older bash / POSIX compatible — use tr
echo "${str}" | tr '[:lower:]' '[:upper:]' # uppercase
echo "${str}" | tr '[:upper:]' '[:lower:]' # lowercase
# Title case with awk
echo "hello world from vriddh" | \
awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) substr($i,2)} 1'
# Hello World From Vriddh
# Practical — normalise environment name from input
env_input="PRODUCTION"
env_norm="${env_input,,}" # production
case "${env_norm}" in
prod|production) echo "Deploying to PRODUCTION" ;;
stage|staging) echo "Deploying to staging" ;;
dev|development) echo "Deploying to dev" ;;
*) echo "Unknown environment: ${env_input}"; exit 1 ;;
esac
5
Trim whitespace — leading and trailing
BASH
str=" hello world "
# Trim leading whitespace
ltrim() { local v="${1}"; echo "${v#"${v%%[! ]*}"}"; }
# Trim trailing whitespace
rtrim() { local v="${1}"; echo "${v%"${v##*[! ]}"}"; }
# Trim both sides
trim() { local v="${1}"; v="${v#"${v%%[! ]*}"}"; echo "${v%"${v##*[! ]}"}"; }
echo "'$(trim "${str}")'" # 'hello world'
# Simpler with sed (but spawns subprocess)
echo "${str}" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
# Trim with xargs (tiny but forks process)
echo " hello " | xargs # hello
# Read from file stripping whitespace
while IFS= read -r line; do
line=$(trim "${line}")
[[ -z "${line}" ]] && continue # Skip empty lines
[[ "${line}" == \#* ]] && continue # Skip comments
echo "Processing: ${line}"
done < config.txt
6
Contains, starts with, ends with — pattern matching
BASH
str="production-db-01.example.com"
# ── Contains ──────────────────────────────────────────────
[[ "${str}" == *"db"* ]] && echo "Contains 'db'"
# ── Starts with ───────────────────────────────────────────
[[ "${str}" == prod* ]] && echo "Starts with 'prod'"
[[ "${str}" == "prod"* ]] && echo "Starts with 'prod' (quoted)"
# ── Ends with ─────────────────────────────────────────────
[[ "${str}" == *".com" ]] && echo "Ends with '.com'"
# ── Regex matching with =~ ───────────────────────────────
[[ "${str}" =~ ^prod ]] && echo "Regex: starts with prod"
[[ "${str}" =~ [0-9]+ ]] && echo "Regex: contains number"
[[ "${str}" =~ \.com$ ]] && echo "Regex: ends with .com"
# Capture regex groups with BASH_REMATCH
log="[2026-05-01 10:30:45] ERROR: Connection refused"
if [[ "${log}" =~ \[([0-9-]+)\ ([0-9:]+)\]\ ([A-Z]+):\ (.*) ]]; then
echo "Date : ${BASH_REMATCH[1]}"
echo "Time : ${BASH_REMATCH[2]}"
echo "Level : ${BASH_REMATCH[3]}"
echo "Message: ${BASH_REMATCH[4]}"
fi
# Validate IP address format
ip="192.168.1.100"
ip_regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
[[ "${ip}" =~ $ip_regex ]] && echo "Valid IP" || echo "Invalid IP"
7
printf — formatted string output
BASH
# printf format: %[flags][width][.precision]type
printf "%-20s %5d %8.2f\n" "vriddh" 42 3.14159
printf "%-20s %5d %8.2f\n" "alice" 30 2.71828
printf "%-20s %5d %8.2f\n" "bob" 25 1.41421
# vriddh 42 3.14
# alice 30 2.72
# bob 25 1.41
# Padding strings
printf "%30s\n" "right-aligned" # right-aligned with spaces
printf "%-30s|\n" "left-aligned" # left-aligned
printf "%030d\n" 42 # 000000000000000000000000000042
# Capture printf output into variable
formatted=$(printf "%-10s: %s" "Status" "OK")
echo "${formatted}"
# Repeat a character
printf '%0.s─' {1..50}; echo # ──────────────────────────────────────────────────
Terminal output
vriddh@prod-01:~$str="production-db-01.example.com"; echo "${str##*.}"
com
vriddh@prod-01:~$echo "${str^^}"
PRODUCTION-DB-01.EXAMPLE.COM
vriddh@prod-01:~$echo "${str/db/mysql}"
production-mysql-01.example.com
vriddh@prod-01:~$[[ "$str" =~ ^prod ]] && echo "Is production"
Is production
vriddh@prod-01:~$printf "%-20s %5s\n" "Server" "Status"; printf "%-20s %5s\n" "prod-db-01" "OK"
Server Status
prod-db-01 OK
█
✔ String manipulation cheatsheet —
${#v} length · ${v:n:l} substring · ${v#p} strip prefix (shortest) · ${v##p} strip prefix (longest) · ${v%p} strip suffix (shortest) · ${v%%p} strip suffix (longest) · ${v/p/r} replace first · ${v//p/r} replace all · ${v^^} uppercase · ${v,,} lowercase · [[ v =~ regex ]] regex match.💡 Pure bash vs external tools — Parameter expansion runs entirely within bash with no subprocesses. Each call to
sed, awk, tr, or grep forks a new process. In a tight loop processing thousands of strings, this difference can be 10–100x in speed. Use parameter expansion whenever possible.