Генерация конфигов и скриптов прямо из bash - дело частое. Развернуть приложение, собрать файл настроек, записать вспомогательный скрипт на удалённую машину. Для многострочного текста в оболочке есть удобный инструмент - here-document, или heredoc, позволяющий встроить целый блок текста прямо в скрипт без россыпи отдельных команд печати. Но у него есть коварная развилка, на которой спотыкаются даже опытные авторы: один и тот же блок ведёт себя совершенно по-разному в зависимости от того, взят ли разделитель в кавычки.
Суть развилки в подстановке. По умолчанию heredoc раскрывает переменные и выполняет команды внутри блока, как двойные кавычки. А кавычки вокруг разделителя выключают всякую подстановку, превращая блок в буквальный текст. Когда нужно вставить значение переменной - годится первое поведение. Когда нужно сохранить знак доллара как есть, например при генерации другого скрипта, - только второе. Разберём оба режима, типичные ошибки и сценарии, где выбор разделителя критичен.
По умолчанию heredoc раскрывает переменные и выполняет команды
Базовый синтаксис прост. Оператор перенаправления, за ним слово-разделитель, затем блок текста, и наконец тот же разделитель на отдельной строке, закрывающий блок. Слово-разделитель может быть любым, но по традиции пишут что-то вроде сокращения от конца файла.
cat << EOF
Пользователь: $USER
Сегодня: $(date +%F)
Это многострочный блок.
EOF
При незакавыченном разделителе блок ведёт себя как строка в двойных кавычках. Переменная пользователя раскрывается в имя, подстановка команды с датой выполняется и подставляет результат. Оболочка обрабатывает блок до того, как передать его команде.
Пользователь: alice
Сегодня: 2026-05-30
Это многострочный блок.
Это поведение по умолчанию идеально для шаблонов - конфигов, писем, запросов, где как раз нужно вставить актуальные значения переменных в заготовленный текст. Вывод heredoc обычно перенаправляют в файл, и так одной конструкцией рождается готовый файл настроек с подставленными данными.
имя_приложения="МоёПриложение"
порт=8080
cat > приложение.conf << EOF
app_name = $имя_приложения
listen_port = $порт
EOF
Кавычки вокруг разделителя выключают любую подстановку
Теперь главное. Стоит взять разделитель в кавычки - одинарные, двойные, без разницы, - и поведение переворачивается. Подстановка переменных и выполнение команд полностью отключаются. Блок становится буквальным текстом, символ в символ.
cat << 'EOF'
Переменная: $HOME
Команда: $(whoami)
Буквальный доллар: $1
Всё это НЕ будет раскрыто
EOF
На выходе - ровно то, что написано, без раскрытия. Знак доллара остаётся знаком доллара, подстановка команды не срабатывает, позиционный параметр печатается буквально. Достаточно одной пары кавычек вокруг открывающего разделителя, на закрывающем разделителе кавычки уже не нужны.
Эта буквальность незаменима, когда генерируемый текст сам содержит синтаксис переменных, который не должен трогать текущая оболочка. Самый яркий случай - генерация другого скрипта. В создаваемом скрипте есть свои переменные, и если разделитель не закавычить, внешняя оболочка раскроет их прямо сейчас, испортив результат.
cat > /tmp/развернуть.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
echo "Запуск развёртывания..."
cd /opt/приложение
git pull origin main
systemctl restart приложение
echo "Готово"
SCRIPT
chmod +x /tmp/развернуть.sh
Закавыченный разделитель здесь гарантирует, что синтаксис переменных создаваемого скрипта не будет истолкован внешней оболочкой. Скрипт сохранится ровно таким, каким задуман, со всеми своими переменными нетронутыми. Это и есть подавление переменных через закавыченный разделитель - основной приём, ради которого режим существует.
Самая частая ошибка выдаёт себя пустыми значениями
Стоит назвать симптом прямо, потому что узнаётся он мгновенно. Если в выводе heredoc вдруг появляются пустые места там, где ждались значения, или неожиданно подставилось что-то постороннее, - почти наверняка забыт закавыченный разделитель. Внешняя оболочка раскрыла переменные, которые должны были остаться буквальными.
Лечение прямое: сменить незакавыченный разделитель на закавыченный, и буквальный текст вернётся. Обратная ситуация - текст вышел буквальным, хотя ждалось раскрытие, - означает лишний набор кавычек, который надо убрать.
Эта пара симптомов покрывает почти все недоразумения с heredoc. Пустые значения - убрали лишнее раскрытие закавычиванием. Нераскрытые переменные - сняли лишние кавычки. Держа в голове эту связь, отлаживать блоки заметно проще.
Селективное экранирование смешивает буквальное и раскрытое
Иногда в одном блоке нужно и раскрыть часть переменных, и сохранить другие буквально. Закавыченный разделитель тут не подходит - он выключает раскрытие целиком. Решение - незакавыченный разделитель плюс экранирование тех мест, что должны остаться буквальными, обратной косой чертой.
сервер="production"
порт=8080
cat << EOF
Сервер: $сервер
Порт: $порт
Буквальный доллар: \$переменная
Экранированная обратная кавычка: \`команда\`
EOF
Здесь переменные сервера и порта раскрываются, а экранированный знак доллара остаётся буквальным - обратная косая черта перед ним отменяет подстановку для этого конкретного места. Так в одном блоке уживаются и динамические значения, и буквальный синтаксис. Это тоньше, чем простой выбор между двумя режимами, и требует аккуратности, зато даёт полный контроль над каждым символом доллара.
Дефис у оператора убирает ведущие табы для читаемого отступа
Есть неудобство: закрывающий разделитель обязан стоять в самом начале строки, без отступа. В скрипте с вложенными блоками и условиями это ломает выравнивание - heredoc приходится писать прижатым к левому краю, ломая красоту отступов.
Лекарство - дефис, добавленный к оператору перенаправления. Он велит оболочке отбрасывать ведущие символы табуляции и у строк блока, и у закрывающего разделителя. Теперь весь блок можно аккуратно отступить вместе с окружающим кодом.
if true; then
cat <<- EOF
Эта строка была с табами, теперь они срезаны
Отступ сохраняется в коде, но не в выводе
EOF
fi
Важнейшая оговорка: срезаются именно табы, а не пробелы. Если редактор подставляет пробелы вместо табов, приём не сработает, и придётся настроить вставку настоящих табов внутри таких блоков. Это распространённый источник недоумения, ведь на вид отступ есть, а дефис его не убирает - потому что отступ сделан пробелами.
Закрывающий разделитель не терпит ни единого лишнего символа
Здесь притаилась ошибка номер один по частоте. Закрывающий разделитель должен стоять в начале строки и не иметь ни ведущих пробелов, ни хвостовых символов. Любой посторонний символ - пробел после слова, случайный отступ - и оболочка не распознает закрытие блока. Она продолжит считать текстом всё дальше, пока не упрётся в конец файла, и выдаст ошибку о незавершённом heredoc.
Это самый частый сбой при работе с heredoc, и коварен он тем, что причина невидима глазу - хвостовой пробел не виден. Когда блок ведёт себя странно или скрипт жалуется на неожиданный конец файла, первым делом стоит проверить закрывающий разделитель на невидимые символы вокруг него.
Близкий родственник heredoc - here-string, конструкция из тройного оператора для передачи одной короткой строки на вход команде. Она компактна и удобна там, где целый многострочный блок избыточен, а нужно просто скормить команде одно значение.
текст="привет мир"
tr 'а-я' 'А-Я' <<< "$текст"
# короткая передача строки без многострочного блока
Что складывается в рабочее правило
Картина упрощается до ясного выбора. Незакавыченный разделитель - когда в текст нужно вставить значения переменных и результаты команд. Это режим шаблонов: конфиги с актуальными данными, письма, запросы. Закавыченный разделитель - когда текст должен остаться буквальным до символа: генерация других скриптов, конфигов с собственным синтаксисом переменных, любого содержимого со знаками доллара, которые нельзя раскрывать.
Селективное экранирование через обратную косую черту смешивает оба режима в одном блоке, когда часть переменных раскрывается, а часть остаётся буквальной. Дефис у оператора срезает ведущие табы, возвращая коду читаемый отступ, но работает только с табами, не с пробелами. А закрывающий разделитель требует чистоты - ни пробела вокруг, иначе блок не закроется.
Главная мысль одна: кавычки вокруг разделителя - это рубильник подстановки. Есть кавычки - текст буквальный, нет кавычек - переменные раскрываются. Стоит запомнить, что пустые значения в выводе кричат о забытых кавычках, а нераскрытые переменные - о лишних, и работа с heredoc перестаёт преподносить сюрпризы. Один маленький символ вокруг разделителя определяет судьбу всего блока.