Parameter expansion derives a value from a shell parameter before a command receives its arguments. It can provide a default, reject missing configuration, or remove part of a string without launching another process. This is expansion during execution, not an instruction to evaluate every variable when the script is first parsed: a function reads the current value when it runs.
Defaults distinguish missing from empty
${setting:-default} uses the default when setting is unset or empty. ${setting-default} substitutes only when it is unset, preserving an intentional empty value. The colon therefore changes configuration semantics; it is not optional punctuation. ${setting:+enabled} produces enabled only for a set, nonempty value, whereas ${setting+enabled} also accepts an empty one.
Ordinary default expansion does not assign anything. ${setting:=default} additionally stores the selected default in the variable, so subsequent reads see it. ${setting:?message} reports an unset or empty value on standard error and exits a noninteractive shell unsuccessfully. Without the colon, both operators test only whether the variable is unset. Choose according to the caller’s contract, rather than silently replacing required configuration.
Braces also separate a variable name from adjacent text. "${prefix}_suffix" appends a suffix to prefix, while "$prefix_suffix" reads a different variable altogether. This distinction matters when constructing filenames from several settings: an apparently missing value can be a name-boundary mistake rather than absent configuration. Print each component separately before investigating the resulting path, and remember that the final string can be syntactically valid even when one component came from the wrong variable.
Patterns transform strings, not files
${name#pattern} and ${name##pattern} remove the shortest and longest matching prefix. The corresponding suffix operators are % and %%. They use shell patterns, not regular expressions, and leave the original variable unchanged. If nothing matches, the result is the original string. A matching slash is just text here; these operations neither inspect directories nor resolve symbolic links.
Double-quote the complete expansion when its result must remain one argument. Quoting the result does not disable the pattern inside the braces: "${name##*/}" still removes through the last slash. These portable operators differ from Bash extensions such as substring offsets and global replacement. A script invoked as sh must not assume those extensions exist merely because the author’s interactive shell supports them.
When it breaks
An unquoted result can undergo field splitting and filename expansion. Braces delimit the variable name but do not protect argument boundaries. A value containing spaces or an asterisk can consequently turn into unrelated arguments. Also, a default expression is expanded only when selected; placing a command substitution there can execute a command on the missing-value branch. Keep defaults simple when side effects would be surprising.
Parameter expansion is not a command templating language. Text obtained from a variable is not reparsed into shell operators or quoting syntax. Construct argument lists with arrays in Bash, or explicit positional parameters in portable shell, instead of storing a command line and applying eval. set -u catches some missing values but does not validate their meaning, and pipefail has no bearing on these string operations.
Check it quickly
Run this self-contained example with Bash. It changes only variables inside a child shell and prints both the substituted values and the resulting variable state. The brackets make empty output visible.
bash --noprofile --norc <<'BASH'
unset demo_setting
printf 'unset=[%s]\n' "${demo_setting-default}"
demo_setting=
printf 'empty=[%s] colon=[%s]\n' \
"${demo_setting-default}" "${demo_setting:-default}"
printf 'substitute=[%s] stored=[%s]\n' \
"${demo_setting:-fallback}" "$demo_setting"
: "${demo_setting:=saved}"
printf 'assigned=[%s]\n' "$demo_setting"
demo_name='reports/weekly report.tar.gz'
printf 'base=[%s] one=[%s] all=[%s]\n' \
"${demo_name##*/}" "${demo_name%.*}" "${demo_name%%.*}"
printf 'original=[%s]\n' "$demo_name"
show_value() { printf 'current=[%s]\n' "$demo_setting"; }
demo_setting=later
show_value
if (unset demo_setting; : "${demo_setting:?required for demo}") 2>/dev/null; then
printf 'unexpected success\n'
else
printf 'missing value rejected\n'
fi
BASH
Expect unset=[default], then empty=[] colon=[default]. The substitution line leaves stored=[]; the assignment line reports saved. The basename is weekly report.tar.gz, the shortest suffix removal leaves reports/weekly report.tar, and the longest leaves reports/weekly report. The original still contains its complete value.
Finally, the function prints later, proving that its variable was not frozen when the function was defined. The last line confirms rejection without terminating the outer demonstration. These observations distinguish substitution, assignment, and error handling; a successful command alone would not prove that an empty configuration value was preserved correctly.
Sources
- Bash manual parameter expansion, defines expansion operators, checked 2026-06-10.
- POSIX shell parameter expansion, unset versus null values and portable pattern removal, checked 2026-09-07.