Bash: Генерация конфигов через envsubst, here document и here string
Приветствую!

В этой заметке коротко разберем несколько простых способов собрать конфиг из Bash-скрипта с помощью here document, here string и envsubst.

Называть это полноценной шаблонизацией, наверное, не буду. Скорее это обычная генерация текстовых файлов с подстановкой переменных. Но на практике обычно подобного и хватает: создать конфиг при первом запуске контейнера, подставить домен, порт, путь до рабочей директории или параметры SMTP.

Примеры ниже близки к тому, что я использовал в openconnect-middle-server: есть образ контейнера, есть .env, есть набор дефолтных файлов, из которых при старте собирается рабочая конфигурация.

Here document

here document позволяет передать многострочный текст в стандартный ввод команды. В Bash это конструкция вида << EOF.

Например, можно сразу создать небольшой конфиг:

BASH
cat << EOF > app.conf
server_name = test.r4ven.me
server_port = 443
work_dir = /var/lib/example
EOF
Нажмите, чтобы развернуть и увидеть больше

Команда cat получает текст до маркера EOF и записывает его в app.conf.

Если внутри блока есть переменные, Bash подставит их значения:

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
Нажмите, чтобы развернуть и увидеть больше

На выходе будет обычный файл:

Если подстановка переменных не нужна, маркер можно взять в кавычки:

BASH
cat << 'EOF' > app.conf.tmpl
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOF
Нажмите, чтобы развернуть и увидеть больше

В этом случае Bash не тронет ${SERVER_NAME} и оставит текст как есть. Такой вариант удобно использовать для создания файла-шаблона.

Here string

here string - похожая конструкция, но для одной строки. Выглядит так:

BASH
grep "443" <<< "server_port = 443"
Нажмите, чтобы развернуть и увидеть больше

То есть строка справа от <<< передается команде в стандартный ввод.

В скриптах это бывает удобно, когда не хочется городить echo ... | command, особенно если дальше идет чтение через read:

BASH
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:

BASH
sudo apt install gettext-base
Нажмите, чтобы развернуть и увидеть больше

Проверить наличие:

BASH
command -v envsubst
Нажмите, чтобы развернуть и увидеть больше

Пример с тем же конфигом:

BASH
cat << 'EOF' > app.conf.tmpl
server_name = ${SERVER_NAME}
server_port = ${SERVER_PORT}
work_dir = ${WORK_DIR}
EOF
Нажмите, чтобы развернуть и увидеть больше

Задаем переменные окружения:

BASH
export SERVER_NAME="test.r4ven.me"
export SERVER_PORT="443"
export WORK_DIR="/var/lib/example"
Нажмите, чтобы развернуть и увидеть больше

Генерируем итоговый файл:

BASH
envsubst < ./app.conf.tmpl > ./app.conf
Нажмите, чтобы развернуть и увидеть больше

В отличие от обычного here document с подстановкой переменных, тут шаблон можно хранить отдельным файлом в репозитории. Это удобнее, если конфиг большой или его нужно редактировать отдельно от скрипта.

Пример из openconnect-middle-server

В прошлой статье мы говорили про openconnect-middle-server. Так вот, там есть некая функция, которая создает рабочий файл из шаблона, но только если целевой файл еще не существует:

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
}
Нажмите, чтобы развернуть и увидеть больше

Дальше она вызывается из 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"
Нажмите, чтобы развернуть и увидеть больше

Например, файл server.tmpl для certtool содержит переменные:

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
Нажмите, чтобы развернуть и увидеть больше

А значения берутся из окружения контейнера. Поэтому один и тот же образ можно запустить с разными доменами и параметрами без пересборки, просто отредактировав файл .env.

С msmtprc похожая история:

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
Нажмите, чтобы развернуть и увидеть больше

Ограничение списка переменных

По умолчанию envsubst заменяет все переменные, которые видит во входном потоке. Иногда это не нужно.

Например, если в конфиге есть $PATH, $remote_addr или переменные другого приложения, их можно случайно заменить текущим окружением или пустой строкой.

Чтобы этого не произошло, можно явно указать список переменных:

BASH
envsubst '${SERVER_NAME} ${SERVER_PORT}' < app.conf.tmpl > app.conf
Нажмите, чтобы развернуть и увидеть больше

В этом случае envsubst заменит только SERVER_NAME и SERVER_PORT. Остальное оставит без изменений.

Нужно понимать, что envsubst не Bash и не Jinja. Он не выполняет условия, циклы и подстановки со значением по умолчанию. К сожалению 😒.

Например, такая строка не отработает как ожидается:

BASH
server_name = ${SERVER_NAME:-localhost}
server_port = $(grep 'port=' ./some_app.conf | cut -d'=' -f2)
Нажмите, чтобы развернуть и увидеть больше

Значение по умолчанию нужно подготовить заранее:

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

envsubst < app.conf.tmpl > app.conf
Нажмите, чтобы развернуть и увидеть больше

И еще один момент: envsubst молча заменит неизвестную переменную на пустую строку. Поэтому перед генерацией нормального конфига лучше проверять обязательные переменные отдельно.

Например так:

BASH
: "${SERVER_NAME:?SERVER_NAME is required}"
: "${SERVER_PORT:?SERVER_PORT is required}"
Нажмите, чтобы развернуть и увидеть больше

Послесловие

Here document - и Here string я использую регулярно. Это очень полезные инструменты.

А про утилиту envsubst я узнал совсем недавно, когда пересобирал свой образ OpenConnect для настройки Middle сервера. С её помощью удобно работать с отдельными файлами-шаблонами, что и было одной из целей пересборки.

В мире Linux так постоянно. Вроде работаешь с системой много лет, а подобные инструменты или особенности, о которых ты не знал, но они всегда под носом, всплывают, то тут, то там. И мне это очень нравится.

Спасибо, что читаете. Успехов в изучении Bash! 🐧

Авторские права

Автор: Иван Чёрный

Ссылка: https://r4ven.me/automation/bash-generaciya-konfigov-cherez-envsubst-here-document-i-here-string/

Лицензия: CC BY-NC-SA 4.0

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

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша