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.
| Approach | When to use | When NOT to use |
|---|---|---|
Parameter expansion ${...} | Simple slicing, replacing, length, defaults — especially inside loops | Complex regular expressions or multi-line transforms |
sed / awk | Regex, stream editing whole files, field processing | One-off single-variable tweaks where ${...} already works |
cut | Splitting on a fixed delimiter into columns | When 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, orcutinstead.
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.
| Operator | Meaning |
|---|---|
${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:
| Form | Behaviour |
|---|---|
${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/awkfor 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 pipefailat the top of scripts so an unset variable or failed command stops execution instead of silently continuing.