Skip to content
academia.sh

Lesson 07 / 24

Variables and Quoting Rules

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

Contents

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.

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:

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:

show_args() { printf 'arguments: %d\n' "$#"; printf '  <%s>\n' "$@"; }
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:

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:

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.

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:

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:

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
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:

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.

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:

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:

#!/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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close