Skip to main content
Bash intermediate Lesson 8 of 22

Bash String Manipulation

Master parameter expansion, substrings, find-and-replace, trimming, case conversion, and heredocs in Bash.

Basic Parameter Expansion

Parameter expansion transforms a variable’s value at the point it is used, without modifying the variable itself. It is built into Bash and requires no external tools — no awk, no sed, no subshell. For simple string operations this makes your scripts both faster and easier to read.

name="Alice Smith"

echo "${#name}"        # 11  — string length (# counts characters)
echo "${name:0:5}"     # Alice — substring starting at index 0, length 5
echo "${name:6}"       # Smith — from index 6 to the end
echo "${name: -5}"     # Smith — last 5 characters (note the space before -)

Default Values

Default values solve a very common problem: a variable may or may not be set, and you want your script to work correctly in both cases. This is how scripts handle optional configuration and missing arguments gracefully.

# Use default if variable is unset or empty
greeting="${MESSAGE:-Hello}"
echo "$greeting"   # Hello (if MESSAGE is not set)

# Use default only if unset (not if empty — subtle but important distinction)
value="${SETTING-default}"

# Assign the default and set the variable if it is unset or empty
: "${CONFIG_DIR:=/etc/myapp}"

# Error out immediately if variable is unset or empty (fail fast with a clear message)
: "${DATABASE_URL:?DATABASE_URL is required}"

Prefix and Suffix Stripping

The # and % operators strip patterns from the front and back of a variable. These are extremely useful for extracting filenames, extensions, and path components without calling basename or dirname.

path="/home/alice/projects/app/main.sh"

# Strip shortest match from the front (#)
echo "${path#/}"           # home/alice/projects/app/main.sh
echo "${path#*/}"          # alice/projects/app/main.sh

# Strip longest match from the front (##)
echo "${path##*/}"         # main.sh  (equivalent to basename)

# Strip shortest match from the end (%)
echo "${path%/*}"          # /home/alice/projects/app  (equivalent to dirname)
echo "${path%.*}"          # /home/alice/projects/app/main

# Strip longest match from the end (%%)
echo "${path%%/*}"         # (empty — everything after the first / is removed)

# Practical: extract file extension and base name
filename="report.2025.csv"
echo "${filename##*.}"     # csv  (extension)
echo "${filename%.*}"      # report.2025  (name without last extension)

Find and Replace

The ${var/old/new} syntax replaces patterns within a variable. Use a single / to replace only the first match, or // to replace all occurrences. No external command needed.

text="The quick brown fox jumps over the lazy dog"

# Replace first occurrence only
echo "${text/fox/cat}"    # The quick brown cat jumps over the lazy dog

# Replace all occurrences (//)
sentence="foo bar foo baz foo"
echo "${sentence//foo/replaced}"   # replaced bar replaced baz replaced

# Replace only at the start (/#)
echo "${sentence/#foo/START}"      # START bar foo baz foo

# Replace only at the end (/%}
echo "${sentence/%foo/END}"        # foo bar foo baz END

Case Conversion (Bash 4+)

Case conversion operators were added in Bash 4.0. They let you normalize input — converting user-provided strings to a consistent case before comparing or storing them.

name="alice smith"

echo "${name^}"    # Alice smith   — capitalize first character only
echo "${name^^}"   # ALICE SMITH   — uppercase all characters

name="ALICE SMITH"
echo "${name,}"    # aLICE SMITH   — lowercase first character only
echo "${name,,}"   # alice smith   — lowercase all characters

A practical use — accepting both “Y” and “y” as confirmation:

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

Substring Extraction

Substring extraction uses ${var:start:length}. Negative start indices count from the end of the string (note the required space before the minus sign to avoid ambiguity with the :- default value operator).

str="Hello, World!"

echo "${str:7:5}"     # World  (start at index 7, take 5 characters)
echo "${str:7}"       # World! (start at index 7 to end)
echo "${str: -6}"     # orld!  (last 6 characters — note the space)
echo "${str: -6:5}"   # orld   (last 6, then take only 5)

Checking String Content

Pattern matching inside [[ ]] lets you test string content without calling grep. Use == with glob patterns for simple prefix/suffix/contains checks, and =~ for full regular expressions.

email="user@example.com"

# Contains substring — glob with * on both sides
if [[ "$email" == *"@"* ]]; then
  echo "Has @ sign"
fi

# Starts with — glob with * only at the end
if [[ "$email" == user* ]]; then
  echo "Starts with 'user'"
fi

# Full format validation with regex
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
  echo "Valid email format"
fi

Trimming Whitespace

Bash has no built-in trim function, but parameter expansion can do it. The pattern looks cryptic at first but works by stripping the leading whitespace-only prefix and the trailing whitespace-only suffix.

# Trim leading whitespace
ltrim() {
  local s="$1"
  s="${s#"${s%%[! ]*}"}"   # strip the longest leading run of spaces
  echo "$s"
}

# Trim trailing whitespace
rtrim() {
  local s="$1"
  s="${s%"${s##*[! ]}"}"   # strip the longest trailing run of spaces
  echo "$s"
}

# Trim both ends
trim() {
  local s="$1"
  s="${s#"${s%%[! ]*}"}"
  s="${s%"${s##*[! ]}"}"
  echo "$s"
}

result=$(trim "   hello world   ")
echo "'$result'"   # 'hello world'

Or use sed when readability matters more than avoiding a subshell:

trim_sed() {
  sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
}
echo "  hello  " | trim_sed   # hello

String Splitting

Splitting a string on a delimiter is done by setting IFS temporarily for a read command. This is the idiomatic Bash way to parse delimited data like CSV fields or colon-separated paths.

# Split a CSV-like string into an array
csv="alice,bob,carol,dave"
IFS=',' read -ra names <<< "$csv"

for name in "${names[@]}"; do
  echo "Name: $name"
done
# Name: alice
# Name: bob
# Name: carol
# Name: dave

# Split $PATH into its component directories
IFS=':' read -ra dirs <<< "$PATH"
echo "First PATH entry: ${dirs[0]}"

Heredoc

Heredocs embed multi-line strings directly in a script. They are cleaner than escaping every newline and are the standard way to write multi-line SQL queries, config file templates, email bodies, and usage messages.

# Basic heredoc — variables are expanded
cat <<EOF
This is line one.
This is line two.
Variables expand: $USER
EOF

# Indented heredoc — <<- strips leading tabs (not spaces)
generate_config() {
  cat <<-EOF
	[database]
	host = localhost
	port = 5432
	name = myapp
	EOF
}

# Quoted heredoc (single-quoted delimiter) — no variable expansion
cat <<'EOF'
No expansion: $USER $(date) `command`
All literal text — useful for writing scripts that contain bash syntax.
EOF

# Heredoc written directly to a file
cat > /etc/myapp/config.ini <<EOF
[server]
host = ${APP_HOST:-0.0.0.0}
port = ${APP_PORT:-8080}
EOF

Herestring

A herestring (<<<) feeds a single string to a command’s stdin. It avoids the overhead of echo "..." | command and does not create a subshell the way a pipe does.

# Instead of: echo "$var" | command
# Use: command <<< "$var"

read -r first_word <<< "hello world"
echo "$first_word"   # hello

# Parse a colon-delimited line without a pipe (no subshell)
IFS=: read -r user _ uid gid _ home shell <<< "root:x:0:0:root:/root:/bin/bash"
echo "User: $user, UID: $uid, Home: $home"

Practical: Slugify a String

This function combines several string operations to convert an arbitrary string into a URL-safe slug — a common requirement when generating filenames, IDs, or URLs from human-readable input.

slugify() {
  local input="$1"
  local slug="${input,,}"              # 1. convert to lowercase
  slug="${slug//[^a-z0-9 -]/}"        # 2. remove anything that is not a letter, digit, space, or dash
  slug="${slug// /-}"                  # 3. replace spaces with dashes
  echo "$slug"
}

slugify "Hello, World! This is a Test"
# hello-world-this-is-a-test

What’s Next

The next tutorial takes a deeper look at arrays — indexed and associative array operations, slicing, and reading files into arrays.

Frequently Asked Questions

Do I need external tools like sed to manipulate strings in Bash?
Not for most operations. Bash's built-in parameter expansion handles substrings, replacement, trimming, and case conversion without forking an external process, which is faster for simple operations.
What is parameter expansion?
Parameter expansion is the ${variable} syntax with special modifiers that transform the value — like ${var#prefix} to strip a prefix, or ${var/old/new} to replace text.
What is a heredoc used for?
Heredocs let you write multi-line strings inline in a script without escaping newlines. They're commonly used for configuration templates, SQL queries, or multi-line messages.