Variables, Quoting and Expansion · about 40 min

Quoting: single, double and backslash

Why this matters

A surprising number of characters mean something to bash: space, $, *, ?, ;, &, |, <, >, (, ), quotes, backslash, #, ~, !, newline. File names, user input, passwords and messages contain these all the time. Quoting is how you tell bash "this is data, not syntax". Getting it right is the single most important habit in shell scripting; most shell bugs, and many security holes, are quoting bugs.

The problem quoting solves

When bash reads a line, it first breaks it into tokens using the special characters. A space separates words, ; ends a command, > redirects, $ starts an expansion, * is a wildcard. Try:

$ echo Price: $5; total
Price:
bash: total: command not found

Two things went wrong. $5 is a special parameter (the fifth argument of a script, empty here) so it expanded to nothing. The ; ended the echo command, and total was run as a second command. Quoting fixes both:

$ echo 'Price: $5; total'
Price: $5; total

Three ways to quote

Single quotes: everything literal

Everything between ' and the next ' is taken literally. No expansion, no escapes, nothing:

$ echo '$HOME is where `the` heart is; * \n'
$HOME is where `the` heart is; * \n

The one character you cannot put inside single quotes is a single quote, because the next ' always ends them, and a backslash there is just a backslash.

Double quotes: literal, except for $, backtick and backslash

Inside "..." spaces and most special characters lose their meaning, but expansions still happen: $variable, ${...}, $(command), $((arithmetic)) and the old backtick form. A backslash is special only before $, `, ", \ or a newline.

$ name=Ada
$ echo "Hello, $name; you have * new messages"
Hello, Ada; you have * new messages
$ echo "Say \"hi\" and pay \$5"
Say "hi" and pay $5
$ echo "back\slash"
back\slash

The * stayed a star: no file name expansion inside double quotes. The ; did not end the command. But $name was expanded. This makes double quotes the everyday tool for strings that contain variables, and, as lesson 4 shows, the essential tool for using variables at all.

Backslash: one character

Outside quotes, a backslash removes the special meaning of the single next character:

$ echo Price: \$5\; total
Price: $5; total
$ touch my\ file.txt
$ ls
my file.txt

A backslash at the very end of a line escapes the newline, so the command continues on the next line. This is how long commands are split for readability:

tar -czf backup.tar.gz \
    --exclude='*.tmp' \
    documents/ photos/

(Make sure nothing, not even a space, follows that backslash. \ followed by newline is an escaped space, and the command ends there.)

Mixing quotes in one word

Quoted and unquoted pieces next to each other with no space between them form a single word. This lets you combine styles:

$ echo 'It'"'"'s '"$name"'!'
It's Ada!
$ echo 'It'\''s fine'
It's fine

Read the first as five adjacent pieces: 'It', "'", 's ', "$name", '!'. The second idiom, '\'', means: close the single quotes, an escaped ', reopen. It is ugly but common in scripts that generate shell code.

ANSI-C quoting: $'...'

bash has a fourth form: $'...', where backslash escapes work as in C:

$ echo $'tab:\there\nnew line, and an apostrophe: \''
tab:    here
new line, and an apostrophe: '

\t is a tab, \n a newline, \\ a backslash, \' a single quote. It is not POSIX (module 16 covers portability), but it is the clearest way to put a tab or newline into a bash string.

echo itself does not interpret \n by default in bash: echo 'a\nb' prints a\nb. That is one reason many scripts prefer printf, which always does:

$ printf 'a\tb\n'
a    b
$ printf '%s has %s files\n' "$name" 3
Ada has 3 files

printf FORMAT ARGS... prints the format, replacing each %s with the next argument. Unlike echo, it adds no newline of its own. You will use it a lot from now on.

Quote removal: what the program actually gets

Quotes are instructions to bash, not data. After bash has used them to decide where words begin and end and what to expand, it deletes them. This last step is literally called quote removal:

you type:      touch  'my notes.txt'   "$name.log"   it\'s
bash parses:   word 1 │ word 2           │ word 3      │ word 4
after expansion and quote removal:
argv[0]=touch  argv[1]=my notes.txt   argv[2]=Ada.log   argv[3]=it's

touch receives three file names, none containing any quote character. Programs never see your quotes (unless you quote a quote, as in "say \"hi\""). This also means quoting is a shell concept: once bash has built the argument list and called execve, quoting no longer exists.

Comments and other characters

An unquoted # at the start of a word begins a comment that runs to the end of the line:

$ echo one # two three
one
$ echo one#two
one#two
$ echo 'one # two'
one # two

In an interactive bash, ! inside double quotes can trigger history expansion (!! means "the last command"). If echo "Hello!" behaves oddly in your terminal, use single quotes. Scripts do not have history expansion, so this only bites interactively.

Common mistakes

Exercises

Do the quizzes in your head first, then try each line in the shell to confirm. The code exercise is compared character for character, so every quote and space counts.

Recap

Going further: run help printf and try printf '%5s|%-5s|\n' ab cd. Then use printf '[%s]\n' ... to inspect exactly which arguments a line produces: printf '[%s]\n' one "two three" 'four'"five".

Exercises

Exercise 1 · quiz

Single or double?

HOME is /home/learner. Which lines print exactly cost: $HOME (with a literal dollar sign and the word HOME)? Select all that apply.

Answer
  • echo 'cost: $HOME'

  • echo "cost: \$HOME"

  • echo cost: \$HOME

Inside single quotes nothing is special, so $HOME stays as typed. Inside double quotes $ still triggers expansion, so the second line prints cost: /home/learner; a backslash before the $ turns that off for one character. Outside any quotes a backslash also protects the next character. All three working forms end with the same argument after quote removal.

Exercise 2 · quiz

An apostrophe inside single quotes

You want to print It's done. Why does echo 'It\'s done' leave bash waiting with a > prompt instead of printing?

Answer
  • Backslash does not escape anything inside single quotes, so the second ' ends the quoted part and the third ' opens a new one that is never closed.

Inside single quotes every character is literal, including the backslash, and the only thing that can end them is the next '. So 'It\' is a complete quoted word (containing It\), then s done follows unquoted, then ' opens a new quoted string that never closes. bash shows its continuation prompt > waiting for the closing quote (press Ctrl-C to escape). Working forms: "It's done", 'It'\''s done' (close, escaped quote, reopen), or $'It\'s done'.

Exercise 3 · write code

Print it exactly

Write a script that prints these three lines exactly (every character matters):

Total: $5 for "two" items
It's 100% done
Path is: /work and not $PWD

The third line must get /work by expanding the variable PWD (the checker runs the script in /work), while the second $PWD on that line is printed literally.

File: script.sh

Starting point

echo Total: $5 for "two" items
echo It's 100% done
echo Path is: $PWD and not $PWD

Example

expected output

Total: $5 for "two" items
It's 100% done
Path is: /work and not $PWD
Hint 1

Line 1 contains a $ and double quotes but no apostrophe: single quotes protect everything.

Hint 2

Line 2 contains an apostrophe: put it inside double quotes.

Hint 3

Line 3 needs one expansion and one literal $: double quotes, and a backslash before the literal dollar.

Reference solution
echo 'Total: $5 for "two" items'
echo "It's 100% done"
echo "Path is: $PWD and not \$PWD"

Exercise 4 · task in a shell

Files with awkward names

In /work, create these three empty files with touch. The names are shown between the markers >> and <<, which are not part of the names:

Check your work with ls -b, which shows unusual characters escaped.

Hint 1

Each name must reach touch as a single argument with no expansion: which quotes do that?

Hint 2

Single quotes work for all three: touch 'my notes.txt' '$HOME.txt' 'say "hi".txt'.

Reference solution
touch 'my notes.txt'
touch '$HOME.txt'
touch 'say "hi".txt'