Linux & Bash · Part I · Module 9 · Quoting and Shell Expansion
Brace and Tilde Expansion
The first two stages of the pipeline are the two most useful ones to type by hand. Braces turn one word into many; the tilde saves you writing out a home directory. Both are routinely confused with the file-matching stage, and the difference between generating a name and finding one decides which tool you need.
Intermediate17 min readLinuxPart IFundamentalsQuotingExpansion
Part I · Module 9 · Chapter 9.5 · Brace and Tilde Expansion
Chapter 9.1 put braces at stage one and the tilde at stage two, proved that braces run before variables, and moved on. This chapter is the detail — and the distinction that makes braces worth understanding rather than just copying.
1. Braces Generate; Globs Find
$ echo nope_{1..3}.log
nope_1.log nope_2.log nope_3.log
$ echo nope_*.log
nope_*.logThere are no files called nope_1.log in that directory. The brace produced three names anyway. The glob produced nothing — it stayed literal, because nothing matched.
2. The Brace Forms
Lists
$ echo {apple,banana,cherry}
apple banana cherry
$ echo file.{txt,bak}
file.txt file.bakAnything attached before or after the braces is repeated onto each item. That is what makes it useful rather than just a way to type commas.
Ranges
$ echo {1..5}
1 2 3 4 5
$ echo {a..e}
a b c d eRanges work on integers and on single letters, and they count downward too — {5..1} gives 5 4 3 2 1.
Cross products
Put two braces in one word and you get every combination, left varying slowest:
$ echo {A,B}{1,2}
A1 A2 B1 B2
$ echo run_{1..3}_seed{7,9}
run_1_seed7 run_1_seed9 run_2_seed7 run_2_seed9 run_3_seed7 run_3_seed9Six directories from one short line. This is where braces earn their place.
A brace needs a comma or a range
$ echo {single}
{single}
$ echo {}
{}
$ echo {a}
{a}All literal. With nothing to vary, there is no expansion, and the braces are left as ordinary characters. That is deliberate and occasionally useful, but it mostly matters as an explanation for why a brace you expected to expand did not: check that it contains either a comma or a ...
The backup idiom
$ echo cp config.yml{,.bak}
cp config.yml config.yml.bakThe empty item before the comma contributes the bare word. It reads strangely the first time and is extremely common:
cp config.yml{,.bak} # back it up
mv run.log{,.old} # rotate it3. Braces and Variables, Again
Chapter 9.1 proved braces run first. In practice that means one specific thing does not work:
$ n=5
$ echo {1..$n}
{1..5}The three ways round it:
echo $(seq 1 "$n") # a command, so stage 3
for ((i = 1; i <= n; i++)); do ...; done # evaluated at run time
eval "echo {1..$n}" # works, and see 9.4 §6Prefer the second in a script and the first at a prompt. The third is here only so you recognise it; eval turns the whole line back into code, for the reasons Chapter 9.4 gave.
Note the asymmetry: a variable inside the braces is too late, but a variable next to them is fine, because that part is just text being repeated:
$ prefix=run
$ echo ${prefix}_{1..3}
run_1 run_2 run_3Stage one copies the literal text ${prefix}_ onto each item, and stage three then substitutes it in all three copies.
4. The Tilde
$ echo ~
/home/engineer
$ echo ~/work
/home/engineer/work~ at the start of a word becomes your home directory. Not in the middle — /tmp/~/x is a literal tilde — and only as the first character.
The four forms
| Form | Becomes |
|---|---|
~ | your home directory |
~user | that user's home directory |
~+ | the current directory — the same as $PWD |
~- | the previous directory — the same as $OLDPWD |
$ echo ~root
/var/root
$ cd /tmp
$ echo ~+
/tmp
$ echo ~-
/home/engineer/project~- pairs with the cd - from Chapter 2.4 — same idea, usable inside a longer path:
cp report.txt ~-/archive/Quotes switch it off
Chapter 9.3 covered this and it is worth repeating here, because it is the one place where the quote-everything habit actively hurts:
$ echo ~/project
/home/engineer/project
$ echo "~/project"
~/projectmkdir -p ~/backups # correct
mkdir -p "~/backups" # creates a DIRECTORY NAMED ~ in the current dirThat second line does not fail. It silently makes a directory called ~, which then sits there confusing people and is awkward to delete, because rm -rf ~ means something else entirely. (rm -rf ./~ is the safe spelling.)
5. Using Them Together
Setting up a regression tree is the natural worked example, and it is one line:
$ mkdir -p ~/runs/sweep_{1..3}/{logs,waves,reports}
$ find ~/runs/sweep_1 -type d | sort
/home/engineer/runs/sweep_1
/home/engineer/runs/sweep_1/logs
/home/engineer/runs/sweep_1/reports
/home/engineer/runs/sweep_1/wavesNine directories, from one command. The cross product gave sweep_1/logs, sweep_1/waves, sweep_1/reports, sweep_2/logs and so on; -p created the intermediate levels.
Then, once they contain something, the other tool:
grep -l ERROR ~/runs/sweep_*/logs/*.logBrace to create, glob to search — in two consecutive commands, which is the clearest possible statement of §1.
Practice
1 — Generate.
echo {1..5}
echo {a..e}
echo {5..1}
echo {apple,banana}
echo file.{txt,bak,log}2 — Cross-multiply.
echo {A,B}{1,2}
echo run_{1..3}_seed{7,9}
echo {A,B}{1,2}{x,y}3 — Find the ones that stay literal.
echo {single}; echo {}; echo {a}; echo {1.5}4 — Check your Bash version against the two risky forms.
bash --version | head -1
echo {1..10..3}
echo {05..10}
seq 1 3 10
seq -w 5 105 — Prove braces precede variables.
n=4
echo {1..$n}
echo $(seq 1 "$n")
prefix=run; echo ${prefix}_{1..3}6 — Build and then search.
mkdir -p /tmp/bt/sweep_{1..3}/{logs,waves}
find /tmp/bt -type d | sort
touch /tmp/bt/sweep_{1,3}/logs/run.log
echo /tmp/bt/sweep_*/logs/*.logNote that the glob found two files, not three — sweep_2 has no log, and the glob only ever names what exists.
7 — Walk the tilde forms.
echo ~
echo ~root
cd /tmp && echo ~+ && echo ~-8 — Make the quoting mistake safely.
cd /tmp/bt
mkdir -p "~/oops"
ls -d ./~
rm -rf "./~"9 — Watch a tilde from a variable stay literal.
t="~/project"; echo "$t"; echo "${t/#\~/$HOME}"10 — Clean up. rm -rf /tmp/bt
Check Your Understanding
1. What is the difference between echo run_{1..3}.log and echo run_*.log in an empty directory?
2. echo {single} prints {single}. Is something wrong?
3. n=4; mkdir run_{1..$n} creates one directory. What is it called, and why?
4. mkdir -p "~/backups" succeeds but your home directory is unchanged. What happened?
5. A config file contains logdir = ~/logs and your script does cd "$logdir", which fails. Why, and what are two fixes?
6. You want eight directories run_01 to run_08, zero-padded, on any machine. How?
6. Remember
- A brace generates text; a glob finds files. The brace never consults the filesystem; the glob can only name what exists.
- Brace to create, glob to search.
mkdir -p run_{1..8}to make them, thengrep ERROR run_*/*.logto search them. - Lists and ranges:
{a,b,c}and{1..5}and{a..e}, with attached text repeated onto each item. - Two braces in one word give a cross product, left varying slowest.
- A brace with no comma and no range stays literal.
cp config.yml{,.bak}is the backup idiom — the empty item gives the bare word.{1..10..3}and zero-padded{05..10}need Bash 4, and macOS ships 3.2. Useseqandseq -wfor portability.- A variable cannot supply a brace range (stage one precedes stage three), but a variable beside a brace is fine.
~expands only at the start of a word:~,~user,~+for here,~-for the previous directory.- Quotes switch off the tilde, and
mkdir "~/x"silently creates a directory called~. - Use
"$HOME"in scripts and~at the prompt. - A tilde arriving from a variable is never expanded — it is data by then. Fix it with
"${v/#\~/$HOME}". - Put
echoin front of a destructive brace before running it. A brace gives no feedback when the names are wrong.
Two stages left unexamined, and they are the two that cause damage: what happens when an expansion produces nothing, and what happens when it produces far too much.
Where this fits
Part of the Linux & Bash curriculum.
