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:
| Form | Result when var is set and non-empty | Result when unset or empty |
|---|---|---|
${var:-word} | value of var | word (var unchanged) |
${var:=word} | value of var | word, and var is set to word |
${var:+word} | word | nothing |
${var:?msg} | value of var | error: print msg, a script exits |
${#var} | length of the value | 0 |
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:
- The result is subject to word splitting and globbing if unquoted. Write
"${dir:-/tmp}", not${dir:-/tmp}. - The
:=assignment happens in the current shell. Inside$( )it would happen in the subshell and be lost.
Common mistakes
- Forgetting the braces.
$var:-defaultexpands$varand then prints:-defaultliterally. - Spaces inside.
${var :- x}is a syntax error (bad substitution). Write${var:-x}. - Using
:=on positional parameters.${1:=x}gives$1: cannot assign in this way. Usename=${1:-x}instead. - Expecting
${var:?}to keep a script running. It is meant to stop it. - Unquoted results. A default containing spaces is split like any other value.
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
${var:-word}default;${var:=word}default and assign;${var:+word}alternate;${var:?msg}stop with an error;${#var}length.- With the colon: "unset or empty"; without: "unset" only. Prefer the colon forms.
: "${VAR:=value}"and: "${VAR:?message}"are common idioms for settings and checks.- All of this happens inside bash, during expansion. Quote the result.
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}".