student@ubuntu:~$
unix-foundations Lesson 3 10 min read

Shell Expansion & Quoting

How the shell rewrites your commands before executing them — and how to control it

Reading: Shotts, The Linux Command Line: pp. 93–103

Quick check before you start: Do you know what echo $HOME prints and why? If you can explain that $HOME is a variable the shell expands before echo ever sees it, you are ready for this lesson. If not, review Shell Environment first.

After this lesson, you will be able to:

  • Identify the seven types of expansion the shell performs on every command line
  • Explain the order in which expansions happen
  • Use double quotes, single quotes, and backslash escaping to control which expansions occur
  • Construct commands that use brace expansion and arithmetic expansion to save time

The Big Idea: The Shell Rewrites Your Commands

Every time you press Enter, bash does not send your text directly to the program you named. Instead, it scans the line, performs a series of substitutions called expansions, and hands the rewritten result to the program. The program never sees your original text.

This is the single most important concept for understanding shell behavior. When something unexpected happens at the command line, the answer is almost always: “the shell expanded something you did not expect.”


Expansion Types

Bash performs seven kinds of expansion, in this order:

  1. Brace expansion{A,B,C}, {1..5}
  2. Tilde expansion~, ~username
  3. Parameter expansion$USER, ${var}
  4. Arithmetic expansion$((2+2))
  5. Command substitution$(command)
  6. Word splitting – divides results into separate arguments
  7. Pathname expansion*, ?, [...]

The order matters. Brace expansion happens first, so its output can be further expanded by later stages. Pathname expansion happens last, so it operates on the fully expanded text.

Key Insight: Word splitting (step 6) is not something you invoke. It happens automatically on the results of parameter expansion, arithmetic expansion, and command substitution. This is why quoting matters – double quotes suppress word splitting.


1. Pathname Expansion

When the shell sees *, ?, or [...] in an unquoted word, it replaces that word with the list of matching filenames. This is what you have been using with ls *.c – the shell expands *.c into a list of filenames before ls ever runs.

echo *               # Lists all files in the current directory
echo D*              # Files starting with D: Desktop Documents
echo /usr/*/share    # Matches /usr/local/share, /usr/kerberos/share, etc.

The echo command here is not doing anything special. It simply prints its arguments. The shell expanded * into filenames and passed them as separate arguments to echo.

Common Pitfall: If no files match the pattern, bash leaves the pattern unchanged. Running echo Q* in a directory with no Q-files prints the literal text Q*. This is confusing but intentional.


2. Tilde Expansion

A ~ at the beginning of a word expands to the current user’s home directory. A ~username expands to that user’s home directory.

echo ~               # /home/student
echo ~root           # /root
cd ~/Documents       # Changes to /home/student/Documents

Tilde expansion only works at the beginning of a word. Writing echo hello~ does not expand the tilde.


3. Arithmetic Expansion

The shell can evaluate integer arithmetic using $(( )):

echo $((2 + 2))      # 4
echo $((5 ** 2))     # 25  (exponentiation)
echo $((10 / 3))     # 3   (integer division, no decimals)
echo $((10 % 3))     # 1   (modulo -- remainder)

The supported operators:

Operator Meaning
+ Addition
- Subtraction
* Multiplication
/ Integer division
% Modulo (remainder)
** Exponentiation

Expressions can be nested:

echo $(( ($((5**2)) * 3) ))   # 75
echo $(( (5**2) * 3 ))        # 75 (cleaner with grouping)

4. Brace Expansion

Brace expansion generates multiple text strings from a pattern. Unlike the other expansions, it does not look at files on disk – it is purely textual.

Comma-separated lists:

echo Front-{A,B,C}-Back
# Front-A-Back Front-B-Back Front-C-Back

Ranges:

echo {1..5}             # 1 2 3 4 5
echo {Z..A}             # Z Y X W V U T S R Q P O N M L K J I H G F E D C B A
echo {01..15}           # 01 02 03 ... 15  (zero-padded)

Practical use – create a directory structure:

mkdir -p Photos/{2024..2026}-{01..12}

This single command creates 36 directories: Photos/2024-01, Photos/2024-02, …, Photos/2026-12. Without brace expansion, you would need 36 separate mkdir calls.

Key Insight: Brace expansion happens before all other expansions, which means you cannot use a variable inside braces. Writing echo {1..$N} does not work because $N has not been expanded yet when brace expansion runs.


5. Parameter Expansion

Variables are expanded by prefixing the name with $:

echo $USER            # student
echo $HOME            # /home/student
echo $PATH            # /usr/local/bin:/usr/bin:/bin:...

If you misspell a variable name, the shell expands it to an empty string with no error:

echo $UESR            # (blank -- no error, no warning)

Use ${var} when the variable name is adjacent to other text:

echo "${USER}_backup"  # student_backup
echo "$USERbackup"     # (blank -- shell looks for variable USERbackup)

6. Command Substitution

Command substitution replaces $(command) with the output of that command:

echo "Today is $(date)"
# Today is Mon Mar 23 10:15:00 PDT 2026

ls -l $(which cp)
# -rwxr-xr-x 1 root root 71516 ... /usr/bin/cp

The older backtick syntax also works but is harder to read and cannot be nested:

echo "Today is `date`"     # Same result, older syntax

Prefer $(command) in all new code. Backticks exist for historical compatibility.

You can use entire pipelines inside command substitution:

file $(ls -d /usr/bin/* | head -5)

From Java: String Interpolation vs. Shell Expansion

If you have used Java’s String.format() or text blocks, shell expansion will feel partially familiar – but it is fundamentally different.

Java Bash
String.format("Hello %s", name) echo "Hello $USER"
Interpolation happens at runtime, inside the program Expansion happens before the program runs
The program controls substitution The shell controls substitution; the program never sees the original text
No equivalent Pathname expansion (*.c), brace expansion ({1..5})

The critical difference: in Java, your program decides what to substitute. In bash, the shell rewrites the command line before your program ever executes. The program receives the expanded result as its arguments.

# What you type:
echo *.c

# What echo actually receives (after shell expansion):
echo main.c utils.c test.c

echo has no idea that * was involved. It only sees three filenames as three separate arguments.


Controlling Expansion: Quoting

Now that you know the shell aggressively expands your input, you need to know how to stop it. Bash provides three quoting mechanisms.

Double Quotes: Selective Suppression

Double quotes suppress word splitting, pathname expansion, brace expansion, and tilde expansion. They allow parameter expansion, arithmetic expansion, and command substitution.

echo "this is a    test"
# this is a    test          (spaces preserved -- no word splitting)

echo "*.c"
# *.c                        (literal asterisk -- no pathname expansion)

echo "$USER owns $(hostname)"
# student owns cs-lab01      (parameter + command substitution still work)

echo "{A,B,C}"
# {A,B,C}                    (literal braces -- no brace expansion)

Double quotes are essential for handling filenames with spaces:

ls -l "two words.txt"       # Correct: treats as one filename
ls -l two words.txt         # Wrong: ls looks for "two" and "words.txt"

Single Quotes: Total Suppression

Single quotes suppress all expansion. Every character between single quotes is treated literally:

echo 'text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER'
# text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER

Nothing was expanded. Use single quotes when you want the shell to leave your text completely alone.

Comparison of Quoting Levels

# No quotes -- all expansions active:
echo text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER
# text /home/me/ls-output.txt a b foo 4 me

# Double quotes -- parameter, arithmetic, and command substitution still active:
echo "text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER"
# text ~/*.txt {a,b} foo 4 me

# Single quotes -- nothing expanded:
echo 'text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER'
# text ~/*.txt {a,b} $(echo foo) $((2+2)) $USER

Escaping with Backslash

A backslash before a special character removes its special meaning. This is useful inside double quotes when you want to suppress a specific expansion:

echo "The balance for user $USER is: \$5.00"
# The balance for user student is: $5.00

The first $USER was expanded. The \$ was treated as a literal dollar sign.

Common escape sequences with echo -e:

echo -e "Line one\nLine two"
# Line one
# Line two

echo -e "Column1\tColumn2"
# Column1    Column2
Sequence Meaning
\\ Literal backslash
\$ Literal dollar sign
\n Newline (with echo -e)
\t Tab (with echo -e)

Common Pitfall: echo does not interpret \n by default. You must use echo -e to enable escape sequence interpretation. Alternatively, use printf, which always interprets escape sequences: printf "Line one\nLine two\n".


Check Your Understanding
What does the following command print?
echo "The cost is $((5*3)) for $USER"
AThe cost is $((5*3)) for $USER
BThe cost is 15 for student
CThe cost is 15 for $USER
DThe cost is $((5*3)) for student
Answer: B. Double quotes allow parameter expansion and arithmetic expansion. $((5*3)) expands to 15, and $USER expands to the current username. Only single quotes would suppress these expansions.

What Comes Next

You now understand how bash transforms every command line through expansion before any program executes, and how to use quoting to control that process. These concepts apply to every shell command you will write for the rest of this course – from compiling C programs to writing shell scripts.

In the next lesson, you move into C Foundations with grep and regular expressions – tools for searching text patterns across files. The * in grep is not the same as the * in pathname expansion, and understanding that distinction depends on everything you learned here.