Bash: Generating configs with envsubst, here document, and here string
Greetings!

In this note, we will briefly look at a few simple ways to build a config from a Bash script using here document, here string, and envsubst.

I probably would not call this full-fledged templating. It is more like regular text file generation with variable substitution. But in practice, this is usually enough: create a config on the first container startup, substitute a domain, port, path to the working directory, or SMTP parameters.

The examples below are close to what I used in openconnect-middle-server: there is a container image, there is an .env, and there is a set of default files from which the working configuration is assembled at startup.

Here document

here document allows you to pass multiline text to a command’s standard input. In Bash, this is a construct like << EOF.

For example, you can create a small config right away:

BASH
cat << EOF > app.conf
server_name = test.r4ven.me
server_port = 443
work_dir = /var/lib/example
EOF
Click to expand and view more

The cat command receives the text up to the EOF marker and writes it to app.conf.

If there are variables inside the block, Bash will substitute their values:

BASH
SERVER_NAME="test.r4ven.me"
SERVER_PORT="443"
WORK_DIR="/var/lib/example"

cat << EOF > app.conf
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOF
Click to expand and view more

The output will be a regular file:

If variable substitution is not needed, the marker can be quoted:

BASH
cat << 'EOF' > app.conf.tmpl
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOF
Click to expand and view more

In this case, Bash will not touch ${SERVER_NAME} and will leave the text as is. This option is convenient for creating a template file.

Here string

here string is a similar construct, but for a single line. It looks like this:

BASH
grep "443" <<< "server_port = 443"
Click to expand and view more

That is, the string to the right of <<< is passed to the command’s standard input.

In scripts, this can be convenient when you do not want to build echo ... | command, especially if reading through read follows next:

BASH
line="test.r4ven.me:443"

IFS=":" read -r host port <<< "$line"

echo "$host"
echo "$port"
Click to expand and view more

Output:

For generating large configs, here string is usually not needed. But for short transformations, parsing a string, or passing one value to a command, it is a perfectly normal tool.

envsubst

envsubst does one simple thing: it reads text from standard input, looks for variables like $VAR or ${VAR}, and outputs or redirects the text with substituted values.

In Debian/Ubuntu, the utility is usually installed with the gettext-base package:

BASH
sudo apt install gettext-base
Click to expand and view more

Check that it exists:

BASH
command -v envsubst
Click to expand and view more

Example with the same config:

BASH
cat << 'EOF' > app.conf.tmpl
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOF
Click to expand and view more

Set environment variables:

BASH
export SERVER_NAME="test.r4ven.me"
export SERVER_PORT="443"
export WORK_DIR="/var/lib/example"
Click to expand and view more

Generate the final file:

BASH
envsubst < ./app.conf.tmpl > ./app.conf
Click to expand and view more

Unlike a regular here document with variable substitution, here the template can be stored as a separate file in the repository. This is more convenient if the config is large or needs to be edited separately from the script.

Example from openconnect-middle-server

In the previous article, we talked about openconnect-middle-server. So, there is a certain function there that creates a working file from a template, but only if the target file does not already exist:

BASH
render_template() {
    local src="$1"
    local dst="$2"

    [[ -f "$src" ]] || die "Template not found: $src"

    if [[ ! -f "$dst" ]]; then
        envsubst < "$src" > "$dst"
        echo "Generated: $dst"
    fi
}
Click to expand and view more

Then it is called from entrypoint.sh:

BASH
render_template "/templates/ocserv.conf" "/app/ocserv.conf"
render_template "/templates/ca.tmpl" "/app/ca.tmpl"
render_template "/templates/server.tmpl" "/app/server.tmpl"
render_template "/templates/msmtprc" "/app/msmtprc"
Click to expand and view more

For example, the server.tmpl file for certtool contains variables:

BASH
cn = $OC_SRV_CA
dns_name = $OC_SRV_CN
organization = $OC_SRV_CN
expiration_days = -1
signing_key
encryption_key #only if the generated key is an RSA one
tls_www_server
Click to expand and view more

And the values are taken from the container environment. So the same image can be started with different domains and parameters without rebuilding it, just by editing the .env file.

With msmtprc, the story is similar:

BASH
host $OC_OTP_MSMTP_HOST
port $OC_OTP_MSMTP_PORT
auth on
user $OC_OTP_MSMTP_USER
password $OC_OTP_MSMTP_PASSWORD
from $OC_OTP_MSMTP_FROM
Click to expand and view more

chmod 400 /app/msmtprc

PLAINTEXT
Click to expand and view more

Limiting the list of variables

By default, envsubst replaces all variables it sees in the input stream. Sometimes this is not needed.

For example, if the config contains $PATH, $remote_addr, or variables of another application, they can be accidentally replaced with the current environment or an empty string.

To avoid this, you can explicitly specify the list of variables:

BASH
envsubst '${SERVER_NAME} ${SERVER_PORT}' < app.conf.tmpl > app.conf
Click to expand and view more

In this case, envsubst will replace only SERVER_NAME and SERVER_PORT. It will leave the rest unchanged.

You need to understand that envsubst is not Bash and not Jinja. It does not execute conditions, loops, or substitutions with a default value. Unfortunately 😒.

For example, this line will not work as expected:

BASH
server_name = ${SERVER_NAME:-localhost}
server_port = $(grep 'port=' ./some_app.conf | cut -d'=' -f2)
Click to expand and view more

The default value needs to be prepared beforehand:

BASH
export SERVER_NAME="${SERVER_NAME:-localhost}"
export SERVER_PORT="${SERVER_PORT:-443}"

envsubst < app.conf.tmpl > app.conf
Click to expand and view more

And one more point: envsubst will silently replace an unknown variable with an empty string. So before generating a normal config, it is better to check required variables separately.

For example, like this:

BASH
: "${SERVER_NAME:?SERVER_NAME is required}"
: "${SERVER_PORT:?SERVER_PORT is required}"
Click to expand and view more

Afterword

Here document and Here string are tools I use regularly. They are very useful tools.

And I learned about the envsubst utility quite recently, when I was rebuilding my OpenConnect image for setting up a Middle server. It makes working with separate template files convenient, which was one of the goals of the rebuild.

In the Linux world, this happens all the time. You seem to have been working with the system for many years, and then tools or features like these, which you did not know about, but which were always right under your nose, pop up here and there. And I really like that.

Thanks for reading. Good luck learning Bash! 🐧

Copyright Notice

Author: Иван Чёрный

Link: https://r4ven.me/en/automation/bash-generaciya-konfigov-cherez-envsubst-here-document-i-here-string/

License: CC BY-NC-SA 4.0

Использование материалов блога разрешается при условии: указания авторства/источника, некоммерческого использования и сохранения лицензии.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut