Heredoc Patterns
Overview
Heredocs (here documents) allow embedding multi-line text directly in scripts. Essential for configuration generation, remote commands, and clean multi-line strings.
Basic Syntax
command <<DELIMITER
content line 1
content line 2
DELIMITER
The delimiter can be any word. Common choices: EOF, END, DOC, SQL.
Delimiter Variations
Standard (Variables Expanded)
NAME="World"
cat <<EOF
Hello, $NAME
Current directory: $(pwd)
EOF
Output:
Hello, World Current directory: /home/user
Quoted Delimiter (Literal Content)
cat <<'EOF'
Variables like $NAME are NOT expanded
Commands like $(pwd) are literal
Backslashes \ stay as-is
EOF
Output:
Variables like $NAME are NOT expanded Commands like $(pwd) are literal Backslashes \ stay as-is
Use <<'EOF' (quoted) when content should be literal. Use <<EOF (unquoted) when you want variable expansion.
|
Variable Expansion
Writing to Files
With sudo
# This WON'T work (redirection happens before sudo)
# sudo cat > /etc/protected.conf <<EOF
# content
# EOF
# CORRECT: Use tee
sudo tee /etc/protected.conf > /dev/null <<EOF
[settings]
value = secure
EOF
# CORRECT: Use cat with sudo tee
cat <<EOF | sudo tee /etc/protected.conf > /dev/null
[settings]
value = secure
EOF
Git Commit Messages
Multi-line Commit
git commit -m "$(cat <<EOF
feat(auth): Add OAuth2 support
- Implement authorization code flow
- Add token refresh mechanism
- Configure redirect URIs
EOF
)"
Remote Commands (SSH)
Execute Script on Remote Host
ssh user@server <<'EOF'
cd /var/log
grep "ERROR" *.log | tail -20
systemctl status nginx
EOF
With Variables (local expansion)
LOGDIR="/var/log/myapp"
DATE=$(date +%Y-%m-%d)
ssh user@server <<EOF
find $LOGDIR -name "*.log" -mtime -1
grep "$DATE" $LOGDIR/app.log | tail -50
EOF
Configuration Generation
Nginx Config
DOMAIN="example.com"
PORT="8080"
cat > /etc/nginx/sites-available/$DOMAIN.conf <<EOF
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
location / {
proxy_pass http://127.0.0.1:$PORT;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
}
}
EOF
Note: $DOMAIN and $PORT expand locally, while $host and $remote_addr are escaped to remain as Nginx variables.
Systemd Unit
SERVICE_NAME="myapp"
USER="appuser"
EXEC_PATH="/usr/local/bin/myapp"
cat > /etc/systemd/system/${SERVICE_NAME}.service <<EOF
[Unit]
Description=$SERVICE_NAME Service
After=network.target
[Service]
Type=simple
User=$USER
ExecStart=$EXEC_PATH
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable $SERVICE_NAME
Database Schema
DB_NAME="production"
TABLE_PREFIX="app_"
psql -U admin $DB_NAME <<EOF
CREATE TABLE IF NOT EXISTS ${TABLE_PREFIX}users (
id SERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_${TABLE_PREFIX}users_email
ON ${TABLE_PREFIX}users(email);
EOF
Herestrings
Single-line variant using <<<:
# Pass string as stdin
grep "pattern" <<< "search in this string"
# Variable as input
DATA="line1
line2
line3"
grep "line2" <<< "$DATA"
# To command
base64 <<< "encode this"
# Read into variable
read VAR <<< "value"
Functions with Heredocs
Pipeline Integration
YAML Generation
APP_NAME="myapp"
VERSION="1.0.0"
REPLICAS=3
cat <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: $APP_NAME
spec:
replicas: $REPLICAS
template:
spec:
containers:
- name: $APP_NAME
image: registry/$APP_NAME:$VERSION
ports:
- containerPort: 8080
EOF
Common Patterns
Conditional Content
DEBUG=${DEBUG:-false}
cat <<EOF
[application]
name = myapp
$(if [ "$DEBUG" = "true" ]; then
echo "log_level = debug"
echo "verbose = true"
else
echo "log_level = info"
fi)
EOF
Quick Reference
# Basic heredoc (variables expand)
cat <<EOF
Hello $NAME
EOF
# Literal heredoc (no expansion)
cat <<'EOF'
Literal $NAME
EOF
# Strip leading tabs
cat <<-EOF
Indented content
EOF
# Write to file
cat > file.txt <<EOF
Content
EOF
# Append to file
cat >> file.txt <<EOF
More content
EOF
# With sudo
sudo tee /etc/file > /dev/null <<EOF
Content
EOF
# Git commit
git commit -m "$(cat <<'EOF'
Multi-line message
EOF
)"
# SSH remote execution
ssh user@host <<'EOF'
remote commands
EOF
# Herestring (single line)
grep pattern <<< "search string"
Troubleshooting
Common Issues
| Issue | Solution |
|---|---|
Variables not expanding |
Use |
Variables expanding unexpectedly |
Use |
Whitespace issues |
Use |
Redirect with sudo fails |
Use |
Heredoc in function fails |
Ensure proper quoting and escaping |
API Testing with curl
REST API Requests
# POST with JSON body
curl -X POST "$API_URL/users" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d "$(cat <<EOF
{
"username": "$USERNAME",
"email": "$EMAIL",
"role": "user"
}
EOF
)"
# PUT update
curl -X PUT "$API_URL/users/$USER_ID" \
-H "Content-Type: application/json" \
-d "$(cat <<EOF
{
"status": "active",
"updated_at": "$(date -Iseconds)"
}
EOF
)"
GraphQL Queries
QUERY=$(cat <<'EOF'
{
"query": "query GetUser($id: ID!) { user(id: $id) { name email } }",
"variables": { "id": "123" }
}
EOF
)
curl -X POST "$GRAPHQL_URL" \
-H "Content-Type: application/json" \
-d "$QUERY"
Multi-Part Forms
# Generate boundary
BOUNDARY="----FormBoundary$(date +%s)"
curl -X POST "$UPLOAD_URL" \
-H "Content-Type: multipart/form-data; boundary=$BOUNDARY" \
--data-binary "$(cat <<EOF
--$BOUNDARY
Content-Disposition: form-data; name="title"
My Upload
--$BOUNDARY
Content-Disposition: form-data; name="file"; filename="data.txt"
Content-Type: text/plain
$(cat data.txt)
--$BOUNDARY--
EOF
)"
Docker and Containers
Dockerfile Generation
APP_NAME="myapp"
APP_PORT="8080"
NODE_VERSION="20"
cat > Dockerfile <<EOF
FROM node:${NODE_VERSION}-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE ${APP_PORT}
ENV NODE_ENV=production
ENV PORT=${APP_PORT}
USER node
CMD ["node", "server.js"]
EOF
Docker Compose
PROJECT_NAME="myproject"
DB_PASSWORD=$(openssl rand -base64 24)
cat > docker-compose.yml <<EOF
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
environment:
- DATABASE_URL=postgres://user:${DB_PASSWORD}@db:5432/${PROJECT_NAME}
depends_on:
- db
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: ${PROJECT_NAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
EOF
Container Exec Commands
# Run script inside container
docker exec -i mycontainer bash <<'EOF'
cd /app
npm run migrate
npm run seed
echo "Database initialized"
EOF
# Database operations
docker exec -i postgres psql -U admin <<EOF
CREATE DATABASE IF NOT EXISTS ${DB_NAME};
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
EOF
CTF and Security Automation
Encoding/Decoding
# Batch base64 decode
cat <<'EOF' | while read encoded; do
echo "=== $encoded ==="
echo "$encoded" | base64 -d 2>/dev/null
echo
done
Zm xhZ3t0ZXN0fQ==
c2VjcmV0X2tleQ==
cGFzc3dvcmQxMjM=
EOF
# ROT13 decoder
cat <<'EOF' | tr 'A-Za-z' 'N-ZA-Mn-za-m'
Guvf vf n frperg zrffntr
EOF
# Hex to ASCII
cat <<'EOF' | xxd -r -p
666c61677b746573745f666c61677d
EOF
# Multiple encoding attempts
ENCODED="VGVzdCBTdHJpbmc="
cat <<'EOF' | while read method; do
echo "=== $method ==="
case $method in
base64) echo "$ENCODED" | base64 -d 2>/dev/null ;;
hex) echo "$ENCODED" | xxd -r -p 2>/dev/null ;;
rot13) echo "$ENCODED" | tr 'A-Za-z' 'N-ZA-Mn-za-m' ;;
esac
done
base64
hex
rot13
EOF
Password Generation and Testing
# Generate wordlist mutations
BASE_WORD="password"
cat <<'EOF' | while read pattern; do
eval "echo $pattern"
done > mutations.txt
${BASE_WORD}
${BASE_WORD}123
${BASE_WORD}!
${BASE_WORD}@2026
${BASE_WORD^^}
${BASE_WORD^}
EOF
# Hash cracking prep
TARGET_HASH="5f4dcc3b5aa765d61d8327deb882cf99"
cat <<'EOF' | while read word; do
HASH=$(echo -n "$word" | md5sum | cut -d' ' -f1)
if [ "$HASH" = "$TARGET_HASH" ]; then
echo "FOUND: $word"
fi
done
password
password123
secret
admin
EOF
Network Recon Scripts
# Port scan wrapper
TARGET="192.168.1.1"
cat <<'EOF' | while read port; do
timeout 1 bash -c "echo >/dev/tcp/$TARGET/$port" 2>/dev/null && echo "Port $port: OPEN"
done
22
80
443
8080
3306
5432
EOF
# DNS enumeration
DOMAIN="example.com"
cat <<'EOF' | while read sub; do
host "${sub}.${DOMAIN}" 2>/dev/null | grep -q "has address" && echo "${sub}.${DOMAIN}"
done
www
mail
ftp
admin
api
dev
staging
EOF
Database Operations
MySQL/MariaDB
mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" <<EOF
-- Create tables
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Insert data
INSERT INTO users (username, email) VALUES
('admin', 'admin@example.com'),
('user1', 'user1@example.com')
ON DUPLICATE KEY UPDATE email = VALUES(email);
-- Query and export
SELECT * FROM users INTO OUTFILE '/tmp/users.csv'
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n';
EOF
PostgreSQL
PGPASSWORD="$DB_PASS" psql -h localhost -U "$DB_USER" "$DB_NAME" <<EOF
-- Create schema
CREATE SCHEMA IF NOT EXISTS app;
-- Create tables
CREATE TABLE IF NOT EXISTS app.events (
id SERIAL PRIMARY KEY,
event_type VARCHAR(50),
payload JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Insert with JSON
INSERT INTO app.events (event_type, payload) VALUES
('login', '{"user": "admin", "ip": "192.168.1.1"}'),
('action', '{"type": "update", "target": "profile"}');
-- Query JSON
SELECT * FROM app.events
WHERE payload->>'user' = 'admin';
EOF
SQLite
DB_FILE="app.db"
sqlite3 "$DB_FILE" <<'EOF'
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
level TEXT,
message TEXT
);
INSERT INTO logs (level, message) VALUES
('INFO', 'Application started'),
('WARN', 'High memory usage');
.mode csv
.headers on
.output logs.csv
SELECT * FROM logs;
.output stdout
.quit
EOF
Testing and Automation
Test Data Generation
# Generate test users JSON
cat <<'EOF' | while read i; do
echo "{\"id\": $i, \"name\": \"user$i\", \"email\": \"user$i@test.com\"}"
done | jq -s '.' > test_users.json
1
2
3
4
5
EOF
# Generate CSV test data
cat > test_data.csv <<EOF
id,name,value,timestamp
$(for i in {1..10}; do
echo "$i,item$i,$((RANDOM % 100)),$(date -d "-$i days" +%Y-%m-%d)"
done)
EOF
Expect-like Scripts
# Automated SSH setup
cat <<'EOF' | ssh-keygen -t ed25519 -C "automated" -f ~/.ssh/auto_key
y
EOF
# Automated input to interactive command
./interactive_program <<EOF
yes
myusername
mypassword
n
quit
EOF
CI/CD Pipeline Snippets
# Generate GitHub Actions workflow
cat > .github/workflows/ci.yml <<'EOF'
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- run: npm run build
EOF
# Generate GitLab CI
cat > .gitlab-ci.yml <<'EOF'
stages:
- test
- build
- deploy
test:
stage: test
script:
- npm ci
- npm test
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
EOF
Error Handling Patterns
Validate Before Write
CONFIG_CONTENT=$(cat <<EOF
[server]
host = $HOST
port = $PORT
EOF
)
# Validate variables exist
if [ -z "$HOST" ] || [ -z "$PORT" ]; then
echo "ERROR: HOST and PORT must be set" >&2
exit 1
fi
# Write only if valid
echo "$CONFIG_CONTENT" > /etc/app/config.ini
Backup Before Overwrite
TARGET="/etc/nginx/nginx.conf"
BACKUP="${TARGET}.bak.$(date +%Y%m%d%H%M%S)"
# Backup existing
[ -f "$TARGET" ] && cp "$TARGET" "$BACKUP"
# Write new config
cat > "$TARGET" <<EOF
worker_processes auto;
events {
worker_connections 1024;
}
http {
include /etc/nginx/conf.d/*.conf;
}
EOF
# Validate and rollback if needed
if ! nginx -t 2>/dev/null; then
echo "Invalid config, rolling back"
mv "$BACKUP" "$TARGET"
exit 1
fi
Multi-Line Variable Assignment
Simple Assignment
# Using $()
SCRIPT=$(cat <<'EOF'
#!/bin/bash
echo "This is a script"
exit 0
EOF
)
# Using read
read -r -d '' HTML <<'EOF'
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body><h1>Hello</h1></body>
</html>
EOF
# Using printf
MESSAGE=$(printf '%s\n' \
"Line 1" \
"Line 2" \
"Line 3"
)