Skip to content
DevOps devops shell 5 min read

String Manipulation

Almost every shell script ends up slicing and reshaping text — pulling a file extension off a name, trimming a prefix from a path, or filling in a default when a variable is empty. Bash (the Bourne Again Shell, the default command-line shell on Ubuntu) can do most of this on its own, without calling external tools like sed or awk. This is faster and avoids spawning extra processes. The feature that powers it is called parameter expansion (the ${...} syntax that lets you transform a variable’s value as you read it). This page covers the expansions you will reach for daily.

Why use parameter expansion instead of external tools

When you write ${var/old/new}, Bash itself does the work. No new program starts. Compare that to echo "$var" | sed 's/old/new/', which launches both echo (sometimes) and sed. For a single line that difference is invisible, but inside a loop running thousands of times it adds up to real seconds.

ApproachWhen to useWhen NOT to use
Parameter expansion ${...}Simple slicing, replacing, length, defaults — especially inside loopsComplex regular expressions or multi-line transforms
sed / awkRegex, stream editing whole files, field processingOne-off single-variable tweaks where ${...} already works
cutSplitting on a fixed delimiter into columnsWhen you need to keep the rest of the string

Parameter expansion only works on shell variables, not on a stream of text coming through a pipe. If your data is flowing through a pipe, you need sed, awk, or cut instead.

Getting the length of a string

Put a # in front of the variable name to get its length in characters.

name="Ubuntu"
echo "${#name}"

Output:

6

This is handy for validation — for example, checking a password or token meets a minimum length before you use it.

Substrings — slicing out part of a string

The syntax ${var:start:length} gives you a slice. start is the position to begin at, counting from 0 (the first character). length is how many characters to take. If you leave off length, you get everything from start to the end.

release="ubuntu-24.04-server"
echo "${release:0:6}"     # first 6 characters
echo "${release:7:5}"     # 5 characters starting at position 7
echo "${release:7}"       # everything from position 7 onward

Output:

ubuntu
24.04
24.04-server

You can also count from the end using a negative start position. Note the space before the minus sign (or wrap it in parentheses) — without it, Bash reads :- as the “default value” syntax covered below.

filename="report.tar.gz"
echo "${filename: -2}"    # last 2 characters

Output:

gz

Replacing text

To swap part of a string, use ${var/old/new} for the first match, or ${var//old/new} (double slash) for every match.

path="/var/log/var-cache.log"
echo "${path/var/data}"     # first occurrence only
echo "${path//var/data}"    # all occurrences
echo "${path//\//-}"        # replace every slash with a dash

Output:

/data/log/var-cache.log
/data/log/data-cache.log
-var-log-var-cache.log

To delete text, replace with nothing: ${var/old/}. You can also anchor a replacement to the start with # or the end with %:

url="https://devcraftly.com"
echo "${url/#https/http}"   # only if it appears at the START

Output:

http://devcraftly.com

Removing prefixes and suffixes

These pattern operators are the cleanest way to strip parts off the front or back of a string. They use shell glob patterns (where * means “any characters”), not regular expressions.

OperatorMeaning
${var#pattern}Remove the shortest match from the start
${var##pattern}Remove the longest match from the start
${var%pattern}Remove the shortest match from the end
${var%%pattern}Remove the longest match from the end
file="/etc/nginx/sites-available/default.conf"
echo "${file##*/}"     # strip everything up to the last slash = basename
echo "${file%/*}"      # strip the last slash and after = dirname
echo "${file%.conf}"   # strip the .conf suffix

Output:

default.conf
/etc/nginx/sites-available
/etc/nginx/sites-available/default

Default values for empty or unset variables

${var:-default} returns default if var is empty or unset, but does not change var. This is the safest way to handle optional configuration. Use it for environment variables that may not be set.

echo "Listening on port ${PORT:-8080}"
LOG_DIR="${LOG_DIR:-/var/log/myapp}"
echo "Logs go to $LOG_DIR"

Output:

Listening on port 8080
Logs go to /var/log/myapp

Three related forms are worth knowing:

FormBehaviour
${var:-default}Use default if empty/unset; leave var unchanged
${var:=default}Use default AND assign it to var
${var:?message}Print message to stderr and exit if empty/unset — great for required values
: "${DB_PASSWORD:?DB_PASSWORD is required}"

If DB_PASSWORD is not set, the script stops immediately with a clear error instead of failing later in a confusing way.

Worked example — parsing a filename

A very common task is splitting a filename into its base name and its extension. Combining the operators above does it without any external tools.

#!/usr/bin/env bash
set -euo pipefail

fullpath="/home/ubuntu/backups/db-2026-06-15.sql.gz"

filename="${fullpath##*/}"      # db-2026-06-15.sql.gz
extension="${filename##*.}"     # gz  (longest match from start -> last dot)
name="${filename%.*}"           # db-2026-06-15.sql (strip last extension)
stem="${filename%%.*}"          # db-2026-06-15 (strip ALL extensions)

echo "Directory: ${fullpath%/*}"
echo "Filename:  $filename"
echo "Name:      $name"
echo "Stem:      $stem"
echo "Extension: $extension"

Output:

Directory: /home/ubuntu/backups
Filename:  db-2026-06-15.sql.gz
Name:      db-2026-06-15.sql
Stem:      db-2026-06-15
Extension: gz

Notice the difference between name (drops only .gz) and stem (drops both .sql.gz). Choose based on whether your files have one extension or several.

Changing case

Modern Bash (version 4 and later, which ships on every supported Ubuntu release) can change case directly.

distro="Ubuntu"
echo "${distro^^}"    # uppercase all
echo "${distro,,}"    # lowercase all
echo "${distro^}"     # uppercase first letter only

Output:

UBUNTU
ubuntu
Ubuntu

Best Practices

  • Always wrap variable expansions in double quotes — "${var}" — so spaces and special characters do not break your script.
  • Prefer parameter expansion over sed/awk for simple per-variable edits; it is faster and has no external dependency.
  • Use ${var:-default} for optional values and ${var:?message} for required ones to fail early and clearly.
  • Remember #/## strip from the start and %/%% strip from the end — a single character does the shortest match, doubled does the longest.
  • These operators use glob patterns (*, ?), not regular expressions; do not expect regex syntax here.
  • Add set -euo pipefail at the top of scripts so an unset variable or failed command stops execution instead of silently continuing.
Last updated June 15, 2026
Was this helpful?