Shell Script Hack #9: Here Documents – Clean Multi-Line Text Input

Shell Script Hack #9: Here Documents – Clean Multi-Line Text Input

Need to create a configuration file, send multi-line input to a command, or generate SQL scripts from your shell? Here documents let you embed blocks of text directly in your scripts without messy echo statements or temporary files.

The Problem

You need to create a configuration file with multiple lines. The traditional approach is tedious:

echo "server {" > nginx.conf
echo "    listen 80;" >> nginx.conf
echo "    server_name example.com;" >> nginx.conf
echo "    location / {" >> nginx.conf
echo "        proxy_pass http://localhost:3000;" >> nginx.conf
echo "    }" >> nginx.conf
echo "}" >> nginx.conf

This is repetitive, error-prone, and hard to read.

The Hack: Here Documents (heredoc)

Here documents let you write multi-line text inline with your script:

cat > nginx.conf << 'EOF'
server {
    listen 80;
    server_name example.com;
    location / {
        proxy_pass http://localhost:3000;
    }
}
EOF

Clean, readable, and maintainable!

Basic Syntax

command << DELIMITER
multi-line
content
here
DELIMITER

The delimiter can be any word (commonly EOF, END, or HEREDOC), but must match exactly.

Common Patterns

Create Files

# Create new file
cat > config.yml << EOF
database:
  host: localhost
  port: 5432
  name: mydb
EOF

# Append to existing file
cat >> log.txt << EOF
New log entry
Timestamp: $(date)
EOF

Send Input to Commands

# MySQL queries
mysql -u root -p << EOF
CREATE DATABASE testdb;
USE testdb;
CREATE TABLE users (id INT, name VARCHAR(50));
INSERT INTO users VALUES (1, 'John');
EOF

# Python script
python << EOF
print("Hello from heredoc")
for i in range(5):
    print(f"Number: {i}")
EOF

# Mail with body
mail -s "Alert" admin@example.com << EOF
Server Alert!
CPU usage is above 90%
Time: $(date)
EOF

SSH Remote Commands

# Execute multiple commands on remote server
ssh user@server << 'EOF'
cd /var/www/html
git pull origin main
npm install
pm2 restart app
EOF

Variable Expansion Control

With Expansion (Unquoted Delimiter)

NAME="World"

cat << EOF
Hello, $NAME!
Today is $(date)
Your home: $HOME
EOF

# Output:
# Hello, World!
# Today is Fri Oct 31 20:00:00 UTC 2025
# Your home: /home/user

Without Expansion (Quoted Delimiter)

NAME="World"

cat << 'EOF'
Hello, $NAME!
Today is $(date)
Your home: $HOME
EOF

# Output (literal):
# Hello, $NAME!
# Today is $(date)
# Your home: $HOME

Mixed Expansion

# Escape specific variables
cat << EOF
Expanded: $HOME
Literal: \$USER
Command: $(hostname)
Literal command: \$(date)
EOF

Indentation Control

Remove Leading Tabs (<<-)

if true; then
    cat <<- EOF
	This line starts with a tab
	So does this one
	But tabs are removed in output
	EOF
fi

# Output (no leading tabs):
# This line starts with a tab
# So does this one
# But tabs are removed in output

Note: This only works with TABS, not spaces!

Real-World Use Cases

Docker Configuration

#!/bin/bash

# Generate Dockerfile
cat > Dockerfile << 'EOF'
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
EOF

# Generate docker-compose.yml
cat > docker-compose.yml << EOF
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DB_HOST=${DB_HOST:-localhost}
    volumes:
      - ./data:/app/data
EOF

echo "Docker files created!"

Database Setup Script

#!/bin/bash

DB_NAME="myapp"
DB_USER="appuser"
DB_PASS="secure_password"

# Initialize database
psql -U postgres << EOF
CREATE DATABASE ${DB_NAME};
CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';
GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};
\c ${DB_NAME}

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id),
    title VARCHAR(200) NOT NULL,
    content TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO users (username, email) VALUES
    ('admin', 'admin@example.com'),
    ('user1', 'user1@example.com');
EOF

echo "Database setup complete!"

Configuration File Generator

#!/bin/bash

# Generate Nginx config
SERVER_NAME="example.com"
BACKEND_PORT="3000"

cat > /etc/nginx/sites-available/${SERVER_NAME} << EOF
server {
    listen 80;
    server_name ${SERVER_NAME};

    location / {
        proxy_pass http://localhost:${BACKEND_PORT};
        proxy_http_version 1.1;
        proxy_set_header Upgrade \$http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host \$host;
        proxy_cache_bypass \$http_upgrade;
    }

    location /static/ {
        alias /var/www/${SERVER_NAME}/static/;
        expires 30d;
    }
}
EOF

ln -s /etc/nginx/sites-available/${SERVER_NAME} /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Deployment Script

#!/bin/bash

DEPLOY_USER="deploy"
DEPLOY_HOST="prod-server.com"
APP_NAME="myapp"

# Deploy to remote server
ssh ${DEPLOY_USER}@${DEPLOY_HOST} << 'ENDSSH'
cd /var/www/myapp
git pull origin main

# Install dependencies
npm install --production

# Run migrations
npm run migrate

# Restart application
pm2 restart myapp

# Clear cache
redis-cli FLUSHALL

echo "Deployment completed at $(date)"
ENDSSH

echo "Application deployed successfully!"

Advanced Techniques

Heredoc to Variable

# Store heredoc in variable
read -r -d '' SQL_QUERY << EOF
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id
ORDER BY post_count DESC
LIMIT 10;
EOF

# Use the variable
echo "$SQL_QUERY" | psql -U user database

Heredoc with Sudo

# Write to privileged location
sudo tee /etc/sysctl.conf > /dev/null << EOF
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
vm.swappiness = 10
EOF

# Or use sudo with sh
sudo sh << 'EOF'
echo "Europe/London" > /etc/timezone
ln -sf /usr/share/zoneinfo/Europe/London /etc/localtime
EOF

Piping Heredoc

# Process heredoc with pipeline
cat << EOF | grep -i error | wc -l
INFO: Starting application
ERROR: Connection failed
DEBUG: Retrying...
ERROR: Database timeout
INFO: Shutting down
EOF

# Sort heredoc content
sort << EOF
banana
apple
cherry
date
EOF

Functions with Heredoc

create_user_script() {
    local username=$1
    
    cat > "setup_${username}.sh" << EOF
#!/bin/bash
useradd -m ${username}
mkdir -p /home/${username}/.ssh
chmod 700 /home/${username}/.ssh
chown -R ${username}:${username} /home/${username}
echo "${username} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${username}
EOF
    
    chmod +x "setup_${username}.sh"
    echo "Created setup script for ${username}"
}

create_user_script "developer"

HTML/XML Generation

#!/bin/bash

# Generate HTML report
TITLE="System Report"
DATE=$(date)

cat > report.html << EOF



    ${TITLE}
    


    

${TITLE}

Generated: ${DATE}

Disk Usage

$(df -h)

Memory Usage

$(free -h)

Top Processes

$(ps aux | head -10)
EOFecho "Report generated: report.html"

Testing and Debugging

# Create test data
cat > test_data.json << 'EOF'
{
    "users": [
        {"id": 1, "name": "Alice"},
        {"id": 2, "name": "Bob"}
    ],
    "settings": {
        "theme": "dark",
        "language": "en"
    }
}
EOF

# Verify content
cat test_data.json | jq .

# Create test script
cat > test.sh << 'EOF'
#!/bin/bash
set -e
echo "Running tests..."
npm test
echo "Tests passed!"
EOF

chmod +x test.sh

Common Patterns

# Email with body
mail -s "Subject" user@example.com << EOF
Email body here
Multiple lines
EOF

# Create systemd service
sudo tee /etc/systemd/system/myapp.service << EOF
[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node server.js
Restart=always

[Install]
WantedBy=multi-user.target
EOF

# Cron job configuration
crontab << EOF
0 2 * * * /backup/daily.sh
0 0 * * 0 /backup/weekly.sh
EOF

Pro Tips

  • Quote delimiter for literal text: Use 'EOF' to prevent expansion
  • Use meaningful delimiters: SQL, HTML, JSON instead of just EOF
  • Indent with tabs: Only when using <<- operator
  • Escape special chars: Use backslash for $ when needed
  • Check output: Always verify generated files

Common Mistakes

# Wrong - delimiter must be alone on line
cat << EOF
content
    EOF  # Has spaces/tabs

# Wrong - delimiter mismatch
cat << EOF
content
eof  # Different case

# Wrong - spaces in heredoc operator
cat < < EOF  # Should be <<

# Correct
cat << EOF
content
EOF

When to Use Heredoc

Use heredoc when:

  • Creating multi-line files
  • Sending complex input to commands
  • Generating configuration files
  • Writing SQL queries
  • Creating HTML/XML documents
  • Running remote commands via SSH

Don't use heredoc when:

  • Single line output (use echo)
  • File already exists (use cat file)
  • Need complex logic (use proper script)

Alternative: Here Strings

For single lines, use here strings (<<<):

# Here string (single line)
grep "pattern" <<< "search this text"

# Compare to heredoc
grep "pattern" << EOF
search this text
EOF

# Useful for quick input
bc <<< "scale=2; 10/3"
# Output: 3.33

Complete Example: Server Setup

#!/bin/bash

# Comprehensive server setup script
HOSTNAME="web-server-01"
DOMAIN="example.com"

echo "Setting up server: ${HOSTNAME}"

# Update hostname
sudo hostnamectl set-hostname ${HOSTNAME}

# Configure hosts file
sudo tee -a /etc/hosts << EOF
127.0.0.1 ${HOSTNAME}
EOF

# Install packages
sudo apt-get update
sudo apt-get install -y nginx postgresql redis-server

# Configure Nginx
sudo tee /etc/nginx/sites-available/${DOMAIN} << 'EOF'
server {
    listen 80;
    server_name ${DOMAIN} www.${DOMAIN};
    
    location / {
        proxy_pass http://localhost:3000;
    }
}
EOF

# Create application user
sudo useradd -m -s /bin/bash appuser

# Setup PostgreSQL
sudo -u postgres psql << EOF
CREATE USER appuser WITH PASSWORD 'secure_pass';
CREATE DATABASE appdb OWNER appuser;
EOF

# Configure systemd service
sudo tee /etc/systemd/system/myapp.service << EOF
[Unit]
Description=My Application
After=network.target postgresql.service

[Service]
Type=simple
User=appuser
WorkingDirectory=/home/appuser/app
ExecStart=/usr/bin/node server.js
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable myapp.service

echo "Server setup complete!"

Conclusion

Here documents are one of the shell's most elegant features for handling multi-line text. They make your scripts cleaner, more readable, and easier to maintain. Whether you're generating configuration files, sending complex input to commands, or running remote operations, heredocs simplify the task dramatically.

Master heredocs, and you'll never go back to messy echo chains again!

References

Written by:

441 Posts

View All Posts
Follow Me :