416 words, 3 min read

When writing shell scripts, hardcoded values are a common source of friction. A script that works perfectly on your machine may need adjustments on a colleague's setup, in CI, or in production — and forcing people to edit the script itself is messy.

There's a simple Bash idiom that solves this cleanly.

The Problem

Consider a typical startup script:

PORT=8888
HOST=0.0.0.0
uvicorn myapp:app --host $HOST --port $PORT

The port is hardcoded. If you need to run two instances, or if 8888 is already taken, you have to edit the file.

The Fix: ${VAR:-default}

Replace the hardcoded assignment with a default-value expansion:

PORT=${PORT:-8888}
HOST=${HOST:-0.0.0.0}
uvicorn myapp:app --host $HOST --port $PORT

The syntax ${VAR:-default} means:

Use the value of $VAR if it is set and non-empty; otherwise use default.

The script's behaviour is unchanged when no environment variable is provided — the default kicks in. But now callers can override it without touching the file:

PORT=9000 ./bin/start.sh

Or by exporting it beforehand:

export PORT=9000
./bin/start.sh

Why This Matters

  • No edits required. The script stays clean; overrides happen at call time.
  • CI-friendly. Most CI systems let you set environment variables per job. Your script picks them up automatically.
  • Self-documenting. The default value lives right next to the variable name, so readers immediately know what to expect.
  • Composable. You can layer overrides: a .env file, a CI variable, or a one-liner prefix — whatever fits the context.

Variants Worth Knowing

Syntax Meaning
${VAR:-default} Use default if VAR is unset or empty
${VAR-default} Use default only if VAR is unset (empty string is kept)
${VAR:=default} Use default and assign it back to VAR if unset or empty
${VAR:?message} Abort with message if VAR is unset or empty

For most cases, ${VAR:-default} is the right choice. The := variant is useful when you want the variable available for the rest of the script without repeating the default.

A Real-World Example

Here's a before/after for a uvicorn startup script:

Before:

HOST=0.0.0.0
PORT=8888
uvicorn docai.api.app:app --host $HOST --port $PORT

After:

HOST=${HOST:-0.0.0.0}
PORT=${PORT:-8888}
uvicorn docai.api.app:app --host $HOST --port $PORT

Two characters added per line (${ and :-), zero behaviour change by default, and now fully overridable from the outside. A small habit with outsized payoff.