14.2. Here-Documents#

14.2.1. Common Pitfalls#

1. Variable expansion in quoted heredocs

# Bad: Variables don't expand with 'EOF'
cat << 'EOF'
User: $USER
Home: $HOME
EOF
# Output: User: $USER (literal)

# Good: Use unquoted delimiter
cat << EOF
User: $USER
Home: $HOME
EOF
# Output: User: alice (expanded)

2. Indentation with heredoc

# Bad: Leading spaces are part of content
if true; then
  cat << 'EOF'
  First line
  Second line
EOF
fi
# Output has leading spaces

# Good: Use <<- with tabs
if true; then
  cat <<- 'EOF'
  First line
  Second line
EOF
fi
# Leading tabs removed, but only tabs (not spaces)

3. Special characters in here-strings

# Bad: Unquoted string breaks parsing
grep "error" <<< $error_message  # May break if $error_message has spaces

# Good: Quote the variable
grep "error" <<< "$error_message"

4. Escape sequences in heredocs

# Heredocs don't interpret escape sequences
cat << 'EOF'
Line 1\nLine 2  # Outputs literal \n, not newline
EOF

# Use echo -e or explicit newlines
echo -e "Line 1\nLine 2"
# Or:
cat << 'EOF'
Line 1
Line 2
EOF

5. Mixing quotes and expansion

# Bad: Variable with special chars breaks
password="a'b\"c"
cat << EOF
password=$password  # May break
EOF

# Good: Escape or avoid
cat << 'EOF'
password='a'"'"'b"c'  # Properly escaped
EOF
# Or just use:
password='a'"'"'b"c'
cat << EOF
password=$password
EOF

14.2.2. Here-Strings and Advanced String Handling#

14.2.2.1. Here-Strings (<<<)#

#!/bin/bash

# Here-string: pass string as stdin to command
wc -w <<< "hello world from bash"  # Output: 4

# Parse variables
name="Alice"
grep "$name" <<< "Alice Bob Charlie
Alice Smith
Dave"

# Here-string to variable
response=$(cat <<< "OK")

# Process multi-line string
data="line1
line2
line3"

# Count lines
wc -l <<< "$data"

# Filter with while
read -r first_line rest <<< "the rest of the line"
echo "First: $first_line"
echo "Rest: $rest"

14.2.2.2. String Manipulation#

#!/bin/bash

# Remove patterns (used in heredocs)
text="The quick brown fox"

# Remove prefix
echo "${text#The }"  # quick brown fox

# Remove suffix
echo "${text%fox}"  # The quick brown 

# Replace all occurrences
echo "${text//o/_}"  # The quick brwn f_x

# Extract substring
echo "${text:4:5}"  # quick

# Length
echo "${#text}"  # 19

# Case conversion (Bash 4+)
echo "${text^^}"  # THE QUICK BROWN FOX
echo "${text,,}"  # the quick brown fox

# Trim whitespace
text="  hello  world  "
text="${text#"${text%%[![:space:]]*}"}"  # Trim leading
text="${text%"${text##*[![:space:]]}"}"  # Trim trailing

14.2.2.3. Formatting with Here-Documents#

#!/bin/bash

# Create formatted output
print_table() {
  local -a headers=("ID" "Name" "Status")
  local -a rows=(
    "1:Alice:Active"
    "2:Bob:Inactive"
    "3:Charlie:Active"
  )
  
  # Create table with heredoc
  {
    # Headers
    printf "%-5s %-10s %-10s\n" "${headers[@]}"
    printf "%0.s=" {1..25}
    echo
    
    # Rows
    while IFS=: read -r id name status; do
      printf "%-5s %-10s %-10s\n" "$id" "$name" "$status"
    done <<< "$(printf '%s\n' "${rows[@]}")"
  }
}

print_table

# Email composition
compose_email() {
  local recipient=$1
  local subject=$2
  
  cat << EOF | sendmail "$recipient"
Subject: $subject
To: $recipient
From: system@example.com

$(date) - Automated notification

This is an automated email.
Please do not reply to this address.

Best regards,
System Administrator
EOF
}

14.2.3. Here-Documents (Heredoc)#

Here-documents allow embedding multi-line text directly in scripts, essential for templates and config generation.

14.2.3.1. Basic Heredoc Syntax#

#!/bin/bash

# Simple heredoc
cat << 'EOF'
This is a multi-line string
All lines between << 'EOF' and EOF are included literally
Variables are NOT expanded because of quotes around EOF
EOF

# Heredoc with variable expansion
TITLE="My Report"
cat << EOF
Report Title: $TITLE
Generated: $(date)
Current User: $USER
EOF

# Heredoc to variable
content=$(cat << 'EOF'
Line 1: apple
Line 2: banana
Line 3: cherry
EOF
)

echo "$content"

# Heredoc with indentation (using <<- DELIMITER)
function format_config() {
  cat <<- 'EOF'
    [database]
    host = localhost
    port = 5432
    user = postgres
  EOF
}

# Note: <<- allows leading tabs (not spaces) in the delimiter

14.2.3.2. Heredoc to Commands and Files#

#!/bin/bash

# Heredoc to command (stdin)
grep "error" << 'EOF'
warning: disk space low
error: connection failed
info: process started
error: timeout expired
EOF
# Outputs only error lines

# Heredoc to file
cat > /tmp/config.ini << 'EOF'
[settings]
debug=true
timeout=30
log_level=info
EOF

# Heredoc to multiple files
{
  cat > file1.txt << 'EOF'
Content for file 1
EOF

  cat > file2.txt << 'EOF'
Content for file 2
EOF
}

# Heredoc with pipes
cat << 'EOF' | while IFS=: read -r user pass uid gid name home shell; do
  echo "User: $user (UID: $uid)"
done
root:x:0:0:root:/root:/bin/bash
www-data:x:33:33::/var/www:/usr/sbin/nologin
EOF

14.2.3.3. Template Generation#

#!/bin/bash

# Generate HTML from heredoc
TITLE="Status Report"
TIMESTAMP=$(date)

cat > report.html << EOF
<!DOCTYPE html>
<html>
<head>
  <title>$TITLE</title>
</head>
<body>
  <h1>$TITLE</h1>
  <p>Generated: $TIMESTAMP</p>
  <p>Hostname: $(hostname)</p>
  <ul>
    <li>CPU Cores: $(nproc)</li>
    <li>Memory: $(free -h | grep Mem | awk '{print \$2}')</li>
  </ul>
</body>
</html>
EOF

# Generate SQL script
USER="backup"
TABLES="users,posts,comments"

cat > backup.sql << EOF
-- Backup script
-- Generated: $(date)
-- User: $(whoami)

FLUSH PRIVILEGES;
LOCK TABLES;

$(echo "$TABLES" | tr ',' '\n' | while read table; do
  echo "BACKUP TABLE $table TO '/backups/$table';"
done)

UNLOCK TABLES;
EOF