Bash Parameter Expansion: ${var:-}, Substrings, Trim & Replace

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
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

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:

bash
host=web01
printf '[%s]\n' "$host_backup"
printf '[%s]\n' "${host}_backup"
output
[]
[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":

bash
unset ENVIRONMENT
printf '%s\n' "${ENVIRONMENT-staging}"
ENVIRONMENT=
printf '%s\n' "${ENVIRONMENT-staging}"
output
staging

The 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:

bash
unset ENVIRONMENT
printf '%s\n' "${ENVIRONMENT:-staging}"
ENVIRONMENT=
printf '%s\n' "${ENVIRONMENT:-staging}"
output
staging
staging

Both 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:

bash
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
output
/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
bash -c 'unset API_KEY; : "${API_KEY:?API_KEY is required}"' 2>&1
output
bash: 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:

bash
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}"
output
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:

bash
message=hello
printf 'length=%s\n' "${#message}"
output
length=5

For 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:

bash
date=2026-09-10
printf 'year=%s month=%s day=%s\n' "${date:0:4}" "${date:5:2}" "${date:8:2}"
output
year=2026 month=09 day=10

Negative 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:

bash
word=hello
printf '%s\n' "${word: -2}"
output
lo

Without 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:

bash
word=hello
printf '%s\n' "${word:1:-1}"
output
ell

That 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:

bash
path=/var/log/nginx/access.log
printf '%s\n' "${path#*/}"
printf '%s\n' "${path##*/}"
output
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:

bash
path=/var/log/nginx/access.log
filename=${path##*/}
directory=${path%/*}
printf 'file=%s\ndir=%s\n' "$filename" "$directory"
output
file=access.log
dir=/var/log/nginx

% and %% — remove suffix

Compare explicit suffix removal with shortest and longest .* patterns:

bash
file=archive.tar.gz
printf '%s\n' "${file%.tar.gz}"
printf '%s\n' "${file%%.*}"
printf '%s\n' "${file%.*}"
output
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:

bash
artifact=report-2026.tar.gz
printf 'one=%s all=%s explicit=%s\n' "${artifact%.*}" "${artifact%%.*}" "${artifact%.tar.gz}"
output
one=report-2026.tar all=report-2026 explicit=report-2026

Search 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/}):

bash
name=prefix-host-suffix
printf '%s\n' "${name/#prefix-/}"
printf '%s\n' "${name/%-suffix/}"
output
host-suffix
prefix-host

Omit the replacement to delete every match:

bash
value='one-two-three'
printf '%s\n' "${value//-/}"
output
onetwothree

Replace characters in a string

Chain two replacements when you need more than one character change:

bash
title='My Report / Q3'
safe=${title// /_}
safe=${safe//\//-}
printf '%s\n' "$safe"
output
My_Report_-_Q3

Parameter 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:

bash
mode=Staging
printf 'lower=%s upper=%s\n' "${mode,,}" "${mode^^}"
printf 'stored=%s\n' "$mode"
output
lower=staging upper=STAGING
stored=Staging

${var^} uppercases the first character and ${var,} lowercases it:

bash
name='hello world'
printf '%s\n' "${name^}"
output
Hello world

Parameter 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.

bash
demo() {
  printf 'arg1=%s\n' "${1:-default-arg}"
}
demo
demo provided
output
arg1=default-arg
arg1=provided

For 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:

bash
environment=production
varname=environment
printf '%s\n' "${!varname}"
output
production

Indirect 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
bash -c 'set -u; printf "%s\n" "${MISSING:-fallback}"'
output
fallback

set -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:

bash
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"'
output
dir=/etc/myapp level=info

CONFIG_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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)