How Bash expands a command line
Follow Bash’s expansion steps, slice strings with parameter expansion, and use command substitution and arithmetic.
- List the order of Bash’s expansions and predict a command’s final arguments
- Use parameter expansion for defaults, lengths, trimming and replacing
- Capture command output with $(...) and calculate with $((...))
Before running any command, Bash rewrites the line in a fixed order. Knowing the order turns “why did that happen?!” into “of course”:
- Brace expansion -
{a,b}and{1..3} - Tilde expansion -
~becomes your home directory - Parameter expansion, command substitution
$(...)and arithmetic$((...)), left to right - Word splitting - unquoted results are split at spaces, tabs and newlines
- Pathname expansion - unquoted
*,?and[...]become matching file names - Quote removal - the quotes themselves disappear
Steps 4 and 5 only touch text that came from an expansion and isn’t inside double quotes. That’s the whole reason "$var" is safe and $var isn’t.
Try it
Expansion stepper
Step through each command line one expansion at a time, then predict what the command finally receives. Watch especially for word splitting and globbing on unquoted text.
# variables file="mission log.txt"
$ rm $file
Parameter expansion: string tools built in
Inside ${...}, Bash can do far more than look a variable up - no extra programs needed:
| Expansion | Gives | With file="logs/reactor.tar.gz" |
|---|---|---|
${#var} | length | 19 |
${var:-default} | default if var is unset or empty | |
${var:=default} | same, and assigns it | |
${var:?message} | stops the script with message if unset or empty | |
${var#pattern} | removes the shortest matching prefix | ${file#*/} → reactor.tar.gz |
${var##pattern} | removes the longest matching prefix | ${file##*.} → gz |
${var%pattern} | removes the shortest matching suffix | ${file%.*} → logs/reactor.tar |
${var%%pattern} | removes the longest matching suffix | ${file%%.*} → logs/reactor |
${var/old/new} | replaces the first match (// for all) | ${file/tar/zip} → logs/reactor.zip.gz |
${var^^} / ${var,,} | upper / lower case | ${file^^} → LOGS/REACTOR.TAR.GZ |
${var:start:length} | a substring | ${file:5:7} → reactor |
Memory aid: on a keyboard # is left of $ (so it trims the front) and % is right of it (trims the back); doubling makes it greedy.
1file="logs/reactor.tar.gz"
2echo "${#file} ${file##*/} ${file##*.} ${file%%.*}"
3echo "${file/tar/zip} ${file^^} ${file:5:7}"
4unset captain
5echo "Captain: ${captain:-unassigned}"
6name="ada"
7echo "Hello, ${name^}!"19 reactor.tar.gz gz logs/reactor logs/reactor.zip.gz LOGS/REACTOR.TAR.GZ reactor Captain: unassigned Hello, Ada!
Command substitution and arithmetic
$(command) runs a command and drops its output (minus trailing newlines) into the line. You’ll see old scripts use backticks, `command`, which do the same but nest badly. Quote it like a variable: "$(command)".
$((expression)) does integer arithmetic: + - * / % and ** for powers, with C-style comparisons. Variables inside don’t need $. (( ... )) on its own runs arithmetic as a command - (( count++ )) - and succeeds when the result is non-zero. Bash has no decimals: $((7 / 2)) is 3. Reach for awk or bc when you need fractions.
1cd "$(mktemp -d)"
2touch a.log b.log c.txt
3logs=$(ls *.log | wc -l)
4loudest=$(echo "kestrel" | tr a-z A-Z)
5echo "There are $logs log files aboard $loudest"
6crates=17
7echo "Pallets of 5: $((crates / 5)), leftover: $((crates % 5))"
8(( crates += 3 ))
9echo "Now $crates crates, squared $((crates ** 2))"There are 2 log files aboard KESTREL Pallets of 5: 3, leftover: 2 Now 20 crates, squared 400
Key takeaways
Bash expands in order: braces, tilde, parameters/commands/arithmetic, word splitting, globbing, quote removal.
Word splitting and globbing only hit unquoted expansions - quote to stay safe.
${var#...},${var%...},${var/.../...}and friends slice strings without extra programs."$(cmd)"captures output;$((...))does integer arithmetic.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write Bash scripts
Write a script in the editor and run it for real against sample input. Each run gets a fresh Linux sandbox with Bash 5.2 and the GNU tools on Wandbox, a free public service - so experiment freely, even with rm. Your script and test input are sent there.
Split the path
stdin is one file path. Using only parameter expansion (no basename, dirname, sed or cut), print four lines:
1dir: /station/logs
2file: reactor.2026.log
3name: reactor.2026
4ext: log(for the path /station/logs/reactor.2026.log).
- Reactor log
- Photo
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
Mission clock
stdin is a number of seconds. Print it as HH:MM:SS with leading zeros, then on a second line the total in whole minutes, like:
01:02:05
62 minutes(for 3725 seconds). Use $((...)) and printf '%02d'.
- 3725 seconds
- 59 seconds
- A long shift
Your script runs with Bash 5.2 and GNU tools on Wandbox, a free public service, in a fresh sandbox each time. Your script and test input are sent to that service.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…