skip to content

In bash, what do the parameter expansions ${VAR:-default}, ${VAR-default}, ${VAR:=default} and ${VAR:?message} each do, and what difference does the colon make?

level: middleimportance: must knowfreq 75%

answer

  1. four operators, one optional colon
  2. colon decides whether empty counts
  3. one of them writes back
  4. one of them aborts the script
  5. the plus form is the mirror image

basics

~20 s

${VAR:-default} substitutes default when VAR is unset or empty; ${VAR-default} substitutes only when VAR is unset. ${VAR:=default} substitutes and also assigns it back to VAR. ${VAR:?message} prints message to stderr and aborts the script instead of substituting.

solid answer

~40 s

All four are default-value operators, and the colon decides whether an *empty* value counts as missing. `${VAR:-default}` yields `default` when VAR is unset **or** set to the empty string; `${VAR-default}` yields it only when VAR is genuinely unset, so an explicit `VAR=` wins. `${VAR:=default}` behaves like `:-` but also assigns the value back into VAR, so later references see it too — it does not work on positional parameters. `${VAR:?message}` is the guard clause: if VAR is unset or null, bash writes `VAR: message` to stderr and a non-interactive shell exits non-zero, which is how you fail fast on missing configuration. `${VAR:+alt}` is the mirror image: it yields `alt` only when VAR is set and non-empty, which is handy for conditionally adding a flag. Quote the whole expansion.

code

bash · 9 lines
bash
unset A
B=
C=value

printf '%s|%s|%s\n' "${A-fallback}" "${B-fallback}" "${C-fallback}"
printf '%s|%s|%s\n' "${A:-fallback}" "${B:-fallback}" "${C:-fallback}"

: "${LOG_LEVEL:=info}"
echo "LOG_LEVEL is now $LOG_LEVEL"

go deeper

for a junior

Know that ${VAR:-default} supplies a fallback when a variable is missing, and be able to write it correctly inside double quotes.

for a middle

Explain the colon rule precisely — unset versus unset-or-empty — and say which of the four operators assigns back to the variable and which one aborts the script.

for a senior

Show where each belongs in a real entrypoint: :? guards for required configuration at the top of the script, := for defaults that later code must see, and :- for genuinely optional values under set -u.

for a principal

Be ready to argue for a convention: whether missing configuration should fail loudly at startup or silently default, and why baking defaults into the script with := can hide a broken deployment pipeline that never set the value at all.

## The family Bash has four closely related expansions that all answer the question "what if this variable has no useful value?" They are pure shell syntax — no subprocess, no `test` call — and they appear constantly in entrypoint scripts, CI wrappers and installers. | Expansion | If VAR has a value | If VAR is missing | |---|---|---| | `${VAR:-default}` | the value | `default` (VAR unchanged) | | `${VAR:=default}` | the value | `default`, **and VAR is assigned it** | | `${VAR:?message}` | the value | error on stderr, shell exits | | `${VAR:+alt}` | `alt` | empty string | ## What the colon changes Each operator exists in two forms: with a colon and without. Without the colon, only an *unset* variable is treated as missing. With the colon, both unset **and** null (empty string) are treated as missing. ```bash unset A B= C=value echo "[${A-fallback}] [${B-fallback}] [${C-fallback}]" # [fallback] [] [value] echo "[${A:-fallback}] [${B:-fallback}] [${C:-fallback}]" # [fallback] [fallback] [value] ``` That distinction is the whole point of the question. `B=` is a deliberate empty value. If your configuration treats "exported but empty" as "the operator turned this off", you want the colonless form. If empty means "nobody filled this in", which is far more common when values come from CI variables or a `.env` file, you want the colon form. In practice `:-` is right almost every time, and code that uses the colonless `-` should look deliberate. ## Assignment: `:=` `${VAR:=default}` substitutes *and* stores, so it is the idiom for "apply a default once, at the top of the script": ```bash : "${LOG_LEVEL:=info}" # LOG_LEVEL is now guaranteed non-empty echo "level=$LOG_LEVEL" ``` The leading `:` is the no-op builtin — it evaluates its arguments and does nothing else, which is how you use an expansion purely for its side effect. Note two limits: the assignment is to a shell variable, so it is not exported to children unless VAR was already exported or you `export` it; and `${1:=x}` on a positional parameter is an error (`$1: cannot assign in this way`) — use `${1:-x}` there. ## The guard: `:?` `${VAR:?message}` is the cheapest possible required-argument check: ```bash : "${DATABASE_URL:?must be set in the deploy environment}" ``` If `DATABASE_URL` is unset or empty, bash prints `DATABASE_URL: must be set in the deploy environment` to stderr and the script exits with a non-zero status. Two details matter. First, it exits a **non-interactive** shell; typed at an interactive prompt it just reports the error and returns you to the prompt. Second, it fires at the moment the expansion is evaluated, so putting these lines at the top of the script gives you a clean, immediate failure instead of a confusing error five hundred lines later. Omitting the message (`${VAR:?}`) produces bash's own "parameter null or not set" text. ## The mirror: `:+` `${VAR:+alt}` inverts the test: it produces `alt` when VAR is set and non-empty, otherwise nothing. It is how you add an optional flag without an `if`: ```bash curl ${AUTH_TOKEN:+-H "Authorization: Bearer $AUTH_TOKEN"} "$URL" ``` (That example deliberately leaves the expansion unquoted so it splits into separate arguments; building the argument list in an array is the safer general habit.) ## Practical notes - **Quote the expansion.** `"${VAR:-$DEFAULT}"` — the substituted word is itself subject to expansion, and without quotes the result is split on whitespace and globbed like any other unquoted expansion. - The word after the operator can be any expansion, including a command substitution: `"${PORT:-$(default_port)}"`. It is only evaluated when the default is actually needed. - Under a script that enables `set -u`, `"${OPTIONAL:-}"` is the standard way to read a variable that may legitimately be absent without tripping the unbound-variable error. - These are POSIX-standard operators (the `:-`, `:=`, `:?`, `:+` set), so they work in `/bin/sh` as well as bash.

  • What does ${VAR:+--verbose} give you that ${VAR:-} does not?
    It inverts the test. `${VAR:+--verbose}` expands to `--verbose` only when VAR is set and non-empty, and to nothing otherwise, so you can append an optional flag or suffix without writing an `if`. `${VAR:-}` does the opposite: it yields VAR's value, falling back to nothing when it is missing.
  • Why does ${1:=default} fail, and what do you use instead?
    Bash refuses to assign to positional parameters, so `${1:=default}` errors with `$1: cannot assign in this way`. Use the non-assigning form, `arg="${1:-default}"`, or `set --` to rebuild the positional list. The same restriction applies to other special parameters.
  • Does ${VAR:?msg} exit an interactive shell too?
    No. It writes `VAR: msg` to stderr in both cases, but only a non-interactive shell — a script — exits. At an interactive prompt bash reports the error, returns a non-zero status, and leaves your session alive, which is why testing the idiom by pasting it into a terminal can look like it did nothing.

saying these in an interview costs you the question

  • Says ${VAR:-x} assigns x to VAR
  • Thinks an empty VAR makes ${VAR-x} produce x
  • Believes ${VAR:?msg} only warns and the script continues
  • Uses ${VAR:-} expecting it to fail on missing config
  • Confuses ${VAR:+x} with ${VAR:-x}
  • Leaves the expansion unquoted so the default gets word-split

context