Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Bash & the POSIX Shell  Module 2 – How Bash reads a command line

How Bash reads a line: tokens and expansions

Follow the steps Bash takes with every command line, from tokens to quote removal, predict which words are split and globbed, and watch it with -x.

  • Beginner
  • 5 minutes
  • Examples run with Bash 5.3.20, dash 0.5.13.5, shellcheck 0.11.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 5 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • List the kinds of expansion in the order Bash performs them
  • Predict where word splitting and pathname expansion apply and where they do not
  • Use bash -x and set -x to see the exact words a command receives

Before you start

What you’ll learn · Before you start

What you will learn

  • List the kinds of expansion in the order Bash performs them
  • Predict where word splitting and pathname expansion apply and where they do not
  • Use bash -x and set -x to see the exact words a command receives

Before you start

On this page

n=3; echo {1..$n} looks as if it should print 1 2 3, yet Bash 5.3.20 prints {1..3}. Nothing is broken: Bash expands a line in a fixed order, and the braces are handled before the variable gets its value.

You will follow one line through every step Bash takes, from tokens to quote removal, predict which words get split and globbed and which stay whole, and use -x to see the exact words a command receives instead of guessing.

Bash cuts a line into tokens before it expands anything

Before any $ means anything, Bash reads the line and cuts it into token: One piece of a command line as Bash first cuts it up: a word, or an operator such as ;, |, && or >. Operators are found before any expansion happens.: words, and operators such as ;, |, && and >. The Bash manual’s “Shell Operation” lists the steps that follow: the tokens are parsed into commands, each command’s words are expanded, redirections are set up, and the command runs, with the shell waiting for it to finish unless it was started in the background. So in n=3; echo {x,y}-$n *.txt, the ; ends the first command before either one is expanded, and a ; that appears later from a variable’s value is plain text, never an operator. Quoting acts at this stage too: a ; or | inside quotes is part of a word, so echo 'a;b' prints a;b instead of running a second command.

Expansion itself is a series of steps, always in the same order. Step through the line from the session in the next section and watch its words change at each step:

Interactive · stage by stage One command line through Bash's steps Step through the line from the stages session and watch its words change at each step, in the manual's order.
One command line through Bash's steps, step 7 of 7. Showing Tokens, Brace expansion, Tilde to substitution (x-3 y-3 … 6 sub), Word splitting, Filename expansion, Quote removal, The command runs. In focus: The command runs.Tokenswords and operatorsBrace expansion{x,y}-$n: two wordsTilde to substitutionx-3 y-3 … 6 subWord splittingunquoted results onlyFilename expansion*.txt: two namesQuote removalquotes are droppedThe command runsecho gets 7 words

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. Words and operators. Bash first cuts the line into tokens: words, and operators such as ; and |. The line holds one command, echo, with five words after it.
  2. Braces come first. {x,y}-$n becomes two words, x-$n and y-$n. The $n inside is still unexpanded, which is why a range cannot use a variable.
  3. Left to right. Tilde, parameter, arithmetic and command substitution happen together, left to right: $n becomes 3, ~/bin becomes /home/asha/bin, $((n * 2)) becomes 6 and $(printf sub) becomes sub.
  4. Splitting. Unquoted results of those expansions are split on IFS. None of these results holds a space, so the words stay as they are.
  5. Globbing. *.txt matches two files in the folder, so it becomes notes.txt and todo.txt. A pattern that matched nothing would stay as it is.
  6. Quotes go last. Quote characters that were in the original line are removed now. This line had none, so nothing changes here.
  7. Seven words reach echo. echo receives x-3, y-3, /home/asha/bin, 6, sub, notes.txt and todo.txt, exactly the line that bash -x printed after its plus sign.

The figure follows the order the manual gives: brace expansion first; then tilde, parameter, arithmetic expansion and command substitution, together and left to right; then word splitting; then filename expansion; and finally quote removal. Process substitution, <(…), happens at the same time as the left-to-right group.

Because operators are found first, a ; stored in a variable cannot start a second command. This session tries it:

A semicolon inside a variable is only text Python · operator_session.py
from session import run

# Operators are found when the line is cut into tokens, before expansion: a ; that comes out of a variable is only
# text, so Bash looks for a command whose name is "ls;", and notes.txt is never removed.
run("""
touch notes.txt
cmd='ls; rm notes.txt'
!$cmd
ls
""")

Output

$ touch notes.txt
$ cmd='ls; rm notes.txt'
$ $cmd
bash: ls;: command not found
[exit status 127]
$ ls
notes.txt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 operator_session.py

Hiding an operator in a variable

Wrong: cmd='ls; rm notes.txt'; $cmd, expecting two commands. Right: the value is split into the words ls;, rm and notes.txt, so Bash looks for a command called ls; and fails with status 127. Write the commands out, or put them in a function.

Expansions run in a fixed order, so braces cannot use a variable

Here is the line from the figure, run by a Bash started with -x, which prints each command after expansion, with a + in front, before running it. The session sets HOME to /home/asha so the tilde’s result is visible:

Five kinds of expansion in one line, traced Python · stages_session.py
from session import run

# One line that uses five kinds of expansion, run by a Bash started with -x, which prints each command after
# expansion (with "+ " in front) before running it.
run("""
touch notes.txt todo.txt
HOME=/home/asha
bash -xc 'n=3; echo {x,y}-$n ~/bin $((n * 2)) $(printf sub) *.txt'
""")

Output

$ touch notes.txt todo.txt
$ HOME=/home/asha
$ bash -xc 'n=3; echo {x,y}-$n ~/bin $((n * 2)) $(printf sub) *.txt'
+ n=3
++ printf sub
+ echo x-3 y-3 /home/asha/bin 6 sub notes.txt todo.txt
x-3 y-3 /home/asha/bin 6 sub notes.txt todo.txt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stages_session.py

The trace shows the seven words echo received. The line ++ printf sub comes first because the command substitution runs, in a subshell of its own, while the line is being expanded; the doubled plus marks that deeper level. Brace expansion happens before everything else, which explains the puzzle at the top of this lesson:

A range with a variable, two fixes, and dash Python · brace_first_session.py
from session import run

# Brace expansion runs before parameter expansion, so a variable cannot set a range; two ways that work, and dash.
run("""
n=3
echo {1..3}
echo {1..$n}
seq 1 "$n"
for ((i = 1; i <= n; i++)); do printf '%s ' "$i"; done; echo
dash -c 'echo {1..3}'
""")

Output

$ n=3
$ echo {1..3}
1 2 3
$ echo {1..$n}
{1..3}
$ seq 1 "$n"
1
2
3
$ for ((i = 1; i <= n; i++)); do printf '%s ' "$i"; done; echo
1 2 3
$ dash -c 'echo {1..3}'
{1..3}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brace_first_session.py

When the braces are expanded, {1..$n} is not a valid range, so it stays as it is; then $n becomes 3, giving the literal text {1..3}. For a range set by a variable, use seq 1 "$n" or a C-style for ((i = 1; i <= n; i++)) loop. The last command shows that dash, a plain POSIX shell, has no brace expansion at all.

Process substitution belongs to the left-to-right group too. In diff <(sort a.txt) <(sort b.txt) each <(…) runs its command and is replaced by a file name, such as /dev/fd/63, from which the command’s output can be read, so diff compares two sorted files that never exist on disk.

Key idea

Bash expands every word in one order: braces; then tilde, variables, arithmetic and command substitution from left to right; then splitting, globbing and quote removal.

Checkpoint

Check: Tokens and operators come first · The order of expansions

  1. Question 1 of 2 A script holds run='date; whoami' and later runs $run on its own. What does Bash do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It looks for a command called date; and fails with status 127

    Operators such as ; and | are recognised when the line is cut into tokens. Text that a variable supplies later is only split into words, as the operator session showed with ls;.

  2. Question 2 of 2 What does Bash 5.3.20 print for n=2; echo {1..$n}?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: {1..2}

    Brace expansion runs before every other expansion. For a range set by a variable, use seq 1 "$n" or a C-style for loop.

Only unquoted results are split and globbed

Word splitting and filename expansion do not touch everything. They apply to the unquoted results of parameter expansion, command substitution and arithmetic expansion; text written in quotes, and words you typed yourself, are never split again. printf '[%s]\n' prints each word it receives in brackets, which makes the difference visible:

Split, globbed or whole: what printf receives Python · words_session.py
from session import run

# What a command really receives: printf '[%s]' prints each word it was given in brackets. Unquoted, the value of x is
# split and its *.txt is globbed; quoted, it stays one word. An assignment written before a command applies to that
# command only. A redirection's target is expanded too, and must stay one word.
run("""
touch one.txt two.txt
x='a  b   *.txt'
printf '[%s]\\n' $x
printf '[%s]\\n' "$x"
printf '[%s]\\n' a  b   '*.txt'
GREETING=hello bash -c 'printf "%s\\n" "$GREETING"'
printf '[%s]\\n' "$GREETING"
out='my words.txt'
!printf '%s\\n' hi > $out
printf '%s\\n' hi > "$out"
ls
""")

Output

$ touch one.txt two.txt
$ x='a  b   *.txt'
$ printf '[%s]\n' $x
[a]
[b]
[one.txt]
[two.txt]
$ printf '[%s]\n' "$x"
[a  b   *.txt]
$ printf '[%s]\n' a  b   '*.txt'
[a]
[b]
[*.txt]
$ GREETING=hello bash -c 'printf "%s\n" "$GREETING"'
hello
$ printf '[%s]\n' "$GREETING"
[]
$ out='my words.txt'
$ printf '%s\n' hi > $out
bash: $out: ambiguous redirect
[exit status 1]
$ printf '%s\n' hi > "$out"
$ ls
my words.txt
one.txt
two.txt

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 words_session.py

Unquoted, the value of x became four words: a, b, and the two file names that *.txt matched. Quoted, it stayed one word with its spaces and its asterisk. The third command splits on spaces too, but that is the line being cut into tokens, and '*.txt' was quoted, so it is passed as it is. The two GREETING commands show an assignment written before a command name: GREETING=hello reached the one command it was written before, and the shell itself never set it.

The last three commands show where redirections fit. The manual sets them up after the words are expanded, and the word after > is expanded like any other: > $out became two words, my and words.txt, and Bash refused with ambiguous redirect, because a redirection needs exactly one file name. Quoted, > "$out" created my words.txt, as ls shows. Splitting therefore causes errors in redirections as well as wrong arguments, and the same double quotes cure both.

Ask printf, not echo

To see what a command will receive, put printf '[%s]\n' in its place: one bracket pair per word, so empty words, spaces and asterisks all show. echo joins its words with spaces and hides exactly what you are looking for.

A trace shows the words a command really receives

Tracing turns the guesswork into a record. bash -x script.sh runs a script with tracing on, and inside a script or a shell set -x turns it on and set +x off. Each traced line is the command after every expansion, with quotes added wherever a word contains spaces, so you see where one word ends and the next begins:

Tracing a script, with and without a line number Python · trace_session.py
from session import run, write

# bash -x prints every command after expansion, prefixed with PS4 ("+ " by default), before running it; a PS4 with
# $LINENO says which line of the script each trace comes from.
write("greet.sh", '#!/usr/bin/env bash\nname="Asha Rao"\nprintf "[%s]\\n" $name\nprintf "[%s]\\n" "$name"\n')
run("""
cat greet.sh
bash -x greet.sh
PS4='+ line $LINENO: ' bash -x greet.sh
""")

Output

$ cat greet.sh
#!/usr/bin/env bash
name="Asha Rao"
printf "[%s]\n" $name
printf "[%s]\n" "$name"
$ bash -x greet.sh
+ name='Asha Rao'
+ printf '[%s]\n' Asha Rao
[Asha]
[Rao]
+ printf '[%s]\n' 'Asha Rao'
[Asha Rao]
$ PS4='+ line $LINENO: ' bash -x greet.sh
+ line 2: name='Asha Rao'
+ line 3: printf '[%s]\n' Asha Rao
[Asha]
[Rao]
+ line 4: printf '[%s]\n' 'Asha Rao'
[Asha Rao]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_session.py

The first trace shows printf '[%s]\n' Asha Rao: two separate words, because $name was unquoted, and the second shows 'Asha Rao' as one. The prefix is the variable PS4, + by default; setting it to include $LINENO names the line of the script each trace comes from, which helps in a script of a few hundred lines.

Two related tools are worth knowing. set -v prints each line as it is read, before any expansion, so with both -v and -x you see the line as written and then as expanded. And because traces go to standard error, a script’s real output on standard output stays clean; bash -x script.sh 2> trace.log keeps the whole trace in a file to read afterwards.

Checkpoint

Check: What is split and globbed · Tracing with -x

  1. Question 1 of 2 With x='a b *.txt' and two .txt files in the folder, how many words does printf '[%s]\n' "$x" receive after the format?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: One

    Splitting and globbing apply only to unquoted results of expansions; the session printed [a b *.txt] once.

  2. Question 2 of 2 A trace shows + cp report Q3.pdf backup/. What has gone wrong?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The file name was split into two words, report and Q3.pdf, because its variable was unquoted

    A traced word with a space appears in quotes. Seeing report and Q3.pdf apart means "$file" was written as $file.

Practise: predicting what each line becomes

The questions show lines like the ones in the sessions and ask what each command receives. Each choice is explained as soon as you answer, and the step you get wrong most often comes back:

Practice that adapts to you

Practise this lesson

10 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 Which of these are operators that Bash recognises while cutting a line into tokens?

    Choose every answer that is right.

    Show the answer: Question 1 of 10

    Answer:

    • |
    • &&
    • ;

    Operators are found first; words like *.txt are expanded later.

  2. Question 2 of 10 You want one name that runs "ls -l; df -h" whenever you type it. What works?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: A function: info() { ls -l; df -h; }

    Store commands in functions or scripts, which Bash parses as commands, never in variables.

  3. Question 3 of 10 Which expansion does Bash perform first?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: Brace expansion

    Braces, then the left-to-right group, then splitting, globbing and quote removal.

  4. Question 4 of 10 In echo ~/bin $((n * 2)) $(printf sub), which expansions run together, left to right?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: Tilde, arithmetic and command substitution, with parameter expansion

    The stages session expanded ~/bin, $((n * 2)) and $(printf sub) in this one left-to-right step.

  5. Question 5 of 10 A script must print the numbers 1 to $count, where count comes from the user. Which line works in Bash?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: for ((i = 1; i <= count; i++)); do printf '%s ' "$i"; done

    seq 1 "$count" also works; brace ranges need literal numbers.

  6. Question 6 of 10 Which of these does word splitting apply to?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: The unquoted result of $var

    Splitting and globbing touch only unquoted results of parameter expansion, command substitution and arithmetic expansion.

  7. Question 7 of 10 After GREETING=hello bash -c '…' runs, what does printf '[%s]\n' "$GREETING" print in the same shell?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: []

    The session printed hello from the child Bash and then [] in the shell, exactly as the rule says.

  8. Question 8 of 10 pattern='*.log' and the folder holds app.log and db.log. What does printf '[%s]\n' $pattern print?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: [app.log] then [db.log]

    Unquoted expansions are split and then globbed. Quote them unless you want exactly that.

  9. Question 9 of 10 What sets the "+ " in front of every traced line?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: The PS4 variable

    PS4='+ line $LINENO: ' made each trace name its line in greet.sh.

  10. Question 10 of 10 Why did the stages trace show ++ printf sub before + echo …?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: The command substitution ran, one level deeper, while echo's words were being expanded

    Bash repeats the first character of PS4 once for each level of nesting, so commands inside $(…) show ++.

Interview questions

Warm-up (fresher to 2–5 years): in what order does Bash expand a command line?

Bash first cuts the line into tokens, words and operators, and parses it into commands. Then it expands each word in a fixed order: brace expansion; then tilde expansion, parameter and variable expansion, arithmetic expansion and command substitution, all together from left to right, with process substitution at the same time; then word splitting of the unquoted results; then filename expansion; and quote removal last. The order explains real behaviour: {1..$n} stays literal because braces are expanded before $n has a value, and a ; inside a variable’s value is never an operator because operators were found before expansion.

Two harder questions follow, about a command stored in a variable and about a script that breaks on file names with spaces. Work out your answer, then open each first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A deploy script builds a command in a variable, cmd="rsync -av --exclude='*.log' src/ host:/srv", and runs $cmd. The exclude is ignored and log files are copied. Why, and how do you fix it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A backup loop, for f in $(find . -name '*.pdf'); do cp $f backup/; done, fails with "No such file" for some files and copies others. How do you see exactly what went wrong, and what do you change?

    Model answer, Pro. First step and follow-ups

Flashcards · 4 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. Why can a semicolon stored in a variable never start a second command?

    Bash cuts the line into tokens, words and operators such as ; | && and >, before it expands anything. A semicolon that arrives later from a variable's value is plain text inside a word.

  2. In which order does Bash expand the words of a command?

    Brace expansion first; then tilde, parameter and arithmetic expansion and command substitution together, from left to right; then word splitting of unquoted results; then filename expansion; and quote removal last.

  3. Which results of expansion are split into words and globbed?

    Only unquoted ones. A quoted variable such as "$x" stays one word whatever its value holds, which is why quoting a variable keeps spaces and wildcards from changing the command.

  4. How can you see the exact words a command receives after expansion?

    Run the script with bash -x, or turn on set -x inside it. Each command is printed after expansion, behind the PS4 prefix, which is + and a space by default.

Key takeaways

  • Bash cuts a line into tokens and commands before it expands anything, so operators that come from a variable’s value are plain text.
  • Every word is expanded in one order: braces; tilde, variables, arithmetic and command substitution from left to right; then word splitting, filename expansion and quote removal.
  • Only the unquoted results of expansions are split and globbed, which is why quoting a variable keeps its value as one word.
  • bash -x and set -x print each command after expansion, prefixed with PS4, so you can see the exact words a command receives.

Terms in this lesson

token
One piece of a command line as Bash first cuts it up: a word, or an operator such as ;, |, && or >. Operators are found before any expansion happens.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.