Here documents let you embed multi-line blocks of text inline in a script without creating temp files. They're essential for generating config files, running SQL, feeding data into commands, and creating email templates — all without a single external file.
1
Basic here-doc syntax
BASH
# Basic here-doc — delimiter is arbitrary (EOF, END, SQL, etc.)
cat << EOF
Hello, World!
This is line two.
And line three.
EOF
# Redirect here-doc to file
cat << EOF > /etc/myapp/config.ini
[database]
host = prod-db-01
port = 3306
name = myapp
[app]
workers = 8
timeout = 30
EOF
# Variable expansion happens by default
DB_HOST="prod-db-01"
DB_PORT=3306
cat << EOF
Connecting to ${DB_HOST}:${DB_PORT}
Today is $(date '+%Y-%m-%d')
User is ${USER}
EOF
# Variables and commands ARE expanded above
Terminal output
vriddh@prod-01:~/scripts$bash heredoc_demo.sh
Connecting to prod-db-01:3306
Today is 2026-05-01
User is vriddh
█
2
Quoted delimiter — disable expansion
BASH
# Quote the delimiter to PREVENT variable/command expansion
# Useful for generating scripts, config with literal $ signs
cat << 'EOF'
This is a literal $HOME
No expansion: ${DB_HOST}
Command not run: $(date)
Backslash kept: \n
EOF
# Everything prints literally — no substitution
# Practical use: generate a shell script
cat << 'SCRIPT' > /usr/local/bin/check-disk
#!/usr/bin/env bash
# Auto-generated by setup.sh — do not edit
THRESHOLD=80
USAGE=$(df / | awk 'NR==2 {print $5}' | tr -d '%')
if (( USAGE > THRESHOLD )); then
echo "ALERT: Disk usage ${USAGE}% exceeds ${THRESHOLD}%"
exit 1
fi
echo "OK: Disk usage ${USAGE}%"
SCRIPT
chmod +x /usr/local/bin/check-disk
# Mixed: expand some, not others
APP_NAME="myapp"
cat << EOF
# Config for ${APP_NAME} ← expanded
# Edit \${RUNTIME_VAR} below ← escaped, prints literally
APP_NAME=${APP_NAME}
EOF
3
Indented here-doc — <<-
BASH
# <<- strips leading TABS (not spaces) from each line
# Allows indented here-docs inside functions/if blocks
generate_nginx_config() {
local domain="${1}"
local port="${2:-8080}"
cat <<- EOF
server {
listen 80;
server_name ${domain};
location / {
proxy_pass http://127.0.0.1:${port};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
EOF
}
generate_nginx_config "api.example.com" "3000" \
> /etc/nginx/sites-available/api.example.com
echo "Config generated"
⚠ Tabs not spaces —
<<- strips leading tabs only. If your editor converts tabs to spaces, the closing delimiter won't be found and the script will hang waiting for more input. Always use real tab characters with <<-.
4
Feeding SQL with here-docs
BASH
#!/usr/bin/env bash
# Run multi-line SQL directly from a script
DB_HOST="prod-db-01"
DB_USER="app_user"
DB_NAME="myapp"
REPORT_DATE=$(date '+%Y-%m-%d')
mysql -h "${DB_HOST}" -u "${DB_USER}" -p"${DB_PASS}" "${DB_NAME}" << SQL
SELECT
DATE(created_at) AS day,
COUNT(*) AS orders,
SUM(total_amount) AS revenue,
AVG(total_amount) AS avg_order
FROM orders
WHERE created_at >= DATE_SUB('${REPORT_DATE}', INTERVAL 7 DAY)
AND status = 'completed'
GROUP BY DATE(created_at)
ORDER BY day DESC;
SQL
# Here-doc to variable (bash 4+)
read -r -d '' QUERY << 'SQL'
SELECT table_name, table_rows, data_length
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY data_length DESC
LIMIT 10;
SQL
echo "Stored query: ${QUERY}"
5
Here-strings — <<<
Here-strings are a compact single-line version of here-docs. They feed a string directly as stdin to a command — no echo | cmd needed.
BASH
# ── Here-string <<< — feed a string to stdin ─────────────
# Instead of: echo "hello world" | tr ' ' '\n'
tr ' ' '\n' <<< "hello world"
# Instead of: echo "$var" | grep pattern
log_line="[2026-05-01 10:14:02] ERROR: connection refused"
grep -o "ERROR.*" <<< "${log_line}"
# Parse with read
csv_line="prod-db-01,3306,myapp"
IFS=',' read -r host port db <<< "${csv_line}"
echo "Host=${host} Port=${port} DB=${db}"
# bc arithmetic via here-string
result=$(bc -l <<< "scale=2; 355/113")
echo "pi ≈ ${result}"
# awk on a single string
line="2026-05-01 10:14:02 CPU=85% MEM=72%"
awk '{print $3}' <<< "${line}" # CPU=85%
# Send multi-line string (with $'...' quoting)
cat <<< $'line one\nline two\nline three'
6
Real-world — generate server config from template
BASH
#!/usr/bin/env bash
# generate_configs.sh — Generate configs for all environments
generate_app_config() {
local env="${1}"
local db_host="${2}"
local workers="${3:-4}"
local out="/etc/myapp/${env}.conf"
cat << EOF > "${out}"
# Generated by generate_configs.sh on $(date)
# Environment: ${env}
[server]
workers = ${workers}
timeout = 30
log_level = $([ "${env}" = "production" ] && echo "WARN" || echo "DEBUG")
[database]
host = ${db_host}
port = 3306
name = myapp_${env}
pool_size = $((workers * 2))
[cache]
host = cache-${env}.internal
port = 6379
ttl = 3600
EOF
echo " ✔ Generated: ${out}"
}
generate_app_config "development" "localhost" "2"
generate_app_config "staging" "stg-db-01" "4"
generate_app_config "production" "prod-db-01" "16"
vriddh@prod-01:~/scripts$./generate_configs.sh
✔ Generated: /etc/myapp/development.conf
✔ Generated: /etc/myapp/staging.conf
✔ Generated: /etc/myapp/production.conf
vriddh@prod-01:~/scripts$cat /etc/myapp/production.conf
# Generated by generate_configs.sh on Fri May 01 2026
# Environment: production
[server]
workers = 16
log_level = WARN
[database]
host = prod-db-01
pool_size = 32
█
✔ Here-doc rules — Use
<< EOF (unquoted) when you want variable/command expansion. Use << 'EOF' (quoted) when you want the text literally — generating scripts or configs that contain $ signs. Use <<- for indented here-docs (tabs only). Use <<< for single-line string input as a cleaner alternative to echo ... |.