Bash scripts can set defaults for environment variables that are optionally supplied at execution time.
I was first tipped off to this syntax by Temporal's server container entrypoint . Rather than use CLI arguments that would have to be parsed, the script gets its config from environment variables. This is because it's much easier to configure and read environment variables than CLI arguments with Docker.
But there is still a lot of value with setting variable defaults even when not using Docker.
Syntax for env vars
Here is a Bash script with two variables, $GREETING
and $NAME
, that each have a default value:
#!/usr/bin/env bash
set -euo pipefail
: "${GREETING:=Hello,}"
: "${NAME:=user}"
echo "${GREETING} ${NAME}"
You can probably guess what the expected output will be when we run the script:
$ ./greeting.sh
Hello, user
Explanation
Let's break down what those two lines are doing.
: [arguments]
is a "do nothing" Bash builtin command that performs argument expansion and always succeeds. Here are some examples:
$ : echo hello!
(no output because the `echo` command wasn't run)
$ : VAR=foobar
(no output because no command was run)
$ echo $VAR
(empty output because the variable assignment didn't happen)
${parameter:=word}
is a "shell parameter expansion" that will assign a variable a value if that variable is either unset or null. Here are some examples:
$ echo $VAR
(empty output because the variable isn't set)
$ echo ${VAR:=default}
default
$ echo $VAR
default
$ VAR=""
($VAR is now set but null)
$ echo ${VAR:=override}
override
$ echo $VAR
override
Because the :
Bash builtin processes parameter expansions, it can be used to default variables:
$ echo $FOO
(empty output because the variable isn't set)
$ : ${FOO:=default}
(no output)
$ echo $FOO
default
${parameter:-word}
Comparison with ${parameter:-word}
is another shell parameter expansion that could be used to default values. These two commands produce the same result:
: ${FOO:=default}
FOO="${FOO:-default}"
But it's more common to use ${parameter:-word}
without variable assignment like this:
# Merge the base Git branch into the current branch
git merge --no-edit origin/${GIT_TRUNK:-main}
Even though the two expansions can be used to accomplish a lot of the same goals, you probably want to prefer ${parameter:=word}
for a few reasons:
If you reference the same variable multiple times, you only have to default the value once, avoiding error-prone situations such as:
#!/usr/bin/env bash set -euo pipefail docker run --name local-mysql \ --env MYSQL_ROOT_PASSWORD=mysecretpassword \ --env MYSQL_DATABASE=${MYSQL_DATABASE:-main} \ --publish 3306:3306 --detach \ mysql:latest # Helper to make the below commands shorter # @param {string} $1 DB name # @param {string} $2 Query mysql() { command mysql --host=127.0.0.1 --port=3306 \ --user=root --password=mysecretpassword "$1" \ --execute "$2" } mysql ${MYSQL_DATABASE:-main} "CREATE TABLE users (id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL);" mysql ${MYSQL_DATABASE:-main} "INSERT INTO users (name) VALUES ('Meredith'), ('Henry'), ('Lola');" mysql ${MYSQL_DATABASE:-main} "SELECT * FROM users;"
Grouping all variable defaults at the top of a Bash script helps increase its readability.
Examples
Start a Node.js Express server with a local configuration:
#!/usr/bin/env bash
set -euo pipefail
: "${NODE_ENV:=local}"
: "${DEBUG:=*}"
NODE_ENV="${NODE_ENV}" DEBUG="${DEBUG}" node app.js
Start a PostgreSQL server in a Docker container:
#!/usr/bin/env bash
set -euo pipefail
: "${POSTGRES_USER:=postgres}
: "${POSTGRES_PASSWORD:=postgres}
: "${POSTGRES_DB:=$POSTGRES_USER}"
docker run --name local-postgres \
--env POSTGRES_USER="${POSTGRES_USER}" \
--env POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
--env POSTGRES_DB="${POSTGRES_DB}" \
--publish 5432:5432 --detach \
postgres:latest