Shell Expansion & Quoting
How the shell rewrites your commands before executing them — and how to control it
Quick check before you start: Do you know what
echo $HOMEprints and why? If you can explain that$HOMEis a variable the shell expands beforeechoever 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:
- Brace expansion –
{A,B,C},{1..5} - Tilde expansion –
~,~username - Parameter expansion –
$USER,${var} - Arithmetic expansion –
$((2+2)) - Command substitution –
$(command) - Word splitting – divides results into separate arguments
- 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 textQ*. 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$Nhas 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:
echodoes not interpret\nby default. You must useecho -eto enable escape sequence interpretation. Alternatively, useprintf, which always interprets escape sequences:printf "Line one\nLine two\n".
echo "The cost is $((5*3)) for $USER"$((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.