Перейти к основному содержимому

Синтаксис подстановки 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 не задана. Это осознанное упрощение парсера.

Момент выполнения

  1. source.load() — источник возвращает дерево с буквальными строками, включая ${VAR}.
  2. Интерполяция — загрузчик обходит все строковые листья дерева источников, у которых interpolate = true, и выполняет подстановку.
  3. Deep merge — интерполированные деревья всех источников сливаются по приоритету.
  4. 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):

СуффиксПримеры полей
_keyapi_key, encryption_key, signing_key
_secretclient_secret, webhook_secret
_tokenaccess_token, refresh_token, bot_token
_passworddb_password, admin_password
_passphrasegpg_passphrase
_credentialsaws_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-spec Phase 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 либо дай значение без научной нотации.

См. также