bash parameter expansion, one expansion per page
Thirty lessons on the ${var} mini-language inside bash: the plain dollar and brace forms, defaults with :- and :=, stopping the run with :?, the alternate branch :+, counting characters with ${#var}, slicing by position and from the end, case with ^^ and every trim and replace form from # and ## through % and %%, anchored substitution, indirection with ${!var}, positional parameters from $1 to ${10}, $# and shift, "$@" versus "$*", $0, arrays read out with ${arr[@]}, sized, sliced and keyed, and why the quotes keep each value whole, with one real terminal task per page, from defaulting empty inputs to renaming file extensions in a loop.
One ${...} form, one prompt line, one result. That's a page.
bash parameter expansion, one expansion per page
Thirty lessons on the ${var} mini-language inside bash: the plain dollar and brace forms, defaults with :- and :=, stopping the run with :?, the alternate branch :+, counting characters with ${#var}, slicing by position and from the end, case with ^^ and every trim and replace form from # and ## through % and %%, anchored substitution, indirection with ${!var}, positional parameters from $1 to ${10}, $# and shift, "$@" versus "$*", $0, arrays read out with ${arr[@]}, sized, sliced and keyed, and why the quotes keep each value whole, with one real terminal task per page, from defaulting empty inputs to renaming file extensions in a loop.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Every fact in this book is as the official source states it, fetched and read during this build: the Bash Reference Manual sections Shell Parameter Expansion (the ${parameter} forms, the colon rule for unset versus null, substring offsets and lengths, the pattern trims, substitutions and case modification, indirect expansion and name prefixes), Special Parameters ($* $@ $# $? $$ $! $0), Positional Parameters ($1 to $n, the braces ${10} needs), Arrays (${arr[@]} versus ${arr[*]}, lengths, slices, keys), Word Splitting (when unquoted expansions split), and Shell Parameters (assignment, set versus unset); the BASH(1) man page's Parameter Expansion section as shipped with bash 5.2.21; and the Wikipedia article Bash (Unix shell). Every code claim on every page was run in local bash 5.2.21 and its output confirmed before printing. No facts are sourced from Reddit, Stack Overflow or tutorials. An independent guide, not affiliated with or endorsed by the GNU Project or the Bash maintainers.
Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
Per Bash Reference Manual, Shell Parameter Expansion: the $ character introduces parameter expansion, and the basic form ${parameter} substitutes the value of parameter.
Dollar and braces
Parameter expansion is the mini-language inside every script, and it starts with one character: the dollar sign. Write $name and bash substitutes the value of the variable name.
The braces are optional here: ${name} is the same expansion, the manual's basic form. Where braces matter is the next page's story; for now, read both as one thing: the value.
Every form in this book is this one, plus operators packed inside the braces.
Set name=report.txt at the prompt, print it with $name, then with ${name}, and confirm the two lines match.
Per Bash Reference Manual, Shell Parameter Expansion: ${parameter:-word} substitutes the expansion of word if parameter is unset or null, otherwise the value of parameter; word is itself subject to parameter expansion.
A default with :-
Scripts meet empty variables constantly: the unset flag, the missing option. ${name:-guest} substitutes guest when name is unset or null, and the value itself otherwise.
It is an if-statement compressed into an expansion: the fallback travels with the use, not three lines above it.
The word is expanded first, so ${dir:-$HOME/tmp} falls back to a live value.
With EDITOR unset, echo "${EDITOR:-nano}"; then set EDITOR=vi and run the same line again.
Per Bash Reference Manual, Shell Parameter Expansion: a negative offset counts from the end of the value and must be separated from the colon by at least one space to avoid being confused with the :- expansion; a negative length is read as an offset from the end.
Count from the end
A negative offset counts from the end: ${string: -7} keeps the last seven characters. The manual is blunt: separate the minus from the colon with a space, or bash reads a default.
A negative length is an offset from the end too: ${string:7:-2} runs from character seven to two from the end.
Tail slices: extensions, suffixes, the newest part of a version string.
With ver=1.2.3-rc1, cut the rc tag with ${ver: -3} and the numeric core with ${ver:0:-4}, and print both halves.
Per Bash Reference Manual, Shell Parameter Expansion: the ^^ and ,, variants examine all characters in the expanded value and convert each one that matches the pattern.
Shout it, whisper it
Double the operator and the whole value converts: ${s^^} uppercases every character, ${s,,} lowercases every one.
Use it on filenames that arrive SHOUTING: mv "$f" "${f,,}" and filesystem case-sensitivity stops biting.
Case conversion as a pure expansion: nothing executed, nothing forked.
Rename three uppercase REPORT files to lowercase with a for loop and mv "$f" "${f,,}", keeping the extensions.
Per Bash Reference Manual, Shell Parameter Expansion: ${parameter#word} deletes the shortest matching pattern from the beginning of the expanded value of parameter; if the pattern does not match, the value is unchanged.
Trim the front
The hash trims from the front. ${path#*/} matches the pattern */ against the start of the value and removes the shortest match: one directory slash from /usr/share/doc.
The pattern follows the pathname rules: * ? and character classes all work. If it does not match, the value comes through untouched.
One token off the front: a prefix, a protocol, a leading dash.
Strip one leading directory from three paths with ${path#*/} and keep the results for the greedy form on the next page.
Per Bash Reference Manual, Shell Parameter Expansion: with two slashes, all matches of pattern in the expanded value are replaced with string; applied to @ or an array, the operation runs on each element in turn.
Replace every match
Two slashes sweep the whole value: ${line//one/1} replaces every match. Commas to spaces is the classic: ${csv//,/ }.
Applied to @ or an array subscripted with @, the substitution runs on each element in turn, which meets this book again on the array pages.
One expansion rewrites every separator, every token, everywhere.
Convert a comma-separated list to one space-separated line with ${csv//,/ }, then loop over it with for w in $list.
Per Bash Reference Manual, Shell Parameter Expansion: an exclamation point immediately after the left brace introduces indirect expansion, where the value formed by expanding the rest of parameter becomes the new parameter that is expanded.
The name in a name
An exclamation point right after the brace adds a hop: ${!whichvar} reads whichvar, gets a name, then expands that variable. whichvar=PRIORITY with PRIORITY=high yields high.
The manual calls it indirect expansion, and the bang must sit immediately after the left brace: one level of indirection, no eval anywhere.
The lookup table without an eval: variable names chosen at run time.
With env=prod and prod_host=eu-1, build name="${env}_host" then print ${!name} and confirm you read prod_host.
Per Bash Reference Manual, Special Parameters: within double quotes $* expands to a single word with each parameter separated by the first character of IFS, while $@ expands each parameter to a separate word.
$@ versus $*
Unquoted, both forms split. Quoted, they part ways: "$@" keeps each argument its own word, while "$*" joins them into one word with the first character of IFS between.
The manual's equations: quoted star is "$1c$2c" and on; quoted at is "$1" "$2" and on. Loop over at, print with star.
In a for header, quoted at is nearly always what you mean.
Run for w in "$@" and for w in "$*" with arguments "web server" cache, and count the passes each makes.