The heredoc is the bash mechanism for embedding multi-line text in a script, and the pattern is the tool for generating configuration files, writing SQL to a database client, and passing multi-line input to commands. The syntax uses the << marker with a delimiter, and the delimiter terminates the text block. The article covers the quoted and unquoted forms, the variable expansion behavior, and the config generation pattern.
The basic syntax and the delimiter
The heredoc starts with << followed by a delimiter word and ends with the same delimiter on a line by itself:
cat > /etc/app/config.conf << EOF
server {
listen 8080;
root /srv/app;
}
EOF
The content between EOF markers is written to the file, and the delimiter can be any word, though EOF is the convention.
The quoted and unquoted forms
The quoted delimiter, << 'EOF', prevents variable expansion, and the content is passed literally. The unquoted delimiter, << EOF, expands variables, and the expansion is how the script embeds its own values into the generated config:
HOSTNAME=$(hostname)
cat > /etc/app/config.yml << EOF
server:
host: $HOSTNAME
port: 8080
EOF
The $HOSTNAME is expanded by the shell before the heredoc is passed to cat, and the expansion is the template rendering that the config generation uses.
The config generation pattern
The config generation is the pattern that the deployment scripts use: the script generates the config file with the environment-specific values, the generated file is the one the application reads, and the pattern is the alternative to the config management that the small deployments use. The heredoc is the tool for the generation, and the variables are the parameters.
The SQL execution with heredoc
The database client that reads from the heredoc executes the SQL that the script embeds:
psql -h db.internal -U admin << EOSQL
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP DEFAULT NOW()
);
EOSQL
and the heredoc carries the SQL without escaping, the multiline SQL that the single-line command cannot express cleanly.
The indentation with the dash suffix
The <<- form strips leading tabs from the content, and the indented heredoc preserves the script's indentation without the indentation appearing in the output. The tab must be a literal tab character, and the space indentation is not stripped.
The checklist
The heredoc pattern is the tool that the bash script uses for the multi-line output, and the checklist is short: use the quoted delimiter for literal text, the unquoted for variable expansion. Use the - form with tabs for indented scripts. Generate the config files with the unquoted form and the environment variables. And test the generated file before the production deployment, because the heredoc that generated the wrong config is the heredoc that broke the server.
The heredoc is the mechanism that the bash script uses for the multi-line text, and the mechanism is the tool that the config generation, the SQL execution and the command piping need. The heredoc's behavior with the variable expansion and the command substitution is the detail that the production scripts care about, and the detail is the difference between the template that renders correctly and the template that renders with the wrong values.
The heredoc with command substitution in the content
The unquoted heredoc expands both variables and command substitutions, and the command substitution inside the heredoc is the mechanism for the dynamic values:
cat > /etc/motd << EOF
Welcome to $(hostname)
Kernel: $(uname -r)
Last boot: $(uptime -s)
EOF
The $(hostname), $(uname -r) and $(uptime -s) commands execute before the heredoc is written, and the executed values are the dynamic content that the generated file carries. The command substitution inside the heredoc is the same mechanism as the command substitution outside, and the expansion is the shell's job rather than the tool's.
The heredoc with the ssh command for remote execution
The heredoc that the ssh command receives is the multi-line remote execution that the deployment script uses:
ssh admin@remote << 'EOF'
systemctl stop myapp
rm -rf /tmp/cache/*
systemctl start myapp
EOF
and the quoted delimiter prevents the local shell from expanding the variables, the remote commands that reference remote variables needing the quoted form. The heredoc over SSH is the mechanism that the deployment scripts use for the multi-step remote operations, and the mechanism is the alternative to the Ansible playbook for the simple cases.
The heredoc in the function arguments
The heredoc that a bash function reads uses the read command with the heredoc as the input:
process_config() {
local line
while read -r line; do
echo "processing: $line"
done
}
process_config << EOF
setting1=value1
setting2=value2
EOF
and the function reads the heredoc as its stdin, the pattern that the bash function uses for the multi-line input without a temp file.
The JSON and YAML generation with heredoc
The structured data that the script generates is the JSON or YAML that the API and the configuration system consume, and the heredoc generates the structured data with the variable interpolation:
cat > /tmp/deploy.json << EOF
{
"service": "$SERVICE_NAME",
"version": "$APP_VERSION",
"environment": "$DEPLOY_ENV",
"replicas": $REPLICA_COUNT
}
EOF
and the JSON that the heredoc generates is the valid JSON that the API accepts, the generation that the deployment pipeline uses for the API calls. The numbers in the JSON must not be quoted, and the variable expansion in the unquoted heredoc inserts the value without the quotes, the detail that the JSON generation cares about.
The tab-stripping with the dash suffix
The <<- form strips the leading tabs from the content, and the tab-stripped form allows the heredoc to be indented inside the script without the indentation appearing in the output:
if [ condition ]; then
cat > /tmp/config <<-
setting1=value1
setting2=value2
EOF
fi
The tabs before the content lines and the EOF delimiter are stripped, and the stripped form is the pattern that the nested scripts use. The limitation is that the stripping only works with tabs, not with spaces, and the tab-only stripping is the POSIX specification that the script must respect.
The heredoc with the sudo command
The heredoc that writes to a file requiring root permissions uses the sudo tee pattern:
cat << EOF | sudo tee /etc/sysctl.d/99-custom.conf
kernel.core_uses_pid = 1
fs.suid_dumpable = 0
EOF
and the tee command receives the heredoc through the pipe, the sudo that runs tee with the root privileges and the file that tee writes. The pattern is the one that every config deployment script uses, and the pattern is the reason that sudo tee exists as an idiom.
The security considerations for the heredoc
The unquoted heredoc expands the variables, and the variable expansion is the injection vector if the variable contains shell metacharacters: the variable that contains a semicolon and a command is the variable that the heredoc expands into a command execution. The security practice for the unquoted heredoc is the variable validation: the variables that the heredoc expands must be validated before the expansion, and the validation is the same input validation that the security articles in this series describe.
The quoted heredoc is the safe alternative for the content that does not need expansion, and the quoted form is the one that the SQL and the config generation use when the values are static. The choice between the quoted and unquoted forms is a security decision, and the decision that the script makes is the one that the security review evaluates.
The closing summary for the heredoc pattern
The heredoc is the bash feature that the production scripts use for the multi-line text, and the pattern is the one that the config generation, the SQL execution, the remote command execution and the file writing all use. The mechanism is the same across all the use cases, the difference being the command that reads the heredoc and the delimiter that terminates it. The script that uses heredoc effectively is the script that generates the config files, executes the database migrations and deploys the applications, and the script is the one that the production environment relies on for the operations that the text editor would need twenty logins to perform.
The heredoc with the sudo and the tee combination for system files
The configuration file that requires root permissions to write, the system file that the script modifies, and the heredoc that writes through the sudo tee pipeline is the pattern that every system configuration script uses.
cat << EOF | sudo tee /etc/sysctl.d/99-hardening.conf
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
EOF
and the sysctl settings that the script writes are the hardening values from the sysctl article in this series.
The heredoc for the Dockerfile generation
cat > /tmp/Dockerfile << EOF
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y curl
COPY app /usr/local/bin/app
EXPOSE 8080
CMD [/usr/local/bin/app]
EOF
docker build -t myapp:latest /tmp/
and the Dockerfile that the script generates is the one that the docker build consumes.
The heredoc for the systemd unit generation
cat << EOF | sudo tee /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target
[Service]
ExecStart=/usr/local/bin/myapp
User=appuser
Restart=always
[Install]
WantedBy=multi-user.target
EOF
reload the unit files through systemctl
systemctl enable --now myapp
and the unit file that the script generates is the one that systemd reads.
The heredoc for the cron job installation
cat << EOF | sudo tee /etc/cron.d/backup
0 3 * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
EOF
and the cron entry that the script installs is the one that the cron scheduler reads.
The heredoc with the sed and the awk
The heredoc that provides the awk script with the pattern definitions:
awk -f /dev/stdin << 'AWKSCRIPT'
/ERROR/ { count["error"]++ }
END { for (t in count) print t, count[t] }
AWKSCRIPT
and the awk reads the script from the heredoc.
The nested heredocs and the escaping
cat > /tmp/outer.sh << OUTER
#!/bin/bash
cat > /tmp/inner.conf << INNER
key=value
INNER
echo inner done
OUTER
and the OUTER and INNER delimiters distinguish the two heredocs.
The security considerations
The unquoted heredoc expands the variables, and the variable expansion is the injection vector if the variable contains shell metacharacters. The quoted heredoc is the safe alternative for the content that does not need expansion.
The closing summary
The heredoc is the bash feature that the production scripts use daily, and the patterns from this article cover the use cases: the config generation, the SQL execution, the remote command execution, the Dockerfile generation, the systemd unit generation and the cron job installation. The script that uses the heredoc effectively is the script that generates the config files, deploys the applications and manages the services.
The heredoc pattern is the foundation of config generation in bash scripts, and the deployment scripts use it daily.
The heredoc that the production scripts use for the SQL execution is the pattern that the database migrations use: the migration script that the application runs on the database start, the SQL that the heredoc carries and that the database client executes, and the migration is the operation that the application framework usually manages but that the bash script manages for the cases where the framework is not available.
The heredoc that the script uses for the certificate generation is the pattern that the self-signed CA setup uses: the openssl command that generates the key and the certificate, the configuration that the openssl command reads from the heredoc, and the certificate that the openssl command produces. The certificate generation is the setup that the TLS forwarding article described, and the heredoc is the way that the script provides the configuration to the openssl command.
The heredoc inside a function is the pattern that the reusable code uses: the function that takes the filename and the content as parameters, the heredoc that the function generates from the parameters, and the function that the script calls for each config file that it generates. The function pattern makes the heredoc reusable, and the reusability is the quality that the production script needs.
The relationship between the heredoc and the template engines, the Jinja2 and the Mustache that the web frameworks use, is the relationship between the built-in mechanism and the external dependency: the heredoc is built into bash and requires nothing, the template engines are packages that require installation, and the tradeoff is simplicity versus power. The heredoc that handles the simple substitutions is the tool that the simple scripts need, and the template engine that handles the complex logic is the tool that the complex deployments need.
The heredoc is the mechanism that every bash script in this series uses for the multi-line text, and the mechanism is the one that the script author reaches for when the echo command with the escaped newlines becomes unreadable. The heredoc that makes the multi-line text readable is the heredoc that makes the script maintainable, and the maintainability is the quality that the production script needs.