Variables, Quoting and Expansion · about 40 min

Parameter expansion: defaults, lengths and errors

Why this matters

Real scripts deal with missing information all the time: a setting that may or may not be in the environment, an optional argument, a value that must never be empty because the next line deletes files with it. bash's parameter expansion has compact forms for exactly these cases, so you can write "use this, or a default", "complain and stop if this is missing", or "how long is this?" without any if statements. You will see them in nearly every well-written script.

Plain parameter expansion, revisited

So far you have used $name and ${name}. The braces form is the general one: inside the braces, after the name, you can add an operator that changes the result. The ones in this lesson are:

FormResult when var is set and non-emptyResult when unset or empty
${var:-word}value of varword (var unchanged)
${var:=word}value of varword, and var is set to word
${var:+word}wordnothing
${var:?msg}value of varerror: print msg, a script exits
${#var}length of the value0

The word part is itself expanded, so it can contain variables or command substitutions: ${EDITOR:-${VISUAL:-nano}}, ${logdir:-$HOME/logs}.

Defaults: ${var:-word}

$ echo "Editor: ${EDITOR:-nano}"
Editor: nano
$ EDITOR=vim
$ echo "Editor: ${EDITOR:-nano}"
Editor: vim

This is the standard way to let users override a setting through the environment:

backup_dir=${BACKUP_DIR:-/var/backups}
keep=${KEEP:-7}

Run as is, the script uses /var/backups; run as BACKUP_DIR=/mnt/usb ./backup.sh, it uses the USB disk. (Recall from lesson 1 that a prefix assignment puts the variable in that one command's environment.)

Assign a default: ${var:=word}

:= substitutes the default and stores it, so later uses see it too:

$ unset count
$ echo "${count:=0}"
0
$ echo "$count"
0

It cannot be used on special parameters like $1. A common idiom combines it with the : builtin. : is a command that does nothing and succeeds; its arguments are still expanded, so the expansion's side effect happens:

: "${LOG_LEVEL:=info}"     # set LOG_LEVEL to info unless it is already set

Alternate value: ${var:+word}

:+ is the opposite: it gives word only if the variable has a value. It is useful for optional pieces of a command line:

$ verbose=yes
$ echo "tar -c${verbose:+v}f backup.tar"
tar -cvf backup.tar
$ verbose=
$ echo "tar -c${verbose:+v}f backup.tar"
tar -cf backup.tar

Required values: ${var:?message}

:? stops with an error when the variable is unset or empty:

$ unset DEST
$ echo "${DEST:?DEST is not set}"
bash: DEST: DEST is not set

In an interactive shell, that just aborts the command. In a script, the whole script exits immediately, with status 1, and the message goes to standard error:

#!/bin/bash
: "${DEST:?DEST is not set}"
rm -rf "$DEST"/old-backups

If DEST is missing, the script dies on line 2 instead of running rm -rf /old-backups. Put these checks at the top of any script that depends on settings. Without a message, ${DEST:?} prints a standard one: DEST: parameter null or not set.

The colon: unset versus empty

Each operator also exists without the colon: ${var-word}, ${var=word}, ${var+word}, ${var?msg}. Without the colon, the test is only "is the variable unset?"; a variable set to the empty string counts as having a value.

$ empty=''
$ echo "[${empty-default}] [${empty:-default}]"
[] [default]
$ unset gone
$ echo "[${gone-default}] [${gone:-default}]"
[default] [default]

The colon forms are what you want in nearly all scripts. The non-colon forms are for the rare case where an empty value is meaningful ("the user explicitly set this to nothing").

Length: ${#var}

$ path=/usr/local/bin
$ echo ${#path}
14
$ word='héllo'
$ echo ${#word}
5

The length is counted in characters according to the current locale. In this sandbox the locale is UTF-8, so é is one character even though it takes two bytes. In the C locale it would count bytes and say 6.

A preview of what else lives in the braces

Parameter expansion can do much more: remove prefixes and suffixes (${file%.txt}), replace text (${s/old/new}), take substrings (${s:0:3}), change case (${s^^}). Those are string manipulation, covered in module 15. They are worth knowing exist: they run inside bash with no extra process, unlike calling sed or cut through command substitution.

What happens underneath

All of this is done by bash itself during step 4 of the expansion order, before word splitting. No process is started. That has two consequences you already know how to reason about:

Common mistakes

Exercises

The code exercises are tested with different environment settings (NAME=Ada bash script.sh and so on), so try those in the shell first: copy your script into /work and run it with and without a prefix assignment.

Recap

Going further: in man bash, search /Parameter Expansion and skim the list: you now know the first five forms. Try echo "${HOSTNAME:+on host $HOSTNAME}" and echo "${#PATH}".

Exercises

Exercise 1 · quiz

Set, but empty

name is set to the empty string: name=''. What do these two lines print?

echo "1:${name-anon}"
echo "2:${name:-anon}"
Answer
  • 1: and 2:anon

Without the colon, ${name-anon} uses the default only when name is unset. name is set (to nothing), so the result is empty. With the colon, ${name:-anon} uses the default when the variable is unset or empty. In practice you almost always want the colon form, because an empty value is rarely a useful value.

Exercise 2 · write code

Greeting with a default

Write a script that greets the person named in the environment variable NAME, falling back to world when NAME is unset or empty. It prints exactly one line:

Hello, world!

when run plainly, and Hello, Ada! when run as NAME=Ada bash script.sh. The checker tries both, and also NAME= bash script.sh (set but empty), which must print Hello, world!.

Use a single parameter expansion; no if is needed.

File: script.sh

Starting point

echo "Hello, $NAME!"

Example

expected output

Hello, world!
Hint 1

One of the four ${var:...} forms substitutes a default value.

Hint 2

echo "Hello, ${NAME:-world}!"

Reference solution
echo "Hello, ${NAME:-world}!"

Exercise 3 · write code

Refuse to run without a setting

Write a script that needs the environment variable DEST.

copying to /srv/backup
(11 characters)

File: script.sh

Starting point

echo "copying to $DEST"

Example

expected output

(nothing)
Hint 1

${DEST:?message} prints the message and makes a non-interactive shell exit when DEST is unset or empty. The : builtin does nothing, so : "${DEST:?...}" is a check on its own line.

Hint 2

${#DEST} is the length of the value.

Reference solution
: "${DEST:?DEST is not set}"
echo "copying to $DEST"
echo "(${#DEST} characters)"