Linux Basic — String Substitution
String Substitution
Bash’s substitution operators ${var/old/new} and ${var//old/new} replace text inside a variable without calling sed. Same speed gain as pattern removal — no fork, instant result.
The Four Forms
| Form | Replaces |
|---|---|
${var/PATTERN/REPL} |
first match |
${var//PATTERN/REPL} |
all matches |
${var/#PATTERN/REPL} |
match only if at start |
${var/%PATTERN/REPL} |
match only if at end |
The pattern is a glob, not a regex. REPL is a literal string.
Single vs Global
s="banana"
echo "${s/a/A}" # bAnana (first a only)
echo "${s//a/A}" # bAnAnA (every a)
The single-slash form stops at the first match — handy for “fix the first occurrence” semantics. Double-slash is the everyday workhorse.
Anchored at Start or End
s="hello world hello"
echo "${s/#hello/HI}" # HI world hello (must be at the start)
echo "${s/%hello/BYE}" # hello world BYE (must be at the end)
Use # for prefix-only substitution, % for suffix-only. Both are single-shot — they never replace internal matches.
Delete by Leaving REPL Empty
s="abc-123-def"
echo "${s//-}" # abc123def (delete every '-')
echo "${s/-}" # abc123-def (delete first '-' only)
echo "${s/#abc/}" # -123-def (delete prefix)
echo "${s/%def/}" # abc-123- (delete suffix)
Three useful idioms:
# Strip whitespace around a value (cheap trim)
s=" hello "
trimmed=${s#"${s%%[![:space:]]*}"} # strip leading
trimmed=${trimmed%"${trimmed##*[![:space:]]}"} # strip trailing
echo "[$trimmed]"
# [hello]
Glob Patterns Work Here Too
s="file_2026-06-17_v1.tar.gz"
echo "${s/_v?/}" # file_2026-06-17.tar.gz
echo "${s/_[0-9]*_v?/}" # file.tar.gz
With shopt -s extglob:
shopt -s extglob
s="aaa-bbb-ccc"
echo "${s//@(aaa|ccc)/X}" # X-bbb-X
Substitution in Arrays
Apply to every element with [@]:
files=("a.log" "b.log" "c.txt")
new=( "${files[@]/.log/.gz}" )
printf '%s\n' "${new[@]}"
# a.gz
# b.gz
# c.txt
For all-occurrence within each element:
arr=("a-a" "b-b" "c-c")
res=( "${arr[@]//-/_}" )
printf '%s\n' "${res[@]}"
# a_a
# b_b
# c_c
Substitution + Defaults — Conditional Pipelines
Substitution composes nicely with the default operators:
ext=${file##*.}
ext=${ext,,} # to lowercase
mime=${MIMES[$ext]:-application/octet-stream}
When to Use sed Instead
Reach for sed (or awk) when you need:
- Real regex (capture groups,
\d, alternation, lookarounds) - Multi-line edits
- File-streaming (when the data isn’t in a variable)
# Regex with capture groups — sed wins
echo "2026-06-17" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})$/\3\/\2\/\1/'
# 17/06/2026
For everything else, native substitution is shorter and faster. sed in scripts is covered in detail in sed & awk in Scripts.
Performance — The Same Story
time (for i in {1..10000}; do echo "hello world" | sed 's/world/Bash/' > /dev/null; done)
# real 0m6.8s
time (for i in {1..10000}; do x="hello world"; x="${x/world/Bash}"; done)
# real 0m0.04s
In a hot loop, native substitution is ~150× faster than spawning sed.
Escaping the Pattern
Patterns are globs, not regex — but you still need to escape the delimiter /:
url="http://example.com/path"
echo "${url//\//_}" # http:__example.com_path (use \/)
Inside the pattern, glob metacharacters (*, ?, [) are special. Use [*], [?], [[] to match them literally:
s="a*b?c"
echo "${s//[*]/X}" # aXb?c
echo "${s//[?]/Y}" # a*bYc
A Real-World Example — Template Substitution
template="Hello {NAME}, you have {COUNT} new messages."
name="Alice"
count="3"
msg=${template//\{NAME\}/$name}
msg=${msg//\{COUNT\}/$count}
echo "$msg"
# Hello Alice, you have 3 new messages.
Tiny templating engine in three lines. No envsubst, no sed.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
${s/a/A/g} (sed-style flag) |
${s//a/A} |
double // is the “global” form |
${s/a/A/i} (case-insensitive) |
not supported — use ${s,,} first, or sed |
Bash sub has no i flag |
${s//(foo)/bar} regex group |
use sed -E 's/(foo)/bar/' |
only globs allowed |
${arr/old/new} (no @) |
${arr[@]/old/new} |
without [@], only first element is touched |
${url/\//} to drop one slash, but you want all |
${url//\//} |
single-slash form replaces first only |
Next → Case Conversion & Splitting