Многострочный текст в сценарии всегда был проблемой неудобной. Собирать конфиг из десятка echo читается плохо, printf с переводами строк запутывает, а отдельный файл шаблона тянет за собой вторую сущность, которую надо возить вместе со сценарием. Встроенный документ, heredoc, решает задачу красиво: текст живёт прямо внутри сценария, читается как текст и передаётся команде целиком. За простотой скрывается пара тонких режимов, разница между которыми отличает рабочий сценарий от сценария с дырой на подстановку чужих команд. Статья разбирает оба режима, генерацию конфигов, передачу скриптов через стандартный ввод и гигиену многострочных блоков.

Механизм heredoc и его синтаксис в двух вариантах

Синтаксис встроенного документа читается как служебное предложение: команда, затем оператор с двумя угловыми скобками, имя разделителя, строки текста и закрывающая строка с тем же именем. Всё, что стоит между разделителями, становится вводом команды, как будто его набрали с клавиатуры:

cat << EOF
первая строка текста
вторая строка текста
EOF

Имя разделителя произвольно, и привычный маркер конца это просто традиция, а не зарезервированное слово. Тонкость в кавычках вокруг имени. Разделитель без кавычек включает режим подстановки: переменные и подстановки команд внутри текста разворачиваются перед передачей. Разделитель в кавычках делает текст литеральным: что написано, то и уйдёт, без исключений. Второй режим с дефисом подавляет ведущие табы, позволяя держать блок с отступом внутри сценария, и работает он именно с табами, а не с пробелами, что стоит помнить при настройках редактора.

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

Отдельное удобство для больших сценарных работ: содержимое блока не проходит синтаксический разбор оболочки как код. Оболочка лишь ищет закрывающий разделитель, и всё между строками остаётся текстом. Это свойство делает механизм пригодным для передачи произвольных содержимых, от конфигов до запросов, без экранирования каждой строки.

Генерация конфигурационных файлов шаблонами прямо из скрипта

Классическое применение механизма это развёртывание: сценарий установки создаёт конфигурационный файл целиком, не таская его отдельным артефактом. Текст конфига читается в сценарии как конфиг, правится в одном месте и попадает на диск одной операцией:

cat > /etc/app/config.conf <<'EOF'
listen 8080
workers 4
log_level info
EOF

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

Гигиена записи файла завершает картину. Прямая запись целевым файлом работает, но культурнее атомарная схема: запись во временный файл с проверкой, затем переименование в целевой путь. Читатели конфигурации не видят полуфайлов, а при отказе на середине записи старый конфиг остаётся целым. Права на новый файл выставляются сразу, до переименования, а не после, потому что окно между ними это окно для любопытных.

Конфиги со встроенными переменными окружения при развертывании наполняются вторым режимом, о котором ниже. Базовый же вывод статьи прост: если сценарий создаёт файл из известного текста, встроенный документ это самый читаемый и самый управляемый способ это сделать.

Кавычки в разделителе и предотвращение нежелательной подстановки

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

Закавыченный разделитель выключает разворачивание целиком. Текст уходит буквально, и конфиг веб-сервера с его собственными переменными в стиле доллара, правилами и условиями создаётся без единого экранирования:

cat > site.conf <<'EOF'
location /static {
    try_files $uri @fallback;
    expires 1h;
}
EOF

Правило безопасности умещается в строку: разделитель в кавычках это режим по умолчанию, подстановка включается осознанно и только там, где содержимое контролируемо. Смешанный случай, когда в конфиге нужны и литеральные доллары, и подстановки своих переменных, решается не экранированием каждого спецсимвола, а сменой метода: литеральный блок с маркерами-плейсхолдерами, которые заполняются отдельным проходом после записи. Такой подход дольше на строку и надёжнее на порядок.

Проверка режима стóит одной привычки: при написании блока с любым содержимым, отличным от собственного текста, задать себе вопрос, что случится при встрече с подстановкой внутри. Ответ в незакавыченном режиме всегда один: она выполнится, и если этот ответ не входит в планы, кавычки обязаны появиться в разделителе немедленно.

Передача скриптов и запросов через стандартный ввод

Встроенный документ это не только создание файлов, но и канал для команд. Удалённое выполнение блока команд без промежуточных файлов строится на передаче текста в удалённую оболочку:

ssh admin@backup-host bash -s <<'EOF'
set -euo pipefail
systemctl restart nightly-backup
journalctl -u nightly-backup -n 20 --no-pager
EOF

Закавыченный разделитель здесь означает, что блок интерпретируется на удалённой стороне, а не на локальной: подстановки в тексте выполняются там, где его контекст, и сценарий не разворачивает переменные до отправки. Одна конструкция заменяет два файла, локальный сценарий и удалённый, и весь экспромт остаётся в одном читаемом месте.

Запросы к базам строятся тем же каналом. Клиентские утилиты баз читают команды со стандартного ввода, и сценарий обслуживания отправляет запрос блоком без файлов и без конвейеров со слэшами. Калькулятор произвольной точности, диалоговые режимы утилит и пакетные интерпретаторы принимают блоки тем же способом, и везде выигрыш одинаковый: содержимое видно в сценарии целиком, с контекстом и отступом.

Отдельная строка практики: блоки для удалённого выполнения пишутся по тем же правилам строгого режима, что и обычные сценарии, и первая строка удалённого текста часто задаёт строгие опции. Механизм передачи не меняет требований к содержимому, он лишь меняет место, откуда содержимое берётся.

Типовые ошибки и гигиена многострочных блоков

Первая типовая ошибка это встреча разделителя в содержимом. Если закрывающее слово встречается в тексте блока по своему значению, документ обрывается раньше времени, и остаток уходит в сценарий как команды. Лечение простое: осмысленные уникальные разделители, привязанные к назначению блока, и привычка глянуть на содержимое перед выбором имени.

Вторая ошибка это непослушные отступы. Режим с подавлением табов убирает ведущие табы, но не пробелы, и сценарий, написанный в редакторе с пробелами по умолчанию, молча оставит блок с отступом. Закрывающий разделитель с пробелом перед ним в базовом режиме не узнаётся вовсе, и это первый кандидат на осмотр при странном поведении конструкции.

Третья группа ошибок редакторская. Переводы строк в стиле другой платформы делают разделитель неузнаваемым, невидимые хвосты пробелов за разделителем превращают конец в не-конец, и обе проблемы решаются настройкой редактора и привычкой смотреть на строку конца блока глазами, а не памятью.

Вложенные блоки заслуживают отдельного предупреждения: конструкция внутри конструкции читается всё хуже с каждым уровнем, и практики предпочитают плоские сценарии с последовательными блоками. Там, где вложенность кажется неизбежной, это обычно сигнал вынести внутренний блок в отдельный шаг или файл.

Шаблоны с подстановкой переменных и безопасное заполнение

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

Промышленный паттерн для смешанных конфигов строится иначе: литеральный блок с кавычками в разделителе и маркерами-плейсхолдерами внутри, затем один проход заполнения маркеров значениями. Маркеры выбираются так, чтобы не встретиться в конфиге сами по себе, заполнение выполняется отдельной командой подстановки, и доллары конфига остаются долларами конфига. Схема держит границы rõ: что литерально, что подставляется, и в каком порядке.

Чужие сведения в подстановках не участвуют вовсе. Значения из пользовательского ввода, конфигураций внешних систем и ответов сервисов либо валидируются до попадания в блок, либо подставляются через безопасный механизм заполнения, но никогда не разворачиваются оболочкой. Строка, содержащая подстановку команды, это не теоретическая угроза, а самая частая находка аудита сценариев, и режим по умолчанию с кавычками в разделителе закрывает её в девяти случаях из десяти.

Отдельная вежливость к будущему читателю: блоки с подстановкой помечаются комментарием о том, какие переменные ожидаются. Сценарий, который разворачивает переменные из невидимого контекста, превращает отладку в гадание, и одна строка комментария о списке подставляемых имён экономит час чтения через полгода.

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

Рабочие сценарии применения heredoc в сценариях администрирования

Сценарии, в которых встроенные документы раскрываются лучше всего, выглядят так:

  1. Создание конфигурационных файлов при развёртывании из одного сценария;
  2. Передача блока команд на удалённую машину без промежуточных файлов;
  3. Отправка запросов в базы данных прямо из сценария обслуживания;
  4. Наполнение шаблонов маркерами с отдельным проходом безопасной подстановки;
  5. Диалоговые режимы утилит и пакетные интерпретаторы через стандартный ввод;
  6. Хранение примеров конфигурации в репозитории сценария как документации.

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

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