
Most Bash scripts call sed, awk, or cut for jobs the shell can do by itself. Stripping an extension from a filename, providing a default when a variable is empty, or replacing part of a string are all built into the ${...} syntax, run in the current process, and cost no fork to an external program. The syntax is dense, which is why many people never move past ${var}, but there are only a handful of patterns to learn.
This guide explains Bash parameter expansion: default values, length, substrings, prefix and suffix trimming, search and replace, and case conversion, with practical examples of each.
The Basic Form
${var} expands to the value of var, exactly like $var. The braces become mandatory when the variable name would otherwise run into the surrounding text:
Terminal
file="report"
echo "${file}_final.txt"
output
report_final.txt
Without braces, Bash would look for a variable named file_final. Everything else in this guide builds on this form by adding an operator inside the braces.
Default Values
The :- operator substitutes a fallback when a variable is unset or empty:
Terminal
echo "Deploying to ${ENVIRONMENT:-staging}"
output
Deploying to staging
The variable itself is not modified; the default only applies to this one expansion. To also assign the fallback to the variable, use := instead:
Terminal
: "${LOG_DIR:=/var/log/myapp}"
echo "$LOG_DIR"
output
/var/log/myapp
The leading : is the null command; it evaluates its arguments and does nothing else, which makes it the idiomatic way to trigger the assignment. This pattern is common at the top of scripts to give every configuration variable a sane default.
Two related operators round out the family. :? aborts the script with a message when the variable is missing, which turns a silent misconfiguration into a loud one:
Terminal
: "${API_KEY:?API_KEY must be set}"
output
bash: API_KEY: API_KEY must be set
And :+ is the inverse of :-: it expands to a value only when the variable is set and non-empty, which is useful for building optional command arguments:
Terminal
curl ${PROXY:+--proxy "$PROXY"}
When PROXY is empty, the expansion vanishes entirely and curl runs without the option.
Each operator also exists without the colon (-, =, ?, +). The colon versions treat an empty string the same as an unset variable; the colon-free versions act only when the variable is truly unset. In practice, the colon versions are what you want nearly all the time.
Defaults work on positional parameters too, which makes optional script arguments trivial:
Terminal
target="${1:-.}"
This uses the first argument if given and the current directory otherwise; see our guide on Bash script arguments
for the wider context.
String Length
${#var} expands to the length of the value in characters:
Terminal
password="s3cretPass"
echo "${#password}"
output
10
A common use is input validation, such as rejecting a password shorter than a minimum inside an if statement
. For arrays, the same syntax with [@] counts elements instead: ${#files[@]}.
Substrings
The ${var:offset:length} form extracts a substring. Offsets count from zero, and the length is optional:
Terminal
date="2026-01-01"
echo "${date:0:4}"
echo "${date:5}"
output
2026
01-01
The first expansion takes four characters starting at position zero; the second takes everything from position five onward.
Negative offsets count from the end of the string, but they require a space or parentheses so Bash does not confuse the syntax with the :- default operator:
Terminal
file="backup-2026.tar.gz"
echo "${file: -6}"
output
tar.gz
A negative length means “stop that many characters before the end” rather than a count, so ${var:1:-1} trims one character from each side. Negative lengths require Bash 4.2 or later.
Trimming Prefixes and Suffixes
Four operators remove a pattern from the front or the back of the value. # removes the shortest match from the front, ## the longest match from the front, % the shortest match from the back, and %% the longest match from the back. The patterns are globs, the same wildcards used for filenames.
The classic use is pulling filenames apart:
Terminal
path="/var/www/site/index.backup.html"
echo "${path##*/}"
echo "${path%/*}"
echo "${path%.*}"
echo "${path##*.}"
output
index.backup.html
/var/www/site
/var/www/site/index.backup
html
The first two replicate the basename and dirname commands: ##*/ deletes the longest match of “anything followed by a slash” from the front, and %/* deletes the shortest “slash followed by anything” from the back. The last two split at the final dot, so %.* strips one extension while ##*. keeps only the extension.
The shortest/longest distinction matters exactly when the pattern can match at more than one place. With the file above, ${path%.*} removes only .html, while ${path%%.*} would remove .backup.html as well.
Renaming files in a loop shows the pattern in action. For bulk changes, preview the commands first:
Terminal
for f in *.jpeg; do
printf 'mv -- %q %q\n' "$f" "${f%.jpeg}.jpg"
done
After checking the generated commands, run the rename:
Terminal
for f in *.jpeg; do
mv -- "$f" "${f%.jpeg}.jpg"
done
Each name has its .jpeg suffix trimmed and .jpg appended, with no sed in sight. The -- tells mv that option parsing is done, which protects filenames that begin with a dash.
Search and Replace
The ${var/pattern/replacement} form replaces the first match of a glob pattern, and doubling the first slash replaces all matches:
Terminal
csv="one,two,three"
echo "${csv/,/;}"
echo "${csv//,/;}"
output
one;two,three
one;two;three
Anchors are available: ${var/#pattern/replacement} replaces only a match at the start of the string, and ${var/%pattern/replacement} only at the end. Omitting the replacement deletes the match:
Terminal
echo "${csv//,}"
output
onetwothree
Keep in mind the pattern is a glob, not a regular expression: * and ? work, but . is a literal dot and there are no + or ^ operators. When you genuinely need regular expressions, that is the job of sed or the =~ operator; for everything simpler, the expansion is faster and has no quoting headaches. Our Bash string manipulation
guide compares these approaches side by side.
Changing Case
Bash 4 added case conversion operators: ^^ uppercases the value, ,, lowercases it, and the single forms ^ and , convert only the first character:
Terminal
name="alice"
echo "${name^}"
echo "${name^^}"
header="CONTENT-TYPE"
echo "${header,,}"
output
Alice
ALICE
content-type
Lowercasing is the standard trick for case-insensitive comparisons: normalize both sides with ,, and compare the results. Most current Linux distributions ship Bash 5, and Bash 4 is enough for these operators. Availability is mainly a concern in scripts that must also run on macOS’s ancient default Bash 3.2, where they are missing.
Indirect Expansion
${!var} expands to the value of the variable whose name is stored in var:
Terminal
staging_host="staging.example.com"
production_host="prod.example.com"
env="staging"
ref="${env}_host"
echo "${!ref}"
output
staging.example.com
Indirection occasionally saves the day in configuration-driven scripts, but if you find yourself using it often, an associative array
is usually the cleaner tool.
Combining with Positional Parameters
All the operators work on $1, $2, and friends, and the substring form even works on $@:
Terminal
command="${1:?usage: $0 command [args...]}"
shift_args=("${@:2}")
The first line insists on at least one argument and prints a usage message otherwise; the second collects every argument after the first into an array. These two lines are the skeleton of most subcommand-style scripts.
Quick Reference
For a printable quick reference, see the Bash cheatsheet
.
| Expansion | Result |
|---|---|
${var:-default} |
Value, or default if unset or empty |
${var:=default} |
Same, and assigns the default to var |
${var:?message} |
Value, or exit with message if unset or empty |
${var:+word} |
word only if var is set and non-empty |
${#var} |
Length of the value |
${var:2:5} |
Five characters starting at offset 2 |
${var: -3} |
Last three characters |
${var#pattern} |
Remove shortest matching prefix |
${var##pattern} |
Remove longest matching prefix |
${var%pattern} |
Remove shortest matching suffix |
${var%%pattern} |
Remove longest matching suffix |
${var/old/new} |
Replace first match |
${var//old/new} |
Replace all matches |
${var/#old/new} |
Replace match at start only |
${var/%old/new} |
Replace match at end only |
${var^^} / ${var,,} |
Uppercase / lowercase the value |
${var^} / ${var,} |
Uppercase / lowercase the first character |
${!var} |
Value of the variable named by var |
Conclusion
Parameter expansion replaces a surprising share of the sed | awk | cut pipelines found in everyday scripts, with defaults via :- and :=, filename surgery via # and %, and replacement via // covering the bulk of real cases. The patterns pair naturally with the string techniques in our Bash string manipulation
guide, and with set -u from the Bash set command
, where a well-placed ${var:-} marks a variable as intentionally optional.
