2024-09-18
7 min read

How Does cat << EOF Work in Bash?

How Does cat << EOF Work in Bash?

You've seen scripts with cat << EOF followed by multiple lines of text and ending with EOF. What is this syntax, and how does it work?

TL;DR

cat << EOF is a heredoc (here document) - a way to pass multiple lines of input to a command in Bash. The << redirects the following lines as input until it reaches the delimiter (EOF in this case). You can use any delimiter word, not just EOF. Variables expand inside heredocs by default, or use << 'EOF' with quotes to prevent expansion.

A heredoc lets you embed multi-line content directly in your script without needing separate files or complex quoting.

Here's a basic example:

cat << EOF
This is line 1
This is line 2
This is line 3
EOF

This prints:

This is line 1
This is line 2
This is line 3

The cat command receives all the lines between << EOF and the closing EOF as input, then outputs them.

Understanding the Syntax

Let's break down cat << EOF:

  • cat - the command that receives input
  • << - the heredoc operator (redirection)
  • EOF - the delimiter marking where input starts and ends
cat << EOF           <- Start of heredoc
text here            <- Input to cat
more text            <- More input
EOF                  <- End delimiter (must be on its own line)

The delimiter EOF (End Of File) is conventional but arbitrary. You can use any word:

cat << STOP
This works too
STOP

cat << MYDELIMITER
So does this
MYDELIMITER

The closing delimiter must be on a line by itself with no leading spaces (unless you use <<-, covered later).

Creating Files with Heredocs

A common use is creating files with multiple lines:

cat << EOF > config.txt
server_name=localhost
port=8080
debug=true
EOF

This creates config.txt with those three lines. It's cleaner than using multiple echo statements:

# Without heredoc (verbose)
echo "server_name=localhost" > config.txt
echo "port=8080" >> config.txt
echo "debug=true" >> config.txt

# With heredoc (cleaner)
cat << EOF > config.txt
server_name=localhost
port=8080
debug=true
EOF

Variable Expansion in Heredocs

Variables expand inside heredocs by default:

NAME="Alice"
PORT=8080

cat << EOF
Hello, $NAME!
Server is running on port $PORT
EOF

Output:

Hello, Alice!
Server is running on port 8080

Command substitution works too:

cat << EOF
Current directory: $(pwd)
Current user: $(whoami)
Date: $(date +%Y-%m-%d)
EOF

Preventing Variable Expansion

To treat content literally (no variable expansion), quote the delimiter:

NAME="Alice"

cat << 'EOF'
Hello, $NAME!
This will print literally: $NAME
EOF

Output:

Hello, $NAME!
This will print literally: $NAME

Note the quotes around 'EOF' - they prevent expansion.

You can use single or double quotes:

cat << "EOF"
$NAME won't expand here either
EOF

Using <<- to Ignore Leading Tabs

If you're indenting heredoc content in a script for readability, use <<- instead of <<:

#!/bin/bash

if true; then
    cat <<- EOF
	This is indented with a tab
	Another line with a tab
	EOF
fi

The <<- strips leading tab characters (not spaces) from each line, including the delimiter. This lets you indent the heredoc to match your code structure.

Important: This only works with tabs, not spaces. If you use spaces, they'll appear in the output.

Passing Heredocs to Other Commands

Any command that reads from stdin can use heredocs:

# Pass multi-line input to grep
grep "error" << EOF
This is a log entry
An error occurred here
Another normal entry
EOF

Output:

An error occurred here

Passing to mysql:

mysql -u root -p << EOF
CREATE DATABASE myapp;
USE myapp;
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(100));
EOF

Passing to ssh:

ssh user@server << EOF
cd /var/www
ls -la
df -h
EOF

The remote commands execute on the server, with their output shown locally.

Assigning Heredocs to Variables

You can capture heredoc content in a variable:

CONFIG=$(cat << EOF
server=localhost
port=8080
debug=true
EOF
)

echo "$CONFIG"

This stores the multi-line string in $CONFIG. Note the quotes when echoing to preserve newlines.

Another syntax without cat:

read -r -d '' CONFIG << EOF
server=localhost
port=8080
debug=true
EOF

echo "$CONFIG"

The read -r -d '' reads until it hits the null delimiter, effectively capturing everything until EOF.

Heredocs in Functions

Heredocs work inside functions:

create_config() {
    cat << EOF > "$1"
# Configuration file generated on $(date)
server_name=$2
port=$3
EOF
}

# Usage
create_config "app.conf" "localhost" 8080

This function creates a config file with parameters passed as arguments.

Appending with Heredocs

Use >> to append instead of overwrite:

# Create initial file
cat << EOF > log.txt
Application started
EOF

# Append more content
cat << EOF >> log.txt
User logged in
Database connected
EOF

Multi-line Comments with Heredocs

A clever trick for multi-line comments in Bash:

: << 'COMMENT'
This is a multi-line comment
It won't be executed
Useful for temporarily disabling code blocks
COMMENT

echo "This will run"

The : command does nothing (it's a no-op), and the heredoc is passed to it but ignored.

Practical Example: Generating HTML

Creating an HTML file with dynamic content:

#!/bin/bash

TITLE="My Web Page"
DATE=$(date +%Y-%m-%d)

cat << EOF > index.html
<!DOCTYPE html>
<html>
<head>
    <title>$TITLE</title>
</head>
<body>
    <h1>$TITLE</h1>
    <p>Generated on: $DATE</p>
    <p>Welcome to my website!</p>
</body>
</html>
EOF

echo "HTML file created: index.html"

Practical Example: Docker Compose File

Generating a Docker Compose file with environment-specific values:

#!/bin/bash

APP_PORT=3000
DB_PASSWORD=$(openssl rand -base64 32)

cat << EOF > docker-compose.yml
version: '3.8'

services:
  app:
    image: myapp:latest
    ports:
      - "$APP_PORT:3000"
    environment:
      - DB_PASSWORD=$DB_PASSWORD
      - NODE_ENV=production

  database:
    image: postgres:14
    environment:
      - POSTGRES_PASSWORD=$DB_PASSWORD
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:
EOF

echo "Docker Compose file created with random password"

Practical Example: Email Template

Sending emails with templates:

#!/bin/bash

USER_NAME="Alice"
RESET_LINK="https://example.com/reset?token=abc123"

mail -s "Password Reset" [email protected] << EOF
Hello $USER_NAME,

You requested a password reset. Click the link below:

$RESET_LINK

This link expires in 24 hours.

If you didn't request this, please ignore this email.

Best regards,
The Support Team
EOF

Heredocs with Pipes

You can pipe heredoc output to other commands:

cat << EOF | grep -v "^#" | sort
# This is a comment
zebra
apple
# Another comment
banana
EOF

This filters out comments and sorts the remaining lines.

Common Pitfalls

The closing delimiter must be on its own line with no extra characters:

# Wrong - has spaces before EOF
cat << EOF
text
    EOF

# Wrong - has text after EOF
cat << EOF
text
EOF

# Correct
cat << EOF
text
EOF

Variables in the delimiter don't expand:

DELIM="END"

# This looks for literal EOF, not the value of $DELIM
cat << $DELIM
text
EOF  # This won't work

# You must use the actual delimiter
cat << EOF
text
EOF

Why Use Heredocs?

Heredocs are great when you need to:

  • Embed multi-line text in scripts without external files
  • Generate configuration files with variable substitution
  • Pass complex input to commands
  • Improve script readability compared to multiple echo statements
  • Avoid quoting nightmares with special characters

For simple cases, echo might be clearer:

# For single lines, echo is fine
echo "server=localhost" > config.txt

# For multiple lines with variables, heredoc is cleaner
cat << EOF > config.txt
server=localhost
port=$PORT
debug=true
EOF

The cat << EOF syntax (heredocs) is a powerful feature for embedding multi-line text in Bash scripts. Whether you're creating configuration files, generating code, or passing complex input to commands, heredocs make your scripts cleaner and more maintainable than alternatives.

Published: 2024-09-18|Last updated: 2024-09-18T09:00:00Z

Found an issue?