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
$VARif it is set and non-empty; otherwise usedefault.
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
.envfile, 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.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.