Bash Parameter Expansion: Defaults, Substrings, and Replacement

Bash Parameter Expansion: Defaults, Substrings, and Replacement

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.

By admin

Leave a Reply

Your email address will not be published. Required fields are marked *