Quoting & Globbing: What the Shell Does Before Your Command Runs
- Explain that the shell rewrites your command line (expansion) BEFORE the command ever runs
- Use *, ?, and […] as globs — filename patterns the shell expands — and explain why a glob is NOT a regex
- Pick the right quotes: none, double, or single — and say exactly what each one turns off
- Predict when an unquoted variable or glob will bite (empty values, spaces, accidental matches) and quote to prevent it
The shell edits your line first
Internalize the one model that makes quoting and globbing make sense: expansion happens before the command runs.
Here is the idea that quietly explains half of a beginner's “why did that happen?!” moments: the command you type is not the command that runs. Before your program ever starts, the shell rewrites the line — it expands
*into matching filenames, swaps$HOMEfor its value, splits the result into words, and only then hands the finished list of words to the command.So
ls *.txtisn't a command that means “list txt files.” The shell first turns*.txtintoa.txt b.txt c.txt, then runsls a.txt b.txt c.txt.lsnever saw a*at all.Once you can see that rewrite happening, quoting and wildcards stop being magic: quotes control which rewrites the shell is allowed to do, and globs are just one of the rewrites.
mkdir glob-demo && cd glob-demo mkdir Documents Downloads touch notes.txt photo.jpg echo *Documents Downloads notes.txt photo.jpg
echo just prints its arguments — so this proves what the shell did to the line. echo never received a ''; the shell expanded '' into the names in the current directory (the four we just created) and handed those to echo. The command is a passive witness to the shell's rewrite. Globbing: wildcards are the shell's, and they are NOT regex
Use *, ?, and […] to match filenames, and stop conflating them with regular expressions.
Globbing (a.k.a. filename expansion) is the shell matching patterns against names in the filesystem. Three pieces cover almost everything:
- *`
** — any run of characters, including none (*.log→ every name ending in.log`). - `?` — exactly one character (
file?.txt→file1.txt,fileA.txt, but notfile10.txt). - `[…]` — one character from a set or range (
[abc],[0-9]).
Two quiet rules that surprise people: a leading-dot “hidden” file is not matched by
*(so*won't touch.bashrc), and a glob only expands if it matches something — if nothing matches, bash hands the command the literal pattern text unchanged.Example — not runnable on your lab box
ls *.confapp.conf db.conf nginx.conf
The shell expands *.conf to the three matching filenames and runs 'ls app.conf db.conf nginx.conf'. This is filename matching against the current directory — not a search of file contents, and not a regex. Example — not runnable on your lab box
grep -c 'error.*timeout' app.log4
- -c
- print the count of matching lines instead of the lines
Here '.' is a REGEX (grep's language): '.' = any char, '' = zero-or-more of it, so '.' = any run of characters between 'error' and 'timeout'. Written as a glob you'd say '', but grep isn't globbing — it's matching a regex against each line's content. Same symbols, different language. Check yourself
In the shell, which wildcard matches exactly ONE character?
- *`
Quoting: three levels, and what each one turns off
Choose no quotes, double quotes, or single quotes deliberately by knowing which expansions each allows.
Quotes are how you tell the shell “do less rewriting to this part.” There are exactly three levels:
- No quotes — everything happens: variables expand, globs expand, and the result is split into words on spaces/tabs.
- "Double quotes" — variables (
$x) and command substitution ($(…)) still expand, but word-splitting and globbing are turned OFF. Spaces inside are preserved as one argument. - 'Single quotes' — nothing expands. Every character is literal, including
$,*, and spaces. (The one thing single quotes can't contain is another single quote.)
A short way to hold it: single = literally this, double = this, but let variables in, none = rewrite freely.
name='Ada Lovelace'; echo "$name" | wc -w; echo $name | wc -w2 2
Both print 2 words of content — but for different reasons, and the difference matters the moment a command counts ARGUMENTS instead of words. In double quotes, "$name" is ONE argument ('Ada Lovelace'). Unquoted, $name is split into TWO arguments ('Ada' and 'Lovelace') before the command sees them. wc -w happens to count words either way; most commands do not. echo '$HOME costs $5 and * stays literal'$HOME costs $5 and * stays literal
Single quotes disable EVERY expansion: $HOME is not substituted, $5 is untouched, and * is not globbed. Use single quotes whenever you want the shell to keep its hands off — regex patterns for grep, awk programs, passwords in a config, literal dollar signs. Where it bites — and how quoting saves you
Recognize the three classic failures caused by unwanted expansion and fix each with the right quotes.
The rewrite model predicts exactly where beginners get burned. Three cases cover most of them:
1. Empty or spaced variables.
grep $q file— if$qis empty, grep gets no pattern; if it'stwo words, grep treatstwoas the pattern andwordsas a filename.grep "$q" filefixes both.2. Filenames with spaces.
rm my file.txttries to remove two files,myandfile.txt.rm "my file.txt"(orrm 'my file.txt') removes the one you meant.3. Globs firing when you didn't want them.
find . -name *.log— if asomething.logexists in your current directory, the shell expands*.logbefore find runs, and find gets the wrong argument. Quote it —find . -name '*.log'— so find receives the pattern and does the matching itself.q=''; grep "$q" /etc/hostname; echo "exit: $?"exit: 2
A safety demo: with q empty, even the QUOTED form shows grep reacting to an empty pattern — but the argument boundaries are still correct (one empty pattern, one file). Unquoted (grep $q /etc/hostname) the empty value vanishes entirely and grep would treat /etc/hostname itself as the PATTERN and wait on stdin. Quoting keeps your arguments in the right slots even when a value is surprising. find /var/log -name '*.log' -maxdepth 2/var/log/dpkg.log /var/log/alternatives.log
- -name '*.log'
- quoted so find (not the shell) expands the glob, against the directory being searched
- -maxdepth 2
- don't recurse deeper than two levels
Quoting '.log' hands the literal pattern to find, so FIND walks /var/log and does the matching there — which is the whole point. Unquoted, the shell would try to expand '.log' against your CURRENT directory first, and find would search the wrong place (or error if there were several matches). When a tool has its own pattern language, quote the pattern so the tool — not the shell — interprets it. Check yourself
You write find . -name *.txt and it behaves oddly only when you're in a directory that already contains a .txt file. What fixes it?
Now do it — watch the shell rewrite your line
Run a few tiny experiments so the expand-then-run model becomes something you can see, not just believe.
Three quick experiments, each one a moment where the abstract idea snaps into something concrete. Do them in your live terminal in order. First, prove that globbing is the shell's doing and the command is just a witness — use
echoso the only thing you see is the rewrite itself.echo *; echo '*'; echo "*"Same three characters, three results. Unquoted '' is expanded by the shell into your filenames. Single-quoted '' and double-quoted "" are both left literal (a bare has no variable to expand, so double quotes keep it literal too), so echo prints a real asterisk. You just watched quoting switch one specific rewrite — globbing — on and off. (Your terminal supplies the output.) x='a b'; printf '[%s]\n' $x; printf '[%s]\n' "$x"Feel word-splitting directly. printf reuses its format for each argument, so counting the […] lines counts the arguments. Unquoted $x splits into TWO arguments ('a' and 'b') → two lines. Quoted "$x" is ONE argument (spaces preserved) → one line. This is exactly why you quote variables that might hold spaces. grep -n 'root' /etc/passwd; echo '--- vs a glob ---'; ls /etc/pass*Finally, hold both languages at once. On the left, 'root' is a REGEX handed to grep (quoted so the shell leaves it alone), matching text inside /etc/passwd. On the right, /etc/pass* is a GLOB the shell expands to a filename before ls runs. Same line, two different pattern languages — and you kept them straight. That is the whole skill.
- The shell rewrites your line before the command runs: it expands globs and variables, splits into words, then executes. The command never sees the original *, $, or spaces — only the result.
- Globs (, ?, […]) are filename matching done by the shell, and are NOT regex: glob = 'any characters', regex * = 'zero or more of the previous thing'. grep/sed/awk take regexes; a bare pattern on the command line is a glob.
- Three quoting levels: none (expand + word-split), "double" (variables expand, no splitting/globbing), 'single' (everything literal).
- Default to double-quoting variables ("$var") to survive empty or space-containing values, and single-quote a tool's own pattern (find -name '.log', grep 'a.b') so the tool — not the shell — interprets it.
| You type | Variables expand? | Globbing / word-split? | Use it when |
|---|---|---|---|
| bare word | yes | yes | you want a glob or word-splitting to happen |
| "double" | yes | no | using a variable that may hold spaces / be empty |
| 'single' | no | no | a literal string: a grep/find pattern, an awk program |
| find -name '*.log' | n/a | no (quoted) | the TOOL should expand the pattern, not the shell |
