| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash-5.2.26-6.el10.x86_64 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user |
| Scope | Bash ${...} parameter expansion: name boundaries, default/assignment/required/alternate operators, length, substrings, prefix and suffix trimming, search and replace, case conversion, positional defaults, indirect expansion, and set -u patterns. Does not cover string concatenation, regex matching, full shell expansion order, or complete arithmetic expansion. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash parameter expansion is the ${...} syntax used to work with variables without external commands. For example, ${name:-guest} uses guest when name is unset or empty, ${file%.*} removes a file extension, and ${text//old/new} replaces text. The operators look similar, but small differences such as - versus :- change how empty variables are handled.
The truth table below shows exactly how all eight operators behave when a variable is unset, empty, or contains a value. The later examples cover substrings, trimming, replacement, case conversion, and other common uses.
Bash parameter expansion vs POSIX sh
This guide targets Bash. Basic forms such as ${var:-default}, ${var#pattern}, and ${var%pattern} also exist in POSIX shells, but features such as ${var//old/new} and ${var^^} are Bash extensions. If a script uses these forms, run it with Bash (bash script.sh) and use an appropriate #!/usr/bin/env bash or #!/bin/bash shebang rather than invoking it with sh. Otherwise you may see Bad substitution errors.
What Is Bash Parameter Expansion?
Parameter expansion is how Bash substitutes a value when it sees $name or ${name} while parsing a command line. Most of the power lives in the braced forms ${parameter:offset}, ${parameter:-word}, and similar operators documented in the GNU Bash manual.
Bash performs these operations directly in the shell, so simple defaults, trimming, and replacements often do not need commands such as sed or cut.
For how quoting changes which expansions run, see Bash single vs double quotes.
${var} and Variable Name Boundaries
Use braces when letters, digits, or underscores follow the variable name. Without braces, Bash reads the longest valid name:
host=web01
printf '[%s]\n' "$host_backup"
printf '[%s]\n' "${host}_backup"[]
[web01_backup]Without braces, Bash interprets $host_backup as a reference to a variable named host_backup, not $host followed by _backup. Because host_backup is unset here, the first command prints an empty line. ${host}_backup explicitly marks where the variable name ends. The Bash variables guide covers assignment and quoting; this article focuses on operators inside the braces.
Default Values and Unset vs Empty Variables
The colon in ${var:-word} is not decorative. It changes whether an empty value counts like an unset variable.
| Syntax | var unset |
var="" |
var="prod" |
|---|---|---|---|
${var-default} |
default |
(empty) | prod |
${var:-default} |
default |
default |
prod |
${var=default} |
assign default |
(empty) | prod |
${var:=default} |
assign default |
assign default |
prod |
${var+word} |
(empty) | word |
word |
${var:+word} |
(empty) | (empty) | word |
${var?word} |
prints error; non-interactive shell exits | (empty) | prod |
${var:?word} |
prints error; non-interactive shell exits | prints error; non-interactive shell exits | prod |
The + forms expand to word when the condition matches; they do not return the variable's stored text. The ? forms write word to stderr when the check fails.
${var-word} vs ${var:-word}
${var-word} substitutes word only when var is unset. An empty string is still "set":
unset ENVIRONMENT
printf '%s\n' "${ENVIRONMENT-staging}"
ENVIRONMENT=
printf '%s\n' "${ENVIRONMENT-staging}"stagingThe second line prints nothing because ENVIRONMENT is set to empty and the form without : does not treat empty as missing.
${ENVIRONMENT:-staging} is the usual configuration fallback:
unset ENVIRONMENT
printf '%s\n' "${ENVIRONMENT:-staging}"
ENVIRONMENT=
printf '%s\n' "${ENVIRONMENT:-staging}"staging
stagingBoth lines print staging because :- treats unset and empty the same way.
Assign a Default with = and :=
Use = or := when the script should store the default, not only print it. ${var=word} assigns only when var is unset; ${var:=word} also assigns when var is empty:
unset LOG_DIR
printf '%s\n' "${LOG_DIR=/var/log/app}"
declare -p LOG_DIR
LOG_DIR=
printf '%s\n' "${LOG_DIR:=/var/log/app}"
declare -p LOG_DIR/var/log/app
declare -- LOG_DIR="/var/log/app"
/var/log/app
declare -- LOG_DIR="/var/log/app"The : "${VAR:=default}" idiom on its own line is a common way to initialize several settings at the top of a script.
Require a Value with ? and :?
Fail fast when a required setting is missing:
bash -c 'unset API_KEY; : "${API_KEY:?API_KEY is required}"' 2>&1bash: line 1: API_KEY: API_KEY is required${var:?word} treats both unset and empty values as an error. In a non-interactive Bash shell, Bash writes the message to stderr and exits with a non-zero status. An interactive shell does not exit, but the command containing the failed expansion is not executed.
Alternate Values with + and :+
The + forms expand to word when a condition holds; they never return the variable's own value. ${var+word} treats an empty string as "set", while ${var:+word} requires a non-empty value:
unset flag
printf 'unset + : [%s]\n' "${flag+yes}"
flag=
printf 'empty + : [%s]\n' "${flag+yes}"
printf 'empty :+ : [%s]\n' "${flag:+yes}"
flag=1
printf 'value :+ : [%s]\n' "${flag:+yes}"unset + : []
empty + : [yes]
empty :+ : []
value :+ : [yes]Use ${var+word} when you only need to know whether the name exists, and ${var:+word} when empty should behave like missing.
Get String Length with ${#var}
Prefix # inside the braces counts characters in the expanded value:
message=hello
printf 'length=%s\n' "${#message}"length=5For an array name, ${#name[@]} counts elements and ${#name[0]} counts characters in the first element — array details live in the arrays guides linked from the course hub.
Extract Substrings
${var:offset} and ${var:offset:length} slice the expanded string. Offsets are zero-based.
Positive offsets
Zero-based offsets slice fixed-width fields such as an ISO date:
date=2026-09-10
printf 'year=%s month=%s day=%s\n' "${date:0:4}" "${date:5:2}" "${date:8:2}"year=2026 month=09 day=10Negative offsets and lengths
A negative offset counts from the end of the string. Bash needs a space (or another separator) before - so it is not parsed as part of the :- default operator:
word=hello
printf '%s\n' "${word: -2}"loWithout the space, ${word:-2} means "use default -2 when word is unset or empty", not "last two characters".
When length is negative, the slice ends that many characters before the end of the string:
word=hello
printf '%s\n' "${word:1:-1}"ellThat expands from index 1 through one character before the final character.
Remove Prefixes and Suffixes
# and ## remove a matching prefix; % and %% remove a matching suffix. The operators differ only in shortest versus longest match:
| Operator | Effect |
|---|---|
# |
shortest prefix |
## |
longest prefix |
% |
shortest suffix |
%% |
longest suffix |
# and ## — remove prefix
Shortest and longest prefix removal on a log path:
path=/var/log/nginx/access.log
printf '%s\n' "${path#*/}"
printf '%s\n' "${path##*/}"var/log/nginx/access.log
access.log#*/ drops through the first slash; ##*/ drops through the last slash.
A common pattern splits a path into directory and filename without dirname or basename:
path=/var/log/nginx/access.log
filename=${path##*/}
directory=${path%/*}
printf 'file=%s\ndir=%s\n' "$filename" "$directory"file=access.log
dir=/var/log/nginx% and %% — remove suffix
Compare explicit suffix removal with shortest and longest .* patterns:
file=archive.tar.gz
printf '%s\n' "${file%.tar.gz}"
printf '%s\n' "${file%%.*}"
printf '%s\n' "${file%.*}"archive
archive
archive.tar${file%%.*} removes from the first dot through the end, leaving archive, while ${file%.*} removes only the final extension, leaving archive.tar. The same ${1%%.*} / ${1#*.} pair splits integer and fractional digits before comma grouping in Bash format number with commas. For a known compound extension such as .tar.gz, explicitly matching the suffix is usually clearer.
Strip a compound extension explicitly when %%.* would remove too much:
artifact=report-2026.tar.gz
printf 'one=%s all=%s explicit=%s\n' "${artifact%.*}" "${artifact%%.*}" "${artifact%.tar.gz}"one=report-2026.tar all=report-2026 explicit=report-2026Search and Replace
${var/pattern/replacement} replaces the first match; ${var//pattern/replacement} replaces every match. Anchor to the start with # (${var/#prefix/}) or to the end with % (${var/%suffix/}):
name=prefix-host-suffix
printf '%s\n' "${name/#prefix-/}"
printf '%s\n' "${name/%-suffix/}"host-suffix
prefix-hostOmit the replacement to delete every match:
value='one-two-three'
printf '%s\n' "${value//-/}"onetwothreeReplace characters in a string
Chain two replacements when you need more than one character change:
title='My Report / Q3'
safe=${title// /_}
safe=${safe//\//-}
printf '%s\n' "$safe"My_Report_-_Q3Parameter expansion can apply multiple simple replacements without sed. This example only replaces spaces and slashes; it is not a complete filename-sanitization routine. Escape / in the pattern as \/ when you use / as the delimiter.
Bash Patterns vs Regular Expressions
Trim and replacement operators use Bash pattern matching (glob-style patterns), not regular expressions. * matches any string, ? matches one character, and [...] matches a character class.
That is why ${path##*/} removes through the last slash — * matches the directory portion — but it will not match "digits only" or other regex-style rules. When you need regex semantics, use [[ string =~ pattern ]] in a test, not ${var/pattern/replacement}. For how globs behave in filenames, see Bash globbing.
Change String Case
Bash 4+ provides case conversion on the expanded value without changing the stored variable:
mode=Staging
printf 'lower=%s upper=%s\n' "${mode,,}" "${mode^^}"
printf 'stored=%s\n' "$mode"lower=staging upper=STAGING
stored=Staging${var^} uppercases the first character and ${var,} lowercases it:
name='hello world'
printf '%s\n' "${name^}"Hello worldParameter Expansion with Script Arguments
Many parameter-expansion operators work with positional parameters. For example, ${1:-default} provides a fallback for the first argument. Assignment forms such as ${1:=default} cannot assign to positional parameters.
demo() {
printf 'arg1=%s\n' "${1:-default-arg}"
}
demo
demo providedarg1=default-arg
arg1=providedFor counting arguments and iterating $@, see Bash script arguments.
Indirect Expansion with ${!name}
${!name} expands to the value of the variable whose name is stored in name:
environment=production
varname=environment
printf '%s\n' "${!varname}"productionIndirect expansion is useful when a variable contains the name of
another variable. For larger scripts, a nameref (declare -n) or
Bash associative array is often easier to
read because the keys remain explicit.
Parameter Expansion with set -u
set -u (nounset) aborts when the shell expands an unset variable. Defaults and required-value operators are how you mark intentional optionality:
bash -c 'set -u; printf "%s\n" "${MISSING:-fallback}"'fallbackset -u does not prevent you from testing an unset variable with parameter-expansion operators such as ${var:-word} and ${var:?word}. The first provides a fallback; the second deliberately reports a custom error when the variable is missing. A bare $MISSING under set -u still fails.
Combine defaults at the top of a script with nounset for safer configuration loading:
CONFIG_DIR=/etc/myapp bash -c 'set -u
: "${CONFIG_DIR:?CONFIG_DIR is required}"
: "${LOG_LEVEL:=info}"
printf "dir=%s level=%s\n" "$CONFIG_DIR" "$LOG_LEVEL"'dir=/etc/myapp level=infoCONFIG_DIR must be provided; LOG_LEVEL falls back to info when unset or empty.
Quick Reference
Defaults and validation
| Syntax | Effect |
|---|---|
${var-word} |
Default if unset only |
${var:-word} |
Default if unset or empty |
${var=word} |
Assign default if unset only |
${var:=word} |
Assign default if unset or empty |
${var+word} |
word if set (even when empty) |
${var:+word} |
word if set and non-empty |
${var?word} |
Error if unset only |
${var:?word} |
Error if unset or empty |
String operations
| Syntax | Effect |
|---|---|
${var} |
Value of var |
${#var} |
Length |
${var:pos:len} |
Substring |
${var#pat} / ${var##pat} |
Remove shortest / longest prefix |
${var%pat} / ${var%%pat} |
Remove shortest / longest suffix |
${var/pat/rep} / ${var//pat/rep} |
Replace first / all |
${var,,} / ${var^^} |
Lowercase / uppercase |
${var,} / ${var^} |
Lowercase / uppercase first character |
${!name} |
Indirect expansion |
Common Parameter Expansion Mistakes
| Trap | What goes wrong | Correct form |
|---|---|---|
${var- default} |
Leading space becomes part of the default value | ${var-default} or ${var:-default} |
${var:-2} vs ${var: -2} |
First is a default value -2 |
Space before - for negative offset |
$host_backup |
Bash looks for variable host_backup |
${host}_backup |
Regex in ${var//…/} |
Glob rules, not regex | Use [[ =~ ]] for regex tests |
${var=word} on empty vars |
Does not assign when empty | Use ${var:=word} when empty should reset |
Bare $VAR under set -u |
Nounset error | ${VAR:-} or ${VAR:?msg} |
Summary
Bash parameter expansion is the ${...} syntax for defaults, validation, slicing, trimming, replacing, and case changes on shell parameters. The truth table above is the fastest way to pick among -, :-, =, :=, +, :+, ?, and :? when empty strings matter. Use ${var:-default} for read-time fallbacks, ${var:=default} when the script should store the default, and ${var:?message} when a missing or empty value should stop the script.
References
- GNU Bash manual — Shell Parameter Expansion
- GNU Bash manual — Shell Parameters
- GNU Bash manual — Pattern Matching
Frequently Asked Questions
What is the difference between ${var:-default} and ${var-default}?
${var:-default} substitutes default when var is unset or empty. ${var-default} substitutes only when var is unset; an empty string stays empty.
How do I require a variable in Bash?
Use ${var:?message}. Bash writes message to stderr when var is unset or empty; in a non-interactive shell, execution stops. This also works with set -u, allowing you to provide a clearer error message than the default unbound-variable error.
Why does a negative substring need a space before the minus sign?
Bash parses ${var:-word} as the default-value operator. ${var: -2} uses a space so -2 is a negative offset, not part of :-.
Are ${var//pattern/repl} patterns regular expressions?
No. They are glob-style patterns. Use [[ string =~ regex ]] when you need regular expression matching.
How do I remove a file extension without sed?
Use ${file%.*} to remove the final extension. Use ${file%%.*} when you intentionally want to remove everything from the first dot onward. For dotfiles such as .bashrc, check for a filename portion before the dot if you do not want the leading dot treated as an extension separator.

