
If you have ever written a Bash script that uses five consecutive echo commands just to print a simple multi-line message, you already know the pain. The code looks cluttered, it is hard to maintain, and fixing a single typo means hunting through a stack of quoted strings. Bash Heredoc solves this problem cleanly and efficiently. This guide covers everything you need to know about Bash Heredoc, from basic syntax to advanced real-world use cases that sysadmins rely on daily.
Prerequisites
Before diving in, make sure you have the following ready:
- A Linux system running Ubuntu 20.04+, Debian 11+, CentOS 7+, or any modern distro
- Bash version 4.x or higher (check with
bash --version) - Basic familiarity with the Linux terminal and command line
- A text editor such as
nano,vim, or VS Code with a remote extension - Sudo or root access is not required for most examples, but some file operations may need it
What Is a Bash Heredoc?
A Here Document, commonly called a Heredoc, is a special type of input redirection built into Bash. It lets you pass a block of multi-line text directly to a command without writing to an external file first.
Think of it as an inline file that lives inside your script. You write the content between two matching delimiter tokens, and Bash feeds everything in between to whatever command you specify.
Heredoc works with any command that reads from standard input, including cat, ssh, mysql, python3, tee, and many others. This makes it one of the most versatile tools in shell scripting.
When Should You Use a Bash Heredoc?
Use a Heredoc when you need to:
- Generate configuration files dynamically inside a script
- Pass multiple commands to a remote SSH session at once
- Feed multi-line SQL queries to a database client
- Embed a Python or Perl snippet inside a Bash script
- Create readable block-style comments for documentation
If the task involves two or more lines of structured text going into a command, a Heredoc almost always produces cleaner code than chained echo statements.
Step 1: Understand Bash Heredoc Syntax
Before you write your first Heredoc, you need to understand its structure. The syntax looks like this:
command << DELIMITER
line one
line two
DELIMITER
Every Heredoc has three core parts:
command: Any command that accepts standard input, such ascatortee<<: The redirection operator that tells Bash a Heredoc is comingDELIMITER: A token you choose to mark where the block starts and ends
| Component | Purpose |
|---|---|
command |
Receives the block as stdin |
<< |
Heredoc redirection operator |
DELIMITER (opening) |
Marks the start of the content block |
| Content lines | The actual text or commands being passed |
DELIMITER (closing) |
Marks the end of the block, must be alone on its line |
Rules for the Delimiter
The delimiter is just a word you pick. Here are the rules that apply to it:
- It must be a single word with no spaces
- It is case-sensitive:
EOFandeofare different tokens - The closing delimiter must appear on its own line with no leading whitespace
- Common choices are
EOF,END, andEOT, but descriptive names likeNGINX_CONFIGorSQL_QUERYare better for readability
Your First Heredoc Example
Open your terminal and type this:
cat << EOF
Hello, Linux user.
Today's date is: $(date +%Y-%m-%d)
Your username is: $USER
EOF
Expected output:
Hello, Linux user.
Today's date is: 2026-04-20
Your username is: yourname
Bash expanded both $(date +%Y-%m-%d) and $USER automatically. This is the default behavior when the delimiter is unquoted.
Step 2: Control Variable Expansion in Bash Heredoc Setup
One of the most important decisions you make with a Heredoc is whether to allow variable expansion inside the block. Bash gives you full control over this behavior, and understanding it prevents a lot of confusing bugs.
Unquoted Delimiter: Expansion Enabled
When you write << EOF without quotes, Bash treats the content just like a regular double-quoted string. Variables expand, command substitutions run, and special characters are interpreted.
NAME="Alice"
cat << EOF
Hello, $NAME.
Current directory: $PWD
Script PID: $$
EOF
Expected output:
Hello, Alice.
Current directory: /home/alice
Script PID: 12345
Use this when your Heredoc content should reflect the current state of the environment.
Quoted Delimiter: Expansion Disabled
Wrap the delimiter in single quotes to treat every character inside the block as a literal string. Nothing expands.
cat << 'EOF'
Hello, $NAME.
This variable will NOT expand: $PWD
Command substitution won't run: $(whoami)
EOF
Expected output:
Hello, $NAME.
This variable will NOT expand: $PWD
Command substitution won't run: $(whoami)
This is the right choice when your Heredoc contains code meant for another language or runtime, such as a Python script, a Dockerfile, or a Kubernetes YAML template.
Escaping Individual Variables
You do not always need to quote the entire delimiter. If you want most variables to expand but need to protect a specific one, prefix it with a backslash:
SERVER="web01"
cat << EOF
Deploying to: $SERVER
Remote home dir: \$HOME
EOF
$SERVER expands locally. \$HOME passes through as a literal string.
Step 3: Use Tab Suppression with <<-
When you write a Heredoc inside a function, loop, or conditional block, you naturally indent your code for readability. Without tab suppression, those leading tabs get included in the output, which is rarely what you want.
The <<- operator strips all leading tab characters from both the content lines and the closing delimiter.
#!/bin/bash
generate_message() {
cat <<- EOF
Deployment started.
Server: $HOSTNAME
Time: $(date)
EOF
}
generate_message
Expected output (no leading tabs):
Deployment started.
Server: myserver
Time: Mon Apr 20 17:00:00 WIB 2026
Critical Note on Tabs vs. Spaces
This trips up almost every developer the first time. The <<- operator strips tabs only. If your text editor automatically converts tabs to spaces, the suppression will not work and Bash will throw an error or include the spaces in the output.
To verify you are using real tabs in your script, run:
cat -A yourscript.sh
Real tab characters show as ^I in the output. Spaces show as regular spaces. If you see spaces where you expect tabs, switch your editor’s indentation setting for that file.
Step 4: Redirect Heredoc Output to Files
So far, all examples have printed to the terminal. In real sysadmin work, you often need to write Heredoc output directly to a file, which is where it becomes genuinely powerful for configure Bash Heredoc use cases.
Overwrite a File
Use > after the command to write the output to a file, creating it if it does not exist or overwriting it if it does:
cat << EOF > /tmp/welcome.txt
Welcome to the server.
Hostname: $HOSTNAME
Generated: $(date)
EOF
Append to a File
Use >> to add content to the end of an existing file without erasing what is already there:
cat << EOF >> /var/log/deploy.log
[$(date +%Y-%m-%d %H:%M:%S)] Deployment complete.
Version: 2.4.1
EOF
Generate a Config File Dynamically
Here is a practical example of generating an Nginx server block inside a Linux server tutorial workflow:
DOMAIN="example.com"
WEBROOT="/var/www/$DOMAIN"
cat << NGINX_CONFIG > /etc/nginx/conf.d/$DOMAIN.conf
server {
listen 80;
server_name $DOMAIN www.$DOMAIN;
root $WEBROOT;
index index.html index.php;
access_log /var/log/nginx/${DOMAIN}_access.log;
error_log /var/log/nginx/${DOMAIN}_error.log;
}
NGINX_CONFIG
This single block replaces a manual config editing process and is fully repeatable inside a deployment script.
Step 5: Real-World Use Cases for Bash Heredoc
This is where Bash Heredoc earns its place in any serious scripter’s toolkit. These are the patterns most commonly used in production environments.
Running Multiple Commands Over SSH
Executing a series of remote commands one by one is slow and error-prone. Heredoc lets you batch them cleanly:
ssh deploy@192.168.1.10 << EOF
cd /var/www/app
git pull origin main
npm install --production
pm2 restart app
echo "Deployment finished on \$(hostname)"
EOF
Note the escaped \$(hostname). The backslash prevents local expansion so the command runs on the remote server instead.
Automating Database Operations
Feed multi-line SQL queries to MySQL without writing a separate .sql file:
DB_NAME="production"
DB_USER="admin"
mysql -u $DB_USER -p$DB_PASS $DB_NAME << SQL_QUERY
CREATE TABLE IF NOT EXISTS audit_log (
id INT AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO audit_log (action) VALUES ('schema_init');
SQL_QUERY
Embedding Python Inside Bash
When you need quick data processing inside a Bash script but do not want to create a separate .py file, embed Python directly:
LOG_FILE="/var/log/app.log"
python3 << PYTHON_SCRIPT
with open("$LOG_FILE") as f:
errors = [l for l in f if "ERROR" in l]
print(f"Total errors found: {len(errors)}")
PYTHON_SCRIPT
Creating Block-Style Comments
Bash does not support multi-line comments natively. Redirect a Heredoc to the null operator : to create one:
: << 'BLOCK_COMMENT'
This section handles database migration.
Author: ops-team
Last updated: 2026-04-20
Warning: Do not run this more than once per release.
BLOCK_COMMENT
Bash skips this block entirely at runtime. It serves purely as documentation inside the script.
Piping Heredoc Output to Another Command
You can pipe a Heredoc directly into tools like sed, awk, or grep:
cat << 'EOF' | sed 's/staging/production/g'
DB_HOST=staging-db.internal
APP_ENV=staging
API_URL=https://staging.example.com
EOF
Expected output:
DB_HOST=production-db.internal
APP_ENV=production
API_URL=https://production.example.com
Step 6: Advanced Bash Heredoc Techniques
Once you are comfortable with the basics, these techniques give you more control and flexibility in complex scripts.
Here-Strings: The Compact Alternative
A here-string (<<<) is a single-line cousin of Heredoc. It passes one string to a command without needing a delimiter block:
# Heredoc version
cat << EOF
Hello World
EOF
# Here-string version
cat <<< "Hello World"
Use here-strings for quick, single-line input. Use Heredoc when the content spans multiple lines or needs more structure.
Heredoc Inside a Loop
Generate multiple config files in a loop, using <<- for clean indentation:
#!/bin/bash
for SITE in site1 site2 site3; do
cat <<- CONF > /etc/nginx/conf.d/$SITE.conf
server {
listen 80;
server_name $SITE.example.com;
root /var/www/$SITE;
}
CONF
echo "Config created for $SITE"
done
Writing Portable Heredocs
If your script needs to run on multiple distros or non-Bash shells like dash, follow these rules:
- Quote the delimiter (
'EOF') to avoid relying on Bash-specific expansion behavior - Avoid
$((arithmetic))and process substitution inside the Heredoc body - Test with
dash yourscript.shon Debian-based systems to catch compatibility issues early
Troubleshooting Common Bash Heredoc Errors
Even experienced scripters run into these issues. Here are the most common ones and how to fix them fast.
Error 1: syntax error: unexpected end of file
Cause: The closing delimiter has whitespace before it, or you used spaces instead of tabs with <<-.
Fix: Make sure the closing delimiter sits at the very beginning of its line with zero indentation. If using <<-, use real tab characters, not spaces.
# Wrong
cat << EOF
Hello
EOF
# Correct
cat << EOF
Hello
EOF
Error 2: Variables Are Not Expanding
Cause: You accidentally quoted the delimiter when you wanted expansion.
Fix: Remove the quotes from the opening delimiter.
# Wrong — no expansion
cat << 'EOF'
Hello $USER
EOF
# Correct — expansion works
cat << EOF
Hello $USER
EOF
Error 3: <<- Not Removing Indentation
Cause: Your editor replaced tab characters with spaces.
Fix: Force tab characters in your editor for the indented lines. In Vim, use :set noexpandtab before writing the Heredoc lines. In VS Code, open the command palette and select “Indent Using Tabs.”
Error 4: SSH Heredoc Running Commands Locally Instead of Remotely
Cause: Variables inside the SSH Heredoc are expanding locally before SSH sends them.
Fix: Escape the variables you want to run on the remote machine with a backslash:
ssh user@host << EOF
echo "Remote hostname: \$(hostname)"
EOF
Error 5: File Not Created When Redirecting to Root-Owned Paths
Cause: Bash runs redirection as the current user. Writing to /etc/ requires elevated privileges, and sudo cat << EOF > /etc/file does not work the way most people expect.
Fix: Use tee with sudo instead:
sudo tee /etc/myapp/config.conf << EOF
[settings]
debug=false
log_level=info
EOF
sudo tee runs with elevated privileges and writes the Heredoc content correctly to protected paths.