Shell Scripting Bash Strings Basics May 2026

Shell Scripting String Manipulation

Master all string operations in bash — length, substring, search, replace, trim, case conversion, split, padding, and regex matching. All using pure bash parameter expansion, no external tools needed.

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.

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}"

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)
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)
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
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
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"
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        # ──────────────────────────────────────────────────
bash — string manipulation demo
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.