| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required for the examples |
| Scope | Bash expansion order from brace expansion through quote removal: how stages interact, why unquoted variables split and glob, special cases such as "$@" and "${array[@]}", and how to inspect final arguments with printf '%q'. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash does not pass the command you type directly to a program. Before execution, it performs several expansions in a defined order: brace expansion, tilde expansion, parameter and variable expansion, arithmetic expansion, command substitution, word splitting, filename expansion, and finally quote removal.
That order explains common surprises such as why {1..$n} does not work, why an unquoted variable can become several arguments, and why a wildcard stored inside a variable may unexpectedly match filenames.
What Is Shell Expansion?
Shell expansion is the set of transformations Bash applies to your input after parsing tokens but before executing the command. These expansions help determine the words and arguments Bash ultimately uses when executing the command.
Bash first parses the command into shell tokens and recognizes quoting, operators, assignments, and redirections. Shell expansion then operates on the appropriate words; it is not the initial parsing step itself. When you type echo hello world, parsing already separated those words — word splitting runs later on unquoted expansion results, not on literal source text.
Expansion is not optional decoration. When you type rm *.log, the shell may pass three pathnames to rm, not the two-character pattern *.log. When you type "$HOME/bin", the shell passes one argument with your home directory expanded inside it.
Only some steps increase the number of words. Brace expansion, word splitting, and filename expansion normally do. Quoted "$@" and "${array[@]}" are important exceptions that still produce multiple arguments despite the quotes.
Bash Expansion Order
GNU Bash documents this order. Treat it as the mental model for every example below:
| Step | Expansion | Can increase word count? |
|---|---|---|
| 1 | Brace expansion | Yes |
| 2 | Tilde, parameter/variable, arithmetic, command substitution; process substitution when supported | Normally no; "$@", "$*", and array expansions have special cases |
| 3 | Word splitting | Yes |
| 4 | Filename/pathname expansion | Yes |
| 5 | Quote removal | No |
Within the second stage, Bash performs tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution in a left-to-right fashion. Process substitution, when supported, occurs at the same time.
What about history expansion (
!)?Bash history expansion is separate from the shell-expansion sequence above and is primarily an interactive-shell feature. It happens earlier, before normal parameter/variable expansion. It is usually disabled in non-interactive shell scripts. GNU Bash stores history after history expansion but before parameter/variable expansion. When commands never reach
~/.bash_historyat all, that is a persistence and startup-file problem—see fix Bash history not saving commands.
Build an Argument-Inspector Helper
To see what a command actually receives, print each argument with an index:
show_argv() {
local i=1
for arg; do
printf ' %d: <%q>\n' "$i" "$arg"
((i++))
done
}For a quick one-liner without a function, printf 'arg=<%q>\n' "$@" wraps each word the same way. The %q format is a Bash printf feature useful for displaying arguments in reusable shell-escaped form — it is not part of portable POSIX printf. Pair that with Bash echo vs printf when you also need format strings or stderr routing.
Follow a Command Through Every Expansion Stage
Track one unquoted expansion from start to finish. Prepare three log files in a disposable directory, including one name with spaces:
mkdir -p ~/bash-exp-lab && cd ~/bash-exp-lab
rm -f *.log 2>/dev/null
touch app.log error.log 'file with spaces.log'With value='two words *.log' and default IFS:
- Parameter expansion —
$valueexpands totwo words *.log; it has not been split into separate words yet. - Word splitting — default
IFSsplits that result intotwo,words, and*.log. - Filename expansion — the word
*.logmatches three files; the other two words are unchanged. - Quote removal — nothing quoted; no change.
- Command —
printfreceives five arguments.
The inspector confirms the final argv:
unset IFS
cd ~/bash-exp-lab
value='two words *.log'
show_argv $value 1: <two>
2: <words>
3: <app.log>
4: <error.log>
5: <file\ with\ spaces.log>Change only the quoting and the pipeline collapses to one word:
show_argv "$value" 1: <two\ words\ \*.log>One pair of double quotes moved the same variable from five arguments to one.
Brace Expansion Happens Before Variable Expansion
Brace expansion runs before parameter expansion. That is why a variable inside {start..end} does not become a numeric range.
Set n=3 and try to brace-expand a range from that variable:
n=3
printf '<%s>\n' {1..$n}<{1..3}>Bash treats {1..$n} as literal text because $n is not expanded during brace expansion. The parameter expansion phase runs later and never rewrites brace syntax that already failed to expand.
When a variable sits next to a brace expression, make the boundary explicit with ${name}:
name=report
printf '<%s>\n' ${name}{1..3}.txt<report1.txt>
<report2.txt>
<report3.txt>Brace expansion created three words before any glob or split step ran.
How do I use a variable inside Bash brace expansion?
For a runtime numeric range, use an arithmetic loop or seq instead of forcing brace syntax:
n=3
for ((i=1; i<=n; i++)); do
printf '%s\n' "$i"
done1
2
3For simply generating numbers:
seq 1 "$n"1
2
3Do not solve this with eval "echo {1..$n}". Although eval can force the generated text to be parsed again, it introduces unnecessary quoting and code-injection problems. For runtime numeric ranges, an arithmetic loop or seq is the safer pattern.
Tilde, Parameter, Arithmetic, and Command Substitution
After brace expansion, Bash performs tilde expansion, parameter and variable expansion, arithmetic expansion, and command substitution in a left-to-right fashion.
Tilde expansion replaces an unquoted leading ~ with your home directory path ($HOME). A quoted tilde stays literal:
printf 'unquoted: <%q>\n' ~
printf 'quoted: <%q>\n' "~"unquoted: </home/golinuxcloud>
quoted: <\~>The exact path depends on the current user; on your system it is whatever $HOME contains.
Parameter expansion substitutes $var and ${var} values. See Bash variables for assignment and ${var} forms, and Bash parameter expansion for ${var:-default} and related patterns.
Arithmetic expansion evaluates expressions inside $((...)) before word splitting. For example:
n=4
printf '%s\n' $((n + 1))5More complex operators and arithmetic syntax are covered in Bash arithmetic.
Command substitution runs here too: $(command) or legacy backticks are replaced by the command's stdout before word splitting. Quoting, nesting, subshell side effects, and trailing-newline behavior are easy to get wrong — see Bash command substitution for the full walkthrough.
On systems that support process substitution, <(command) and >(command) are processed during the same general phase as tilde, parameter, arithmetic, and command substitution. The result normally appears to the command as a pathname such as /dev/fd/63.
Word Splitting Happens After Variable Expansion
Word splitting breaks unquoted expansion results into separate shell words using IFS. It runs on the results of parameter expansion, command substitution, and arithmetic expansion when those results are not inside double quotes.
Double quotes suppress splitting on the quoted portion while still allowing $ expansions inside. Full IFS rules and empty-field behavior are in Bash IFS and word splitting; here the key idea is timing: splitting happens after $var becomes text and before globs run.
Command substitution output is subject to splitting when unquoted:
printf 'arg=<%q>\n' $(printf 'a b\n')arg=<a>
arg=<b>The substituted string contained a space, so the shell passed two arguments to printf.
Filename Expansion Happens After Word Splitting
Filename expansion (globbing) runs after word splitting. Each unquoted word that contains glob characters (*, ?, […]) is tested against the filesystem. Each matching pathname becomes its own shell word.
Ask a direct glob what Bash expands:
cd ~/bash-exp-lab
printf 'arg=<%q>\n' *.logarg=<app.log>
arg=<error.log>
arg=<file\ with\ spaces.log>Globbing did not break the spaced filename. Each matched pathname is one word. Problems appear when an earlier word-splitting step tore user data apart before globbing ran. Pathname rules, nullglob, and shopt options are covered in Bash wildcards and globbing.
Can a wildcard stored in a Bash variable expand?
Yes, when the variable is unquoted. Globbing runs on words produced by unquoted parameter expansion:
pattern='*.log'
printf 'pattern unquoted:\n'
printf ' <%q>\n' $pattern
printf 'pattern quoted:\n'
printf ' <%q>\n' "$pattern"pattern unquoted:
<app.log>
<error.log>
<file\ with\ spaces.log>
pattern quoted:
<\*.log>When the variable holds only a glob pattern, unquoted expansion runs filename expansion directly. When the value also contains spaces, word splitting runs first:
value='two words *.log'
printf 'arg=<%q>\n' $valuearg=<two>
arg=<words>
arg=<app.log>
arg=<error.log>
arg=<file\ with\ spaces.log>Quoting stops both splitting and globbing:
printf 'arg=<%q>\n' "${value}"arg=<two\ words\ \*.log>A glob inside double quotes does not expand:
show_argv "*.log" 1: <\*.log>When nothing matches and nullglob is off, an unquoted non-matching pattern usually stays literal.
Quote Removal Happens Last
Quotes do not reach the program as part of the argument text. They control earlier expansion behavior, then Bash removes them when building argv.
Compare unquoted and double-quoted expansion of a variable that holds a glob pattern:
pattern='*.log'
show_argv $pattern 1: <app.log>
2: <error.log>
3: <file\ with\ spaces.log>The same variable quoted stays one literal word:
show_argv "$pattern" 1: <\*.log>Quote removal is the final step. By then, word splitting and globbing have already decided how many words exist. Bash single vs double quotes compares literal single quotes, expanding double quotes, and $'…' syntax in depth.
Quoted vs Unquoted Variable Expansion
The difference is not cosmetic. Unquoted $var can pass through parameter expansion, word splitting, and filename expansion.
With "$var", parameter expansion still occurs, but Bash does not perform word splitting or filename expansion on the resulting value. The surrounding quote characters are removed later during quote removal. Depending on what the quoted construct contains, other expansions such as command substitution and arithmetic expansion can still occur inside double quotes.
| Form | Parameter expansion | Word splitting | Filename expansion |
|---|---|---|---|
$var |
Yes | Yes on the result | Yes on each resulting word |
"$var" |
Yes | No | No |
Use "$var" when the value may contain spaces, tabs, glob characters, or an empty string. Use unquoted $var only when you intentionally want splitting or globbing on the expanded text.
Special Cases: "$@", "$*", and Bash Arrays
Most double-quoted text becomes one word. Several parameter and array forms are exceptions:
| Expression | Inside double quotes |
|---|---|
"$@" |
One word per positional parameter |
"$*" |
One word; parameters joined by the first character of IFS (space when IFS is unset; no separator when IFS is null) |
"${array[@]}" |
One word per array element |
"${array[*]}" |
One word; elements joined using the same IFS rule |
Set two positional parameters and compare "$*" with "$@":
unset IFS
set -- alpha beta
printf '"$*": '; printf '<%q>\n' "$*"
printf '"$@": '; printf '<%q>\n' "$@""$*": <alpha\ beta>
"$@": <alpha>
<beta>The quotes are still removed, but "$@" preserves separate words — one per argument. That is why "$@" is the safe way to forward script arguments; see Bash script multiple arguments for forwarding patterns.
The same distinction applies to indexed arrays:
items=(one two)
printf '"${items[@]}": '; printf '<%q>\n' "${items[@]}"
printf '"${items[*]}": '; printf '<%q>\n' "${items[*]}""${items[@]}": <one>
<two>
"${items[*]}": <one\ two>Array iteration, slicing, and sparse indices are covered in Bash arrays.
Why Assignments Behave Differently
For a simple assignment such as value=$other, Bash does not run the resulting value through ordinary word splitting and filename expansion the way an unquoted command argument does.
GNU Bash's simple command expansion treats assignment words separately. The value after = undergoes tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal — but not ordinary word splitting or filename expansion.
Compare storing a value through assignment with using it unquoted on a command line:
cd ~/bash-exp-lab
value='one two *.log'
copy=$value
printf 'stored in copy: <%q>\n' "$copy"
printf 'unquoted on command line: '; printf '<%q>\n' $copystored in copy: <one\ two\ \*.log>
unquoted on command line: <one>
<two>
<app.log>
<error.log>
<file\ with\ spaces.log>The assignment copy=$value stored the full string intact — spaces and *.log included — without word splitting or filename expansion on the right-hand side. Word splitting and globbing run only when you expand $copy unquoted as a command argument.
In scripts: assignment preserves the value; quoting matters when you pass that value to commands.
Debug Bash Expansion Problems
When a script passes the wrong number of arguments, inspect argv before blaming the program.
printf 'arg=<%q>\n' "$@" (or the show_argv helper above) shows each word after all expansions. That is clearer than guessing from the source line alone.
set -x traces commands after expansion, normally writing the trace to stderr:
bash -x -c 'value="two words"; printf "<%q>\n" $value'+ value='two words'
+ printf '<%q>\n' two words
<two>
<words>The trace shows two and words as separate words on the printf line — after splitting, before your program runs. Tracing does not replay pre-expansion source text, so pair set -x with %q inspection when you need exact boundaries.
Common Bash Expansion Mistakes
| Mistake | What actually happens |
|---|---|
{1..$n} builds a range from a variable |
Brace expansion runs before $n expands; use an arithmetic loop, seq, or ${name}{1..3} with an explicit prefix |
IFS splits literal shell source code |
IFS affects word splitting on expansion results, not parsed command text |
| Globs always break filenames with spaces | Each glob match is one pathname word; splitting usually broke the name earlier |
| Quotes are passed to the program | Quotes control expansion, then are removed; "$var" is not part of argv |
Summary
Bash applies expansions in a fixed order: brace expansion first; then tilde, parameter, arithmetic, command, and process substitution left to right; then word splitting on unquoted results; then filename expansion; then quote removal. Most surprises come from forgetting that $var runs through splitting and globbing while "$var" keeps the expanded value as one word.
Use printf '%q\n' or an argument inspector to see final argv. Quote variables that may contain spaces or glob metacharacters. Treat "$@" and "${array[@]}" as the exceptions that stay multi-word even inside double quotes. The linked Bash quoting, IFS, globbing, and expansion guides cover those individual behaviors in more detail.
References
- GNU Bash Manual — Shell Expansions — authoritative expansion order
- GNU Bash Manual — Simple Command Expansion — assignment words and how expansion applies before execution
- GNU Bash Manual — Word Splitting —
IFSand field boundaries - GNU Bash Manual — Filename Expansion — pathname globbing after splitting
Frequently Asked Questions
What is the order of Bash expansion?
Brace expansion runs first, then tilde expansion plus parameter, arithmetic, and command substitution (and process substitution on the same phase), then word splitting, then filename expansion, then quote removal. That is the order GNU Bash documents for building the final argument list.
Does Bash perform word splitting before filename expansion?
Yes. Parameter, command, and arithmetic expansion produce text first. Unquoted results then undergo word splitting, and the resulting words are subsequently checked for filename expansion. GNU Bash documents precisely that order.
Can I use a variable inside Bash brace expansion?
Not for a runtime numeric range like {1..$n}. Brace expansion runs before parameter expansion, so $n is not expanded inside the braces. Use an arithmetic loop or seq 1 "$n" for dynamic ranges, or ${name}{1..3} when the brace bounds are literal.
Why does unquoted $var turn into filenames?
After parameter expansion produces text, word splitting may break the value into several words. Filename expansion then runs on each unquoted word that looks like a glob. A variable holding *.log can expand to every matching file in the directory.
Does a quoted glob expand?
No. Inside double quotes, * and ? are ordinary characters. The string "*.log" stays one literal argument unless you remove the quotes.
Why can quoted "$@" still become multiple arguments?
"$@" is a special case. Each positional parameter becomes a separate word even though quotes normally prevent splitting. The quotes still suppress globbing and unwanted splitting within each parameter.

