Bash
Scripting
Programming
Command Line
Linux

How does cat << EOF work in bash?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

cat << EOF is a Bash heredoc (short for "here document"). It feeds a block of text to a command on standard input until the shell encounters a line containing only the delimiter you chose. The shell handles all the parsing before cat ever runs. cat is just a convenient pass-through; the heredoc mechanism works with any command that reads from stdin.

Basic Syntax

A minimal heredoc looks like this:

bash
1cat << EOF
2Hello from a heredoc.
3This is the second line.
4EOF

The shell reads every line between the opening << EOF and the closing EOF marker, bundles them together, and pipes that text to cat on standard input. cat then prints it to stdout.

The structure is always the same: a command, the << operator, a delimiter word, then the body, then the delimiter again on its own line.

The Delimiter Is Not a Keyword

EOF is a convention, not something built into the shell. You can use any string as the delimiter:

bash
1cat << MYCONFIG
2server.port=8080
3server.host=0.0.0.0
4MYCONFIG
bash
1cat << ___
2Line one.
3Line two.
4___

The only rule is that the closing delimiter must appear exactly as written, alone on its line, with no leading or trailing characters (with one exception covered below).

Variable and Command Expansion

By default, heredoc content behaves like a double-quoted string. Shell variables expand, command substitutions execute, and backslash escapes are interpreted:

bash
1project="myapp"
2branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
3
4cat << EOF
5Deploying $project
6Current branch: $branch
7Timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
8EOF

Output:

text
Deploying myapp
Current branch: main
Timestamp: 2026-06-18T14:30:00Z

This makes heredocs powerful for generating configuration files, SQL scripts, or HTTP request bodies that incorporate runtime values.

Quoting the Delimiter Disables Expansion

When you quote the delimiter (single quotes, double quotes, or even a backslash escape), the shell treats the entire body as a literal string. No variable expansion, no command substitution:

bash
1cat << 'EOF'
2This $variable stays literal.
3So does $(this command).
4Backslashes \ are also literal.
5EOF

Output:

text
This $variable stays literal.
So does $(this command).
Backslashes \ are also literal.

This is essential when writing scripts that generate other scripts, Dockerfiles, or any content where dollar signs and backticks should appear verbatim.

All three quoting styles produce the same literal behavior:

bash
cat << 'EOF'    # single-quoted
cat << "EOF"    # double-quoted
cat << \EOF     # backslash-escaped

Redirecting to a File

Heredocs are frequently combined with output redirection to create files inline:

bash
1cat << 'EOF' > /etc/nginx/conf.d/app.conf
2server {
3    listen 80;
4    server_name app.example.com;
5    location / {
6        proxy_pass http://127.0.0.1:3000;
7    }
8}
9EOF

The shell sends the heredoc to cat on stdin, and cat's stdout is redirected to the file. You can use >> to append instead of overwrite.

This pattern appears constantly in setup scripts, Dockerfiles, and infrastructure automation.

Using Heredocs With Other Commands

Because the mechanism belongs to the shell, not to cat, heredocs work with any command that reads stdin:

bash
1# Feed SQL to psql
2psql -U admin -d mydb << EOF
3CREATE TABLE IF NOT EXISTS users (
4    id SERIAL PRIMARY KEY,
5    email VARCHAR(255) NOT NULL UNIQUE,
6    created_at TIMESTAMPTZ DEFAULT NOW()
7);
8EOF
bash
1# Feed Python code to the interpreter
2python3 << EOF
3import json
4data = {"status": "ok", "code": 200}
5print(json.dumps(data, indent=2))
6EOF
bash
1# Run commands on a remote host over SSH
2ssh deploy@prod << EOF
3cd /opt/myapp
4git pull origin main
5systemctl restart myapp
6EOF

Any command that reads from standard input can receive a heredoc. cat is simply the most common example because it acts as a transparent pass-through.

Tab Stripping With <<-

The <<- operator (note the hyphen) strips leading tab characters from the body and the closing delimiter. This lets you indent heredocs to match the surrounding script structure:

bash
1if [ "$env" = "production" ]; then
2	cat <<- EOF
3		database_host=db.prod.internal
4		database_port=5432
5	EOF
6fi

The output will not contain the leading tabs. There is a critical detail here: only hard tab characters are stripped, not spaces. If your editor converts tabs to spaces, the stripping silently stops working and the closing delimiter will not be recognized.

Heredoc vs Herestring

A herestring (<<<) is a related but simpler construct. It passes a single string to a command on stdin, without needing a delimiter:

bash
1# Herestring
2grep "error" <<< "this line has an error in it"
3
4# Equivalent heredoc (more verbose for single lines)
5grep "error" << EOF
6this line has an error in it
7EOF

Use heredocs for multi-line content. Use herestrings for single-line input.

Practical Example: Generating a Docker Compose File

Here is a realistic use case combining variable expansion and file creation:

bash
1#!/bin/bash
2APP_NAME="web-api"
3APP_PORT=8080
4DB_PASSWORD=$(openssl rand -hex 16)
5
6cat << EOF > docker-compose.yml
7services:
8  ${APP_NAME}:
9    build: .
10    ports:
11      - "${APP_PORT}:${APP_PORT}"
12    environment:
13      DATABASE_URL: "postgres://app:${DB_PASSWORD}@db:5432/appdb"
14    depends_on:
15      - db
16  db:
17    image: postgres:16-alpine
18    environment:
19      POSTGRES_PASSWORD: "${DB_PASSWORD}"
20    volumes:
21      - pgdata:/var/lib/postgresql/data
22
23volumes:
24  pgdata:
25EOF
26
27echo "Generated docker-compose.yml with DB password: ${DB_PASSWORD}"

The heredoc expands all variables at generation time, producing a complete compose file with a random password baked in.

Common Pitfalls

Indenting the closing delimiter with spaces. The closing marker must match exactly. If you add spaces before EOF, the shell never sees the end of the heredoc and appears to hang, waiting for more input. Use <<- with tabs if you need indentation.

Trailing whitespace on the delimiter line. Even invisible trailing spaces after EOF prevent the match. This is one of the hardest heredoc bugs to spot because the line looks correct visually.

Forgetting that unquoted delimiters expand variables. If your heredoc body contains $PATH or $(rm -rf /) and the delimiter is unquoted, the shell will expand or execute those expressions. Always quote the delimiter when writing literal content.

Assuming cat owns the syntax. Newcomers often think heredocs are a feature of cat. They are a shell feature. Removing cat and piping to another command works identically.

Mixing tabs and spaces with <<-. The tab-stripping operator only removes tabs. Editors that auto-convert to spaces will break both the stripping and the delimiter match without any visible indication.

Using heredocs inside functions without considering scope. Variables in an unquoted heredoc expand in the current shell scope. If a variable is unset, it silently expands to an empty string, which can produce broken configuration files.

Summary

  • cat << EOF is a shell heredoc that sends a block of text to a command on standard input.
  • The delimiter (EOF, END, or any string) is a user-chosen marker, not a keyword.
  • Unquoted delimiters allow variable and command expansion. Quoted delimiters ('EOF') keep content literal.
  • <<- strips leading tabs for readable indentation in scripts. Only tabs are stripped, never spaces.
  • Heredocs work with any command that reads stdin, not just cat.
  • The closing delimiter must appear alone on its line with no extra characters, including trailing spaces.
  • Use herestrings (<<<) for single-line input when a full heredoc is unnecessary.

Course illustration
Course illustration

All Rights Reserved.