В этой заметке коротко разберем несколько простых способов собрать конфиг из Bash-скрипта с помощью here document, here string и envsubst.
Называть это полноценной шаблонизацией, наверное, не буду. Скорее это обычная генерация текстовых файлов с подстановкой переменных. Но на практике обычно подобного и хватает: создать конфиг при первом запуске контейнера, подставить домен, порт, путь до рабочей директории или параметры SMTP.
🖐️Эй!
Подписывайтесь на наш телеграм @r4ven_me📱, чтобы не пропустить новые публикации на сайте😉. А если есть вопросы или желание пообщаться по тематике — заглядывайте в Вороний чат @r4ven_me_chat🧐.
Примеры ниже близки к тому, что я использовал в openconnect-middle-server: есть образ контейнера, есть .env, есть набор дефолтных файлов, из которых при старте собирается рабочая конфигурация.
Here document
here document позволяет передать многострочный текст в стандартный ввод команды. В Bash это конструкция вида << EOF.
Например, можно сразу создать небольшой конфиг:
cat << EOF > app.conf
server_name = test.r4ven.me
server_port = 443
work_dir = /var/lib/example
EOF
Команда cat получает текст до маркера EOF и записывает его в app.conf.
Если внутри блока есть переменные, 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На выходе будет обычный файл:

Если подстановка переменных не нужна, маркер можно взять в кавычки:
cat << 'EOF' > app.conf.tmpl
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOF
В этом случае Bash не тронет ${SERVER_NAME} и оставит текст как есть. Такой вариант удобно использовать для создания файла-шаблона.
📝 Примечание
Маркер EOF не специальный. Можно написать CONFIG, TEMPLATE, END или что угодно еще. Главное, чтобы открывающий и закрывающий маркеры совпадали.
Here string
here string - похожая конструкция, но для одной строки. Выглядит так:
grep "443" <<< "server_port = 443"То есть строка справа от <<< передается команде в стандартный ввод.
В скриптах это бывает удобно, когда не хочется городить echo ... | command, особенно если дальше идет чтение через read:
line="test.r4ven.me:443"
IFS=":" read -r host port <<< "$line"
echo "$host"
echo "$port"Вывод:

Для генерации больших конфигов here string обычно не нужен. А вот для коротких преобразований, разбора строки или передачи одного значения в команду - вполне нормальный инструмент.
envsubst
envsubst делает одну простую вещь: читает текст из стандартного ввода, ищет переменные вида $VAR или ${VAR} и выводит или перенаправляет текст с подставленными значениями.
В Debian/Ubuntu утилита обычно ставится пакетом gettext-base:
sudo apt install gettext-baseПроверить наличие:
command -v envsubst
Пример с тем же конфигом:
cat << 'EOF' > app.conf.tmpl
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOFЗадаем переменные окружения:
export SERVER_NAME="test.r4ven.me"
export SERVER_PORT="443"
export WORK_DIR="/var/lib/example"Генерируем итоговый файл:
envsubst < ./app.conf.tmpl > ./app.conf
В отличие от обычного here document с подстановкой переменных, тут шаблон можно хранить отдельным файлом в репозитории. Это удобнее, если конфиг большой или его нужно редактировать отдельно от скрипта.
Пример из openconnect-middle-server
В прошлой статье мы говорили про openconnect-middle-server. Так вот, там есть некая функция, которая создает рабочий файл из шаблона, но только если целевой файл еще не существует:
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
}Дальше она вызывается из entrypoint.sh:
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"Например, файл server.tmpl для certtool содержит переменные:
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А значения берутся из окружения контейнера. Поэтому один и тот же образ можно запустить с разными доменами и параметрами без пересборки, просто отредактировав файл .env.
С msmtprc похожая история:
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☝️ После генерации такого файла важно не забыть про права:
chmod 400 /app/msmtprcОграничение списка переменных
По умолчанию envsubst заменяет все переменные, которые видит во входном потоке. Иногда это не нужно.
Например, если в конфиге есть $PATH, $remote_addr или переменные другого приложения, их можно случайно заменить текущим окружением или пустой строкой.
Чтобы этого не произошло, можно явно указать список переменных:
envsubst '${SERVER_NAME} ${SERVER_PORT}' < app.conf.tmpl > app.confВ этом случае envsubst заменит только SERVER_NAME и SERVER_PORT. Остальное оставит без изменений.
Нужно понимать, что envsubst не Bash и не Jinja. Он не выполняет условия, циклы и подстановки со значением по умолчанию. К сожалению 😒.
Например, такая строка не отработает как ожидается:
server_name = ${SERVER_NAME:-localhost}
server_port = $(grep 'port=' ./some_app.conf | cut -d'=' -f2)Значение по умолчанию нужно подготовить заранее:
export SERVER_NAME="${SERVER_NAME:-localhost}"
export SERVER_PORT="${SERVER_PORT:-443}"
envsubst < app.conf.tmpl > app.confИ еще один момент: envsubst молча заменит неизвестную переменную на пустую строку. Поэтому перед генерацией нормального конфига лучше проверять обязательные переменные отдельно.
Например так:
: "${SERVER_NAME:?SERVER_NAME is required}"
: "${SERVER_PORT:?SERVER_PORT is required}"📝 Это shell‑подстановка с проверкой: если переменная SERVER_NAME не установлена или пуста, оболочка выведет сообщение “SERVER_NAME is required” и завершит выполнение с ненулевым кодом.
Послесловие
Here document - и Here string я использую регулярно. Это очень полезные инструменты.
А про утилиту envsubst я узнал совсем недавно, когда пересобирал свой образ OpenConnect для настройки Middle сервера. С её помощью удобно работать с отдельными файлами-шаблонами, что и было одной из целей пересборки.
В мире Linux так постоянно. Вроде работаешь с системой много лет, а подобные инструменты или особенности, о которых ты не знал, но они всегда под носом, всплывают, то тут, то там. И мне это очень нравится.
Спасибо, что читаете. Успехов в изучении Bash! 🐧


