LearnLinux FoundationsBash Shell Environment

Expansion Ordering

bash - a hands-on Linux lab on a real virtual machine.

The capstone that ties the Bash Environment module together: the fixed order the shell applies expansions to a command line. Brace expansion first (text-only), then tilde, then variables, arithmetic, and command substitution, then word splitting, then filename globbing, then quote removal. Three ranks explain almost every surprise: brace runs before variable (so {1..$n} cannot count), variable runs before splitting and globbing (which is why "$var" is safe), and quotes come off last. Verified byte-stable terminal demos make each one visible.

You have learned braces, variables, globs, and command substitution one at a time across this module. Now watch two commands that look almost identical behave completely differently. Both use {1..N}, the brace range you already met, to count.

The first spells the top number out: echo {1..3}. It counts, printing 1 2 3. The second stores that top number in a variable first, then asks for {1..$n}. You would bet money it also prints 1 2 3. It does not. It prints something that looks broken. Nothing is broken. The shell is simply doing its steps in an order you have not been shown yet, and this lesson is that order.

The black boxes in this lesson are a practice terminal: a safe sandbox that checks the one command each step teaches, so you cannot break anything. Almost every output here is byte-for-byte identical on any machine, because it is decided by the shell's rules, not by your disk. The single exception is echo ~, which prints YOUR home folder, so that one output will read differently for you. It is marked where it appears.

Before the shell runs a single program, it rewrites your command line. It expands the shortcuts in the text you typed. It turns {1..3} into 1 2 3, turns ~ into your home folder, turns $USER into your name, and turns * into matching filenames. Only after all that rewriting does it actually run anything.

Here is the part nobody tells beginners: those rewrites happen in a fixed order, always the same order, every time. It is not left-to-right by where they sit on the line. Each KIND of expansion runs as a whole pass, and the passes are ranked. Learn that ranking once and a whole category of confusing behavior stops being mysterious. It becomes predictable.

Think of an assembly line. Your typed command enters at one end. It passes through stations in a set sequence, each station doing one job, and comes out the far end as the final list of words the shell will run. You cannot reorder the stations. You can only understand them.

This is the whole lesson in one table. The shell walks your command line through these passes, top to bottom, always in this order:

Three ranks in that table explain almost every surprise you will ever hit. Brace is FIRST, before variables. Variables are step 3, BEFORE word splitting at step 4 and globbing at step 5. And quotes come off LAST, at step 6, after they have already done their protecting. The rest of this lesson makes each of those three visible in the terminal.

Start with the counting mystery from the top. Brace expansion is station 1. Variable expansion is station 3. So when the shell reaches a brace range, it has NOT yet looked at any $ variable. The $n inside the braces is still just the three characters dollar-e-n, not the number 3.

A brace range only expands when both ends are plain numbers RIGHT THERE in the text. {1..3} qualifies, so it becomes a list. {1..$n} does not, because at station 1 the $n is not a number yet, it is unresolved text. Bash sees a brace range with a non-number end, gives up on it, and leaves the whole thing exactly as typed.

First run the version that works, so you have the baseline. Before you press Enter, commit to it: this one should count.

echo {1..3}

prompt: student@linuxcamp:~$ answer: echo {1..3} output: 1 2 3 hint: Type echo, a space, then the brace range with plain numbers: echo {1..3}

Both ends are plain numbers sitting right in the text, so station 1 expanded the range into the list 1 2 3 before anything else ran. That is brace expansion doing exactly its job. Now change one end into a variable and watch the order bite.

Now the version that surprises everyone. You set n=3, then ask for {1..$n}. You expect 1 2 3 again. But brace expansion at station 1 runs BEFORE variable expansion at station 3, so when the braces are considered, $n has not been turned into 3 yet. The range is not two plain numbers, so bash leaves it untouched. By the time station 3 finally expands $n, the brace moment is long gone.

Before you run it, commit to the real outcome, not the hoped-for one: because of the order, this will NOT count. It will print the pattern with the braces still in it.

n=3; echo {1..$n}

prompt: student@linuxcamp:~$ answer: n=3; echo {1..$n} output: {1..3} hint: Set n first, then echo the range that uses it: n=3; echo {1..$n}

Read that output carefully. It printed {1..3}, WITH the braces, not 1 2 3. The $n did get replaced by 3 at station 3, which is why you see 3 and not the letter n. But station 1 had already refused the range and moved on, so the braces were never expanded into a list. This is the single most common brace mistake: reaching for a variable inside a range. The order forbids it. If you need a variable count, you reach for a different tool, the C-style for (( )) loop or seq, which you meet in later work. The brace range is text-only, and text-only means the numbers must be in the text.

Station 2 is small but worth seeing once. A bare ~ is not a folder name. It is a shortcut the shell rewrites into the full path of your home folder, and it happens at station 2, right after braces and before variables.

Make the invisible visible: echo ~ prints what the tilde turns into.

echo ~

prompt: student@linuxcamp:~$ answer: echo ~ output: /home/student hint: Just echo, a space, and a single tilde: echo ~

That path is this account's home, /home/student. YOURS will read differently, whatever your own username's home folder is, because the tilde expands to the home of whoever is running the command. The lesson is not the exact path, it is that a bare ~ becomes a real absolute path at station 2, long before the command runs.

This is the payoff that ties the quoting lessons to the ordering. Variable expansion is station 3. Word splitting is station 4. Globbing is station 5. So a variable is expanded FIRST, and only THEN, if it is unquoted, does its result get split on spaces and matched against filenames.

That single fact is the whole reason "$var" is safe. Quotes tell the shell to skip stations 4 and 5 for that value. The variable still expands at station 3, so you get its contents, but the contents are then handed on whole, never split, never matched against files. Quoting does not stop the variable from expanding. It stops what happens to the value AFTER it expands.

See the safe case first. Put a glob character inside a variable, then echo it quoted. Before you run it, commit: with quotes, the value should print exactly as stored, star and all.

files="a*.txt"; echo "$files"

prompt: student@linuxcamp:~$ answer: files="a*.txt"; echo "$files" output: a*.txt hint: Store the pattern, then echo it inside double quotes: files="a*.txt"; echo "$files"

The variable expanded at station 3 into a*.txt, and then the double quotes told the shell to skip word splitting and globbing for it. So the * never went hunting for files. The value came through untouched, exactly the characters you stored. That is what quoting buys you: the value, and only the value.

Same stored value, a*.txt, but this time echo it WITHOUT quotes. Now nothing tells the shell to skip stations 4 and 5. The variable still expands at station 3 into a*.txt, but then that result reaches station 5, where the * is a live glob again. It looks around for real files whose names match, and if it finds one, it swaps the pattern for the filename.

In this sandbox a file named a.txt exists in the current folder. Before you run it, commit to the consequence of the missing quotes. The stored a*.txt gets expanded by the variable, then the star matches the real file, so you will NOT see the star in the output.

files="a*.txt"; echo $files

prompt: student@linuxcamp:~$ answer: files="a*.txt"; echo $files output: a.txt hint: Same stored value, but echo it with no quotes this time: files="a*.txt"; echo $files

Look at the delta. Quoted, you got the literal a*.txt. Unquoted, you got a.txt, a real filename. Nothing about the variable changed. The ONLY difference was the quotes, and the quotes control whether stations 4 and 5 run on the value. This is the quiet bug that catches people: an unquoted variable that both splits on its spaces AND then glob-matches against whatever files happen to be sitting in the folder. Your command starts behaving differently depending on what is on disk. The fix, the same fix as always, is to quote the variable: "$files". When in doubt, quote it.

Scaffolding off. No command is printed this time. You have every piece you need.

Show the ordering rule for yourself. Set a variable n to 3, then in the same line echo a brace range from 1 to $n. You already know the outcome: because brace expansion runs before variable expansion, the range will not count. Type the command that proves it.

prompt: student@linuxcamp:~$ answer: n=3; echo {1..$n} output: {1..3} hint: Set n to 3, then echo a brace range whose top end is the variable. Two statements on one line, joined by a semicolon.

That is the ordering made concrete. You set n=3, but when the shell reached the braces at station 1, $n was still unresolved text, so the range refused and stayed as written. Station 3 then turned $n into 3, which is why you see {1..3} and not {1..n}. Braces first, variables after: you just typed the proof from memory, which is the recall the real machine will ask of you.

You earned this. It is the assembly line, in order, and the three insights that make it pay off:

The three that matter most:

When any command surprises you, replay it in your head as the assembly line. Ask which station each piece belongs to and which runs first. Nine times out of ten the surprise is an ordering one, and nine times out of ten the fix is a pair of double quotes.

The practice terminal has shown you the whole assembly line. Braces run first and are text-only, so a variable inside a range cannot count. Tilde becomes your home. Variables, arithmetic, and command substitution run next, and crucially BEFORE word splitting and globbing, which is exactly why quoting a variable keeps it safe. Quotes come off last, after they have done their protecting. Every one of those you typed yourself and watched happen.

This lesson closes the Bash Environment module. The module ends with one real Linux machine, the Bash Environment capstone mission. A VM boots just for you, with its own shell, and there you put the whole module together for real: variables, quoting, expansions, and the order that governs all of them. One difference from here: the mission shows no commands. You read the objective, you recall the form, you type it. That recall is what makes it stick.

Finish any remaining Bash Environment lessons, then go run the capstone and put the whole module to work.

Practice Expansion Ordering in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.