シェルスクリプトのヒアドキュメント設計|設定ファイル・SQLクエリ・通知メール本文を動的生成する実装パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトのヒアドキュメント設計|設定ファイル・SQLクエリ・通知メール本文を動的生成する実装パターン
「Nginxのバーチャルホスト設定をスクリプトで自動生成しようとしたら、echoをずらずら並べるだけで50行以上になってしまった」

設定ファイルを動的に作りたい場面でechoを連ねると、クォートのエスケープが増えて視認性が一気に落ちます。SQLを複数行まとめてMySQLに渡したいときも、変数をどこで展開すべきか悩む場面があるでしょう。

この記事では、シェルスクリプトのヒアドキュメント(here document)を使った動的生成の設計パターンを解説します。基本3構文(変数展開ON/OFF・インデント対応)から、設定ファイル生成・SQL一括実行・通知メール本文の実践パターン、よくあるトラブルシュートまでを網羅します。動作確認環境はRHEL 9.4 / Ubuntu 24.04 LTSです。

この記事のポイント

・<<EOFは変数展開あり、<<'EOF'はシングルクォートで囲んで展開なしにできる
・<<-EOFを使うとタブでインデントした行頭を揃えてコードを整理できる
・ssh・mysql・mailコマンドと組み合わせると複数行の入力を安全に渡せる
・区切り文字はEOF以外の任意の文字列でよい。ネスト時は別名を使う


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜヒアドキュメントが必要なのか(echoの限界)

設定ファイルを動的に生成するとき、echoとヒアドキュメントの書き方を比べてみましょう。どちらも同じNginxのserver{}ブロックを生成するコードです。

#!/bin/bash SERVER_NAME="example.com" DOC_ROOT="/var/www/html/example" # echo版(行ごとにリダイレクトが必要でクォートのエスケープが増える) echo "server {" > /etc/nginx/conf.d/example.conf echo " listen 80;" >> /etc/nginx/conf.d/example.conf echo " server_name ${SERVER_NAME};" >> /etc/nginx/conf.d/example.conf echo " root ${DOC_ROOT};" >> /etc/nginx/conf.d/example.conf echo " index index.html index.htm;" >> /etc/nginx/conf.d/example.conf echo "}" >> /etc/nginx/conf.d/example.conf

#!/bin/bash SERVER_NAME="example.com" DOC_ROOT="/var/www/html/example" # ヒアドキュメント版(ファイルに書きたい内容をそのままスクリプト内に書ける) cat < /etc/nginx/conf.d/example.conf server { listen 80; server_name ${SERVER_NAME}; root ${DOC_ROOT}; index index.html index.htm; } EOF

ヒアドキュメント版は「最終的にファイルに書きたいもの」をそのまま書くだけです。クォートのエスケープを考える必要がなく、変数(${SERVER_NAME}など)はそのまま展開されます。行数が増えるほど、この差は大きくなります。

ヒアドキュメントの基本構文と3つの記法

1. 標準構文(変数展開あり)

基本形は cat <<区切り文字 です。区切り文字(よく「EOF」や「END」が使われます)は開始行と終了行が一致していれば任意の文字列を使えます。終了行の区切り文字は行頭から書く必要があります。

#!/bin/bash DB_HOST="db01.internal" DB_NAME="production" # 区切り文字はEOF以外にも END や HEREDOC など何でもよい cat <

実行結果(RHEL 9.4):

[miyazaki@web01 ~]$ bash heredoc_test.sh [database] host = db01.internal name = production port = 3306

${DB_HOST}と${DB_NAME}が展開されて出力されました。ファイルへ書き込む場合は cat <<EOF > /path/to/file のようにリダイレクトを追加します。

2. 展開を止める構文('EOF'をシングルクォートで囲む)

区切り文字をシングルクォートで囲む('EOF')と、本文内の変数展開・コマンド置換・バックスラッシュ解釈がすべて無効になります。後からsedや別のツールで置換するテンプレートファイルを生成するときに使います。

#!/bin/bash # 変数展開を止めたい場合は区切り文字をシングルクォートで囲む cat <<'EOF' > /tmp/template_raw.txt # このファイルは後でsedで置換する HOST=${DB_HOST} NAME=${DB_NAME} PORT=3306 EOF cat /tmp/template_raw.txt

実行結果:

[miyazaki@web01 ~]$ bash heredoc_noexpand.sh # このファイルは後でsedで置換する HOST=${DB_HOST} NAME=${DB_NAME} PORT=3306

${DB_HOST}がそのまま文字列として書き込まれています。展開が必要かどうかをケースごとに選び分けるのがヒアドキュメント設計の第一歩です。

3. インデントを効かせる(<<-EOF)

<<-EOF(ハイフン付き)を使うと、ヒアドキュメント本文の行頭タブ文字が取り除かれます。関数やif文の中でヒアドキュメントを書くとき、スクリプト本体のインデントに合わせてヒアドキュメントの内容も字下げできます。

#!/bin/bash generate_config() { local server_name="$1" # <<-EOF で行頭のタブを取り除く(本文はタブで字下げしてある) cat <<-EOF [server] name = ${server_name} enabled = true EOF } generate_config "app01" generate_config "app02"

実行結果:

[miyazaki@web01 ~]$ bash heredoc_indent.sh [server] name = app01 enabled = true [server] name = app02 enabled = true

重要:<<-EOFが取り除くのはタブ文字のみです。スペースで字下げしても取り除かれません
・エディタが「タブをスペースに自動変換」する設定になっている場合は動作しません
・後述のトラブルシュートでタブとスペースの確認方法を解説します

実践:設定ファイルの動的生成

1. ループで複数のバーチャルホスト設定を生成する

バーチャルホストが複数ある場合、ドメイン名・ポート番号・ドキュメントルートをループで変えながら設定ファイルを一括生成できます。

#!/bin/bash # 複数バーチャルホストのNginx設定を一括生成するスクリプト # 書式: "サイト名:ポート:ドキュメントルート" SITES=( "site-a:8081:/var/www/site-a" "site-b:8082:/var/www/site-b" "site-c:8083:/var/www/site-c" ) for site_info in "${SITES[@]}"; do IFS=':' read -r name port docroot <<< "${site_info}" conf_file="/etc/nginx/conf.d/${name}.conf" cat < "${conf_file}" server { listen ${port}; server_name ${name}.example.com; root ${docroot}; index index.html index.htm; access_log /var/log/nginx/${name}_access.log; error_log /var/log/nginx/${name}_error.log; } EOF echo "[$(date '+%Y-%m-%d %H:%M:%S')] 生成完了: ${conf_file}" done

実行結果:

[miyazaki@web01 ~]$ sudo bash gen_vhost.sh [2026-08-12 10:14:32] 生成完了: /etc/nginx/conf.d/site-a.conf [2026-08-12 10:14:32] 生成完了: /etc/nginx/conf.d/site-b.conf [2026-08-12 10:14:32] 生成完了: /etc/nginx/conf.d/site-c.conf

設定を反映する際はNginxの構文チェック(nginx -t)と再読み込みが必要です。Apacheを使っている場合はhttpdの再起動が必要になります。httpd の基本操作で起動・停止・設定テストの手順を確認できます。

2. SSH経由でリモートサーバーに設定を流し込む

ヒアドキュメントはSSHコマンドと組み合わせると、リモートサーバー上で実行するコマンドをまとめて渡せます。踏み台なしでリモートに設定を反映するバッチ処理に使えます。

#!/bin/bash REMOTE_HOST="app01.internal" DEPLOY_DIR="/opt/myapp" TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') ssh miyazaki@${REMOTE_HOST} <> "${DEPLOY_DIR}/logs/deploy.log" systemctl restart myapp.service systemctl is-active myapp.service && echo "起動確認OK" || echo "起動失敗" EOF

・リモート側で \$(date) のようなコマンド置換を使う場合はバックスラッシュでエスケープが必要です(ローカル側で展開されてしまうため)
・リモートでの展開も止めたい場合は <<'EOF' を使います

ヒアドキュメントを含むシェルスクリプトの設計力を体系的に身につけたい方は、シェルスクリプト実践講座(Linux Master Pro)もご覧ください。現場で使える設計パターンを体系的に学べます。

実践:コマンドへの複数行入力

1. MySQLにSQLをまとめて実行する

バッチ処理でMySQLにSQL文を複数渡す場面では、ヒアドキュメントをmysqlコマンドに流し込む形が実用的です。

#!/bin/bash DB_USER="appuser" DB_NAME="inventory" TARGET_DATE="2026-08-01" mysql -u "${DB_USER}" -p"$(cat /etc/myapp/db_pass)" "${DB_NAME}" <

・区切り文字にSQLを使ったのは可読性のためです。「何のためのヒアドキュメントか」を区切り文字名で示すと、コードレビュー時に意図が伝わりやすくなります
・パスワードはファイルから読み込んでスクリプト内に直書きしないようにしてください
・SQLの実行結果を確認したい場合は mysql ... <<SQL | tee /var/log/batch_result.log でログに残せます

2. mailコマンドで通知メール本文を動的生成する

cronで動くバッチスクリプトで障害を検知したとき、その場で構造化された本文を生成してメール送信できます。

#!/bin/bash HOSTNAME=$(hostname) DISK_USAGE=$(df -h /var | awk 'NR==2{print $0}') TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S') ALERT_MAIL="admin@example.com" send_disk_alert() { local usage_pct="$1" mail -s "[ALERT] ${HOSTNAME} ディスク使用率 ${usage_pct}% 超過" "${ALERT_MAIL}" <

区切り文字をMAIL_BODYにしたのは「メール本文用のヒアドキュメント」だとひと目でわかるようにするためです。EOFでも機能しますが、ネストしたヒアドキュメントが出てくると名前の衝突を避けるためにも意味のある命名が有効です。

トラブルシュート よくある失敗パターン

変数が展開されない(シングルクォートEOFを使ったとき)

設定ファイルに ${SERVER_NAME} という文字列がそのまま書き込まれてしまう場合、原因の大半は <<'EOF' を使ってしまったことです。

#!/bin/bash SERVER_NAME="example.com" # NG: シングルクォート区切りは変数展開されない cat <<'EOF' server_name ${SERVER_NAME}; EOF # OK: クォートなし区切りは変数展開される cat <

実行結果:

[miyazaki@web01 ~]$ bash heredoc_ng.sh server_name ${SERVER_NAME}; server_name example.com;

「テンプレートファイルとして保存して後でsedで置換する」目的のときだけ <<'EOF' を使い、変数を展開したい場合は <<EOF(クォートなし)を使ってください。

インデントが効かない(タブではなくスペースで字下げしている)

<<-EOF を使ってもインデントが取り除かれない場合、エディタがタブの代わりにスペースを使っている可能性があります。cat -A でタブ文字(^I)とスペースを判別できます。

# cat -Aでタブ(^I)とスペースを確認する [miyazaki@web01 ~]$ cat -A heredoc_indent.sh | grep -n "name =" 7: name = ${server_name};$ # ^Iが見えればタブ(正常) 8: name = ${server_name};$ # スペースのみだと <<-EOF が効かない

^I が表示されている行はタブ文字で字下げされています。スペースが表示されている場合はエディタの設定を確認するか、ヒアドキュメント本文の字下げをスペースからタブに手動で変換してください。Vimの場合は :set noexpandtab でタブ入力に切り替えられます。

本記事のまとめ

やりたいこと 記法・設計パターン
変数を展開して複数行テキストを出力する cat <<EOF ... EOF
変数・コマンド置換を展開せずに生のテキストを出力する cat <<'EOF' ... EOF
関数・if文内でインデントを揃えて書く cat <<-EOF(本文はタブで字下げ)
設定ファイルを変数パラメータで動的生成する cat <<EOF > /etc/nginx/conf.d/${name}.conf
SSHでリモートに複数のコマンドをまとめて渡す ssh user@host <<EOF ... EOF
MySQLに複数のSQL文を一括実行する mysql dbname <<SQL ... SQL
mailコマンドに動的な本文を渡す mail -s "件名" addr <<MAIL_BODY ... MAIL_BODY
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。