You want to pass a multiline config or SQL statement to a command in bash. You could use echo with escapes and multiple lines, which is unreadable garbage. Or you could use a here document, which is clean and readable.
Here documents (<<EOF) and here strings (<<<) are bash features that solve the “pass text to a command” problem. Most people don’t know they exist. Here’s the thing: once you use them, you’ll never go back to chaining echo commands.
Here Documents: The Basics
A here document passes multiline text to a command:
#!/bin/bash
cat <<EOFThis is line 1This is line 2This is line 3EOF$ bash simple_heredoc.shThis is line 1This is line 2This is line 3The <<EOF starts the here document. Everything until the closing EOF is passed as input to cat. No escaping needed.
You can redirect to any command:
#!/bin/bash
wc -l <<EOFLine 1Line 2Line 3EOF$ bash heredoc_to_command.sh 3Or to a file:
#!/bin/bash
cat > /tmp/config.txt <<EOFserver { listen 80; server_name example.com;}EOF$ bash heredoc_to_file.sh$ cat /tmp/config.txtserver { listen 80; server_name example.com;}The Indentation Problem: <<-EOF
Here documents usually start at column 0. If your script is indented (inside a function or if block), it looks weird:
#!/bin/bash
if true; thencat <<EOFThis textstarts at column 0EOFfiThe closing EOF has to be at column 0, which breaks indentation. Use <<-EOF instead:
#!/bin/bash
if true; then cat <<-EOF This text can be indented EOFfiWith <<-, the closing delimiter can be indented (tabs are stripped). Much cleaner.
Suppressing Variable Expansion
By default, variables in here documents are expanded:
#!/bin/bash
name="Alice"cat <<EOFHello $nameEOF$ bash expand_vars.shHello AliceIf you want literal $name, quote the opening delimiter:
#!/bin/bash
name="Alice"cat <<'EOF'Hello $nameEOF$ bash no_expand.shHello $nameThe quotes tell bash: don’t expand variables in this here document.
Piping Here Documents
Pipe the output to another command:
#!/bin/bash
cat <<EOF | grep "test"This is a testNot a matchAnother testEOF$ bash pipe_heredoc.shThis is a testAnother testOr send to a command that reads stdin:
#!/bin/bash
mysql -u root -p <<EOFCREATE DATABASE newdb;USE newdb;CREATE TABLE users (id INT, name VARCHAR(255));EOFNo interactive prompts, clean SQL.
Here Strings: Single-Line Version
For single lines, use here strings (<<<):
#!/bin/bash
wc -w <<<'hello world test'$ bash heredoc_string.sh 3That’s equivalent to:
$ echo 'hello world test' | wc -w 3But cleaner. No forking an echo process.
With variables:
#!/bin/bash
text="hello world"grep "hello" <<<$textHere strings are useful for passing data to commands that expect input without spawning echo.
Real-World Example 1: Config File Generation
#!/bin/bash
domain="example.com"email="admin@example.com"
cat > /etc/nginx/sites-available/$domain <<-EOF server { listen 80; server_name $domain;
location / { proxy_pass http://localhost:3000; }
# Auto-generated: $(date) # Maintainer: $email }EOF
echo "Config created for $domain"The config is readable, variables are interpolated, and it’s all in one block.
Real-World Example 2: Database Setup
#!/bin/bash
DB_USER="app"DB_PASS="secure_password"DB_NAME="myapp"
mysql -u root -p<<-EOF CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS'; CREATE DATABASE $DB_NAME; GRANT ALL PRIVILEGES ON $DB_NAME.* TO '$DB_USER'@'localhost'; FLUSH PRIVILEGES;EOF
echo "Database and user created"Clean, readable SQL. No escape nightmare.
Real-World Example 3: Multiline Variables
Store multiline text in a variable:
#!/bin/bash
readme=$(cat <<-EOF # My Project
This is a readme file. It has multiple lines. All stored in a variable.EOF)
echo "$readme"$ bash multiline_var.sh# My Project
This is a readme file.It has multiple lines.All stored in a variable.Difference: EOF vs Other Delimiters
You don’t have to use EOF. Any word works:
#!/bin/bash
cat <<SCRIPT#!/bin/bashecho "This is bash code"SCRIPTUse a different delimiter if the content contains EOF:
#!/bin/bash
cat <<'MARKER'This contains EOF but won't be confusedwith the actual EOF delimiterMARKERSuppressing Trailing Newline
By default, here documents add a trailing newline. To avoid it, use read:
#!/bin/bash
result=$(cat <<EOFno newline at endEOF)
# But this adds a newline from the variable expansion# To truly remove it, use read -d ''
read -d '' result <<EOFno newlineEOF
echo "Result length: ${#result}"Usually not worth the complexity, but it’s there if you need it.
The Pattern: Use Heredocs for Readability
BAD:
echo "server {" > config.txtecho " listen 80;" >> config.txtecho " server_name example.com;" >> config.txtecho "}" >> config.txtGOOD:
cat > config.txt <<-EOFserver { listen 80; server_name example.com;}EOFThe second is readable. The first is unmaintainable garbage.
The One Thing
Next time you want to write multiline text in bash, use:
#!/bin/bash
# For multiline with variable expansioncat <<-EOF Your content here Variable: $VAR Can span linesEOF
# For multiline without expansioncat <<-'EOF' Your content here Literal: $VAR (not expanded) Can span linesEOF
# For single linecommand <<<'your input here'That’s your toolkit. Use it, and your scripts become readable instead of noise.