---
title: 'Variables and Quoting Rules'
source: 'https://academia.sh/en/courses/shell-programming/variables-and-quoting-rules'
course: 'Shell Programming'
language: en
updated: '2026-08-17T18:10:02+00:00'
license: 'CC BY-SA 4.0'
---

# Variables and Quoting Rules

Assignment syntax, expansion order, word splitting and pathname expansion, the three quoting forms, and default-value expansions.

The log's name in the script is hardcoded for now. Taking it into a variable
looks, at first glance, like a matter of tidiness. In the shell, though, this
means stepping into the language's most error-prone area: expanding a
variable is not a single operation but a multi-stage process, and which
stages apply depends on quoting.

In the Programming Fundamentals course, a variable was a name bound to a
value; reading it gave that value. In the shell, the situation is different:
the text that comes out when a variable is read gets re-resolved together
with the rest of the command line. This lesson's subject is exactly that
re-resolution.

## Assignment

Assignment is written without spaces on either side of the equals sign.

```sh
name = value
```

```
bash: name: command not found
```

The shell took the word `name` as a command name, and `=` and `value` as
arguments. Because the shell splits the line into words by whitespace first,
it could not have behaved otherwise. The correct form has no spaces:

```sh
name=value
echo "[$name] [${name}] [${name}less]"
```

```
[value] [value] [valueless]
```

The braced form marks the boundary between the name and the text that
follows it. If `$nameless` had been written, the shell would look for a
variable named `nameless`.

Variables in the shell have no type; every value is a character string. The
assignment `n=5` does not produce an integer, it produces a two-character
string. A separate notation is used when a numeric operation is needed; this
is the subject of the Arithmetic and Command Substitution lesson.

## What Happens After Expansion

Expanding a variable does not finish the job. The shell applies two more
operations to the result of an unquoted expansion:

1. **Word splitting.** The resulting text is split into words according to
   the characters in the `IFS` variable. `IFS`'s default value is space,
   tab, and newline.
2. **Pathname expansion.** If words contain `*`, `?`, `[...]` characters,
   they are replaced with matching file names.

The result of both operations can be observed. The helper below prints the
number of arguments it receives and each argument separately:

```sh
show_args() { printf 'arguments: %d\n' "$#"; printf '  <%s>\n' "$@"; }
```

```sh
text="two   spaced"
show_args $text
show_args "$text"
```

```
arguments: 2
  <two>
  <spaced>
arguments: 1
  <two   spaced>
```

The unquoted expansion produced two words, the quoted expansion produced
one. The three spaces between them also disappeared in the unquoted case —
word splitting does not preserve separators.

## The Cost of Unquoted Expansion

This behavior has a concrete counterpart in scripts. Consider a file name
that contains a space:

```sh
log_file="access log.log"
show_args $log_file
show_args "$log_file"
```

```
arguments: 2
  <access>
  <log.log>
arguments: 1
  <access log.log>
```

In the unquoted form, the command received two nonexistent file names. In a
line like `rm $log_file`, this means the wrong file gets deleted.

Pathname expansion adds a second layer. If the variable contains a pattern
character, unquoted expansion looks at the file system:

```sh
cd "$(mktemp -d)"          # an empty directory containing only two files
: > "access log.log"
: > missing.log

pattern="*.log"
show_args $pattern
show_args "$pattern"
```

```
arguments: 2
  <access log.log>
  <missing.log>
arguments: 1
  <*.log>
```

Note the order: the variable's value was `*.log`; after expansion, word
splitting, and pathname expansion, two file names emerged. And
`access log.log` arrived as a single argument — because **the result of
pathname expansion is not subject to word splitting.** Splitting applies
only to the variable's own value.

From this comes the rule that will hold for the rest of the course:

> **Variable expansions are always enclosed in double quotes.** Leaving one
> unquoted is done only when word splitting or pathname expansion is
> deliberately wanted.

This rule also applies to `"$var"`, `"$@"`, `"$(command)"`, and
`"${array[@]}"`.

## Three Quoting Forms

**Double quotes** apply expansions but block word splitting and pathname
expansion. Variable, command, and arithmetic expansion still work inside.

**Single quotes** expand nothing; every character inside passes through
literally. A single quote cannot be used inside single quotes — not even a
backslash escapes it.

**Backslash** strips the single character after it of its special meaning.
Inside double quotes it is effective only before `$`, `` ` ``, `"`, `\`, and
a newline; in front of other characters it stays literal.

```sh
text="two   spaced"
echo $text
echo "$text"
echo '$text'
```

```
two spaced
two   spaced
$text
```

On the first line, `echo` received two separate arguments and printed them
with a single space between; on the second line, the spaces were preserved;
on the third, no expansion happened at all.

Regular expressions and `sed` patterns are written in single quotes: the
`$`, `*`, and `\` characters that appear in patterns are not meant to be
interpreted by the shell. This rule will hold throughout the Text
Processing topic.

## IFS and Deliberate Splitting

The `IFS` (internal field separator) variable determines which characters
word splitting happens at. Changing it is one of the ways to split a
delimited line into its fields:

```sh
line="10.0.0.12:404:512"
IFS=:
show_args $line
```

```
arguments: 3
  <10.0.0.12>
  <404>
  <512>
```

`IFS` is a global setting; once changed, it affects every unquoted
expansion in that shell from then on. Forgetting to restore its old value
produces hard-to-explain errors in the script's later lines. The correct
pattern is to set `IFS` for a single command only:

```sh
IFS=: read -r address code bytes <<< "$line"
```

An assignment written before a command is valid only in that command's
environment; once the command finishes, the old value returns. This
pattern will be used in the Loops lesson when reading the log line by
line.

## Default-Value Expansions

The shell offers a family of expansions for handling undefined or empty
variables on the spot. Whether a colon is present determines whether an
"empty" value also counts as missing.

| Expansion | If the variable is undefined | If the variable is empty |
|---|---|---|
| `${v-fallback}` | `fallback` | empty string |
| `${v:-fallback}` | `fallback` | `fallback` |
| `${v=fallback}` | assigns and gives `fallback` | empty string |
| `${v:=fallback}` | assigns and gives `fallback` | assigns and gives `fallback` |
| `${v:?message}` | errors and exits | errors and exits |
| `${v:+value}` | empty string | empty string |
| `${#v}` | 0 | 0 |

```sh
unset LOG_FILE
echo "1 undefined, with :-: [${LOG_FILE:-access.log}]"
LOG_FILE=""
echo "2 empty, with :-: [${LOG_FILE:-access.log}]"
echo "3 empty, with -:  [${LOG_FILE-access.log}]"
```

```
1 undefined, with :-: [access.log]
2 empty, with :-: [access.log]
3 empty, with -:  []
```

The distinction matters: an option being deliberately left empty and not
being given at all are different situations. `:-` treats them the same, `-`
separates them.

The `:?` form is the shortest way to declare required variables:

```sh
unset REQUIRED
echo "[${REQUIRED:?must be set}]"
```

```
bash: REQUIRED: must be set
```

The script stops at this point with a nonzero code. In the Error Handling
lesson, this expansion will be used together with strict mode.

## Environment Variables and Scope

Shell variables belong, by default, only to that shell. `export` puts the
variable into the **environment**; subprocesses started after that point
can see it.

```sh
export LANG=en
bash -c 'echo "in subprocess LANG=$LANG"'
LOCAL=x
bash -c 'echo "in subprocess LOCAL=[${LOCAL-none}]"'
```

```
in subprocess LANG=en
in subprocess LOCAL=[none]
```

Propagation is one-way: even if the subprocess changes the environment, the
parent process is unaffected by it. The rule from the Pipeline lesson holds
here too.

`readonly` makes a variable unchangeable:

```sh
readonly CONST=5
CONST=6
```

```
bash: CONST: readonly variable
```

A naming convention is widespread and worth following: **names that enter
the environment and are shared with the outside world are written
uppercase, names specific to the script are written lowercase.** Using
lowercase also prevents accidentally overwriting shell variables like
`PATH`, `HOME`, `IFS`.

## Applying It to the Script

The `report.sh` script gets rid of the hardcoded log name:

```sh
#!/usr/bin/env bash
# report.sh — produces a summary report from the access log.

log_file="${LOG_FILE:-access.log}"

echo "== Log: $log_file =="
echo "== Most requested paths =="
cut -d' ' -f7 "$log_file" | sort | uniq -c | sort -rn | head -5
```

```
== Log: access.log ==
== Most requested paths ==
   6 /api/data
   4 /missing.html
   4 /index.html
   3 /static/style.css
   3 /product/45
```

Three decisions were made together: the name was pulled into a variable,
the `LOG_FILE` value coming from the environment was given priority, and the
`"$log_file"` expansion was quoted. Without the third, the script would silently
try to read the wrong file when given a path containing a space.

## Summary

- Assignment leaves no space on either side of the equals sign; the shell
  cannot parse it any other way, because it splits the line into words.
- Word splitting and pathname expansion apply to the result of an unquoted
  expansion; the result of pathname expansion is not split again.
- Variable expansions are always enclosed in double quotes; leaving one
  unquoted is a deliberate decision.
- Double quotes expand and block splitting, single quotes expand nothing, a
  backslash strips one character.
- The difference between `-` and `:-` is whether an empty value counts as
  missing; `:?` declares a required variable.
- `export` propagates a variable to subprocesses; propagation is one-way.

## Next Step

The log's name can now come from an environment variable, but this is not
the natural path for whoever calls the script: writing
`./report.sh access.log` is what is expected. The next lesson takes up
passing values to a script from the command line — positional parameters,
quoting the argument list, and the difference between `"$@"` and `"$*"`
that shows up with arguments containing spaces.
