Синтаксис подстановки env-переменных
Интерполяция env-переменных — единственный нормативный механизм подстановки значений в загружаемый конфиг. Спецификация фиксирует два шаблона, правила экранирования и момент выполнения.
Нормативный источник — config-spec/adr/0001-yaml-configuration.md §2.
Два шаблона
| Шаблон | Поведение |
|---|---|
${VAR} | Значение env-переменной VAR. Если не установлена — ConfigError(reason: env_unresolved, details: "VAR"). |
${VAR:-default} | Значение env-переменной VAR. Если не установлена или пустая — литерал default. |
Рекомендуемый regex имени переменной: [A-Z_][A-Z0-9_]* — только ASCII uppercase буквы, цифры, underscore; начинается с буквы или _. Это POSIX-совместимая форма, переносимая между оболочками и языками. Lowercase имена не гарантируются портируемо — реализация MAY их поддерживать, но portable-конфиг использует только uppercase.
Default value: любая строка до закрывающей } (regex [^}]*).
Полное поведение интерполяции нормативно зафиксировано в ADR-0001 §2; машино-читаемая форма правил появится в _meta/interpolation.yaml spec-репозитория (v1.1 backlog).
Экранирование
# Литеральный $ в значении:
bash_prompt: "$$ " # → "$ "
mongo_uri: "mongodb://user:$$pass@host" # → "mongodb://user:$pass@host"
# Обычный текст без ${...}:
template: "Run with $PATH and $HOME" # остаётся как есть (нет ${...} вокруг)
Правила:
$$→$— одинаковое поведение везде, включая контексты без${…}.- Одиночный
$без{и$$— сохраняется буквально (например, в shell-шаблонах). - Вложенные
${…}внутри default — не интерполируются:${FOO:-${BAR}}→ литерал"${BAR}"еслиFOOне задана. Это осознанное упрощение парсера.
Момент выполнения
source.load()— источник возвращает дерево с буквальными строками, включая${VAR}.- Интерполяция — загрузчик обходит все строковые листья дерева источников, у которых
interpolate = true, и выполняет подстановку. - Deep merge — интерполированные деревья всех источников сливаются по приоритету.
- Type coercion — выполняется на уровне методов доступа (
getInt,getBool,getSection), не во время интерполяции.
Следствие: интерполированное значение — всегда строка. getInt("database.port") приводит её к числу строгим regex (см. ниже), getBool — по списку допустимых форм.
Приведение типов из интерполированных строк
ADR-0001 §4.3 нормативно фиксирует правила для int и bool. Для float / number точный regex не закреплён — реализация использует стандартный парсер языка. Ниже — принятое соглашение в Python/TypeScript/Go реализациях; другие binding'и MAY быть немного менее строгими.
| Целевой тип | Допустимые строковые представления | Что отклоняется |
|---|---|---|
int (нормативно) | ^-?\d+$: "42", "-5", "0" | "42.0", "4_2", "0x10", scientific "1e5" |
float / number (де-факто) | Точка как разделитель: "3.14", "-0.5", "1e5", "1.5e-3" | "3,14", "1 000" |
bool (нормативно) | true / false / yes / no / 1 / 0 (case-insensitive) | "on", "off", "t", "f", "да" |
string | Любая | — |
Отклонение → ConfigError(reason: type_mismatch, path, details: "expected int, got \"42.0\"").
Совет. Если env-переменная хранит список (FEATURES="a,b,c") или JSON (CONFIG='{"x":1}') — в конфиге её читают через getString, а разбор на стороне приложения. Спецификация не делает JSON-парсинг внутри интерполяции.
Область применения интерполяции
- Строковые листья YAML / JSON, где источник пометил
interpolate = true(YamlFileSource,JsonFileSource— по умолчаниюtrue). - Значения внутри любой вложенности — объекты, массивы, скаляры. Нет ограничения на глубину.
- Ключи объектов не интерполируются —
${DB}_host: …останется ключом"${DB}_host".
Secret masking
Значения, пришедшие через env-интерполяцию и попавшие в поля с «секретными» именами, автоматически маскируются в diagnostic-выводе (config.dump(), сообщения об ошибках, логи). Паттерны суффиксов (case-insensitive):
| Суффикс | Примеры полей |
|---|---|
_key | api_key, encryption_key, signing_key |
_secret | client_secret, webhook_secret |
_token | access_token, refresh_token, bot_token |
_password | db_password, admin_password |
_passphrase | gpg_passphrase |
_credentials | aws_credentials |
Полный список суффиксов нормативно зафиксирован в ADR-0001 §6; машино-читаемая форма появится в _meta/secret_patterns.yaml spec-репозитория (v1.1 backlog). Реализация не расширяет этот список без отдельного ADR — иначе ломается portable-поведение между языками.
Что маскируется. В diagnostic-выводе значение заменяется на строку "***". Пример:
# Исходный конфиг:
llm:
base_url: "https://api.openai.com"
api_key: "${OPENAI_API_KEY}"
# config.dump() после загрузки:
llm:
base_url: "https://api.openai.com"
api_key: "***" # значение подставлено, но замаскировано
Что НЕ маскируется.
- Чтение через
config.getString("llm.api_key")— возвращает реальное значение. Маскирование применяется только к diagnostic-сериализации. - Значения, не совпадающие с суффиксом (
endpoint,host,timeout_ms). Если нужно пометить кастомное поле как секретное — переименуй в<name>_secret/<name>_token/<name>_keyили используй адаптер источника дляdagstack/config-specPhase 2 (Vault, AwsSecretsManager).
Примеры
Плановые переопределения
database:
host: "${DB_HOST:-localhost}"
port: "${DB_PORT:-5432}"
name: "${DB_NAME:-app}"
user: "${DB_USER}" # обязательна
password: "${DB_PASSWORD}" # обязательна + автомаскирование
pool_size: "${DB_POOL_SIZE:-20}" # интерполируется как строка → getInt приведёт к 20
Смешение интерполяции и литералов
api:
request_header: "Bearer ${OPENAI_API_KEY}" # конкатенация внутри строки
user_agent: "dagstack/${APP_VERSION:-dev} ($$ edition)"
# После интерполяции (если APP_VERSION не задана):
# user_agent: "dagstack/dev ($ edition)"
Пустая env-переменная + default
export DB_USER="" # пустая, но установлена
user: "${DB_USER:-anonymous}" # → "anonymous" (пустая трактуется как отсутствие)
Типичные ошибки
- Lowercase env.
${db_host}— портируемо не гарантируется. Держи имена в upper-case. - Пробелы вокруг default.
${VAR:- default}→ значение" default"(с пробелом). Ведущие пробелы сохраняются. - Вложенные
${…}.${FOO:-${BAR}}не разворачивает${BAR}— получишь буквальный"${BAR}". Для fallback-цепочки — считай в приложении. - Экранирование через
\$. Не поддерживается. Единственный escape —$$. - Интерполяция ключей.
${PREFIX}_host: value→ ключ останется"${PREFIX}_host". Интерполируются только значения. - Вызов
getIntна формате1e5.getInt— только^-?\d+$. ИспользуйgetNumberлибо дай значение без научной нотации.
См. также
- Подстановка env-переменных — концептуальный обзор.
- Секреты — подробнее про автоматическое маскирование.
- Синтаксис путей — как читать интерполированные значения.
- Таксономия ошибок —
env_unresolved,type_mismatch,parse_error. - ADR-0001 §2 — нормативное описание.