Bash 脚本编写教程 / 14 - Here Document 进阶
14 - Here Document 进阶
14.1 Here Document 基础回顾
# 基本语法
cat << EOF
这是一个 Here Document
支持多行文本
EOF
# 变量会被展开
name="World"
cat << EOF
Hello, $name!
当前时间: $(date)
EOF
# 单引号标记:不展开变量和命令
cat << 'EOF'
$name 不会展开
$(date) 不会执行
$((1+1)) 不会计算
EOF
14.2 Here Document 与缩进
# 使用 <<- 去除前导 Tab
if true; then
cat <<- EOF
这段文本的前导 Tab 会被删除
方便在缩进的代码块中使用
EOF
fi
# 输出(前导 Tab 被删除):
# 这段文本的前导 Tab 会被删除
# 方便在缩进的代码块中使用
# ⚠️ <<- 只去除 Tab,不去除空格
# 如果用空格缩进,需要手动处理
# 处理空格缩进的技巧
indent() {
sed 's/^[[:space:]]*//'
}
cat << 'EOF' | indent
前面的空格会被删除
所有行都会被处理
EOF
14.3 Here Document 的多种用途
创建配置文件
#!/bin/bash
# 生成 Nginx 配置
generate_nginx_config() {
local server_name="$1"
local port="$2"
local root_dir="$3"
local env="$4"
cat << EOF
server {
listen $port;
server_name $server_name;
root $root_dir;
index index.html;
# 日志配置
access_log /var/log/nginx/${server_name}_access.log;
error_log /var/log/nginx/${server_name}_error.log;
# 静态文件缓存
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# API 代理
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
}
# 环境标识
# Environment: $env
}
EOF
}
generate_nginx_config "app.example.com" 80 "/var/www/app" "production" \
> /etc/nginx/sites-available/app.conf
生成 SQL 脚本
#!/bin/bash
# 数据库迁移脚本
generate_migration() {
local version="$1"
local description="$2"
cat << EOF
-- Migration: $version
-- Description: $description
-- Generated: $(date '+%Y-%m-%d %H:%M:%S')
BEGIN;
-- 创建新表
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 创建索引
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- 记录迁移版本
INSERT INTO schema_migrations (version, applied_at)
VALUES ('$version', CURRENT_TIMESTAMP);
COMMIT;
EOF
}
generate_migration "20260510_001" "创建用户表" > migrations/001_create_users.sql
生成 Docker Compose 文件
#!/bin/bash
# 生成 Docker Compose 配置
generate_compose() {
local app_name="$1"
local image_tag="$2"
local env="$3"
local db_password
db_password=$(openssl rand -base64 24)
cat << EOF
version: '3.8'
services:
app:
image: ${app_name}:${image_tag}
container_name: ${app_name}-app
ports:
- "8080:8080"
environment:
- NODE_ENV=${env}
- DB_HOST=postgres
- DB_PORT=5432
- DB_NAME=${app_name}
- DB_PASSWORD=${db_password}
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:15-alpine
container_name: ${app_name}-db
environment:
- POSTGRES_DB=${app_name}
- POSTGRES_PASSWORD=${db_password}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pgdata:
networks:
default:
name: ${app_name}-network
EOF
}
generate_compose "myapp" "latest" "production" > docker-compose.yml
echo "数据库密码已随机生成,请妥善保存"
生成 systemd 服务文件
#!/bin/bash
# 生成 systemd 服务配置
generate_systemd_service() {
local name="$1"
local description="$2"
local exec_start="$3"
local user="${4:-root}"
local working_dir="${5:-/opt/$name}"
cat << EOF
[Unit]
Description=$description
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=$user
Group=$user
WorkingDirectory=$working_dir
ExecStart=$exec_start
Restart=on-failure
RestartSec=5
StartLimitBurst=3
StartLimitInterval=60
# 安全配置
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/log/$name
# 资源限制
LimitNOFILE=65536
MemoryMax=1G
# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=$name
[Install]
WantedBy=multi-user.target
EOF
}
generate_systemd_service \
"myapp" \
"My Application Service" \
"/opt/myapp/bin/start.sh" \
"appuser" \
"/opt/myapp" \
> /etc/systemd/system/myapp.service
systemctl daemon-reload
systemctl enable myapp
14.4 Here Document 在函数中的应用
# 生成帮助信息
show_help() {
cat << 'EOF'
用法: deploy.sh [选项] <环境>
选项:
-e, --env <env> 目标环境 (dev|staging|prod)
-v, --version <ver> 部署版本
-f, --force 强制部署
-r, --rollback 回滚到上一个版本
-d, --dry-run 模拟运行
-h, --help 显示帮助
示例:
./deploy.sh --env production --version 2.0.0
./deploy.sh -e staging -v 1.5.0 --force
./deploy.sh --rollback
环境说明:
dev 开发环境,自动部署
staging 预发布环境,需确认
prod 生产环境,需二次确认
EOF
}
# 生成模板文件
generate_template() {
local template="$1"
local output="$2"
shift 2
# 将额外的参数作为变量替换
local content
content=$(cat "$template")
while [[ $# -gt 0 ]]; do
local key="${1%%=*}"
local value="${1#*=}"
content="${content//\{\{$key\}\}/$value}"
shift
done
echo "$content" > "$output"
}
# 使用示例
cat > template.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<p>Version: {{version}}</p>
<p>Deployed: {{date}}</p>
</body>
</html>
EOF
generate_template "template.html" "index.html" \
"title=My App" \
"version=2.0.0" \
"date=$(date '+%Y-%m-%d')"
14.5 Here Document 与管道/重定向
# 与管道结合
cat << EOF | grep "error"
info: 这是信息
error: 这是错误
debug: 这是调试
error: 另一个错误
EOF
# 与 tee 结合
cat << EOF | tee config.txt
host=localhost
port=8080
debug=true
EOF
# 写入文件(覆盖)
cat > /tmp/test.txt << EOF
Hello
World
EOF
# 追加到文件
cat >> /tmp/test.txt << EOF
Goodbye
World
EOF
# 作为命令的标准输入
mysql -u root -p << 'EOF'
SHOW DATABASES;
USE myapp;
SELECT COUNT(*) FROM users;
EOF
# bash -c 使用 Here Document
bash -s << 'EOF'
echo "在子 Shell 中执行"
echo "参数: $@"
EOF
14.6 动态生成脚本
#!/bin/bash
# 生成并执行动态脚本
# 方法一:管道到 bash
cat << 'SCRIPT' | bash
echo "动态脚本执行中"
date
whoami
SCRIPT
# 方法二:写入临时文件执行
tmp_script=$(mktemp /tmp/script_XXXXXX.sh)
cat > "$tmp_script" << 'EOF'
#!/bin/bash
echo "临时脚本: $0"
echo "参数: $@"
EOF
chmod +x "$tmp_script"
"$tmp_script" arg1 arg2
rm -f "$tmp_script"
# 方法三:直接传递给 bash
bash << 'EOF'
echo "Hello from inline script"
for i in {1..5}; do
echo "第 $i 次"
done
EOF
14.7 业务场景:批量生成配置
#!/bin/bash
# generate_configs.sh —— 批量生成应用配置
set -euo pipefail
readonly CONFIG_DIR="/etc/myapp"
readonly ENVIRONMENTS=("dev" "staging" "production")
# 配置模板
generate_config() {
local env="$1"
local host port log_level replicas
case "$env" in
dev)
host="localhost"
port=8080
log_level="debug"
replicas=1
;;
staging)
host="staging.internal"
port=8080
log_level="info"
replicas=2
;;
production)
host="prod.internal"
port=8080
log_level="warn"
replicas=5
;;
esac
cat << EOF
# 应用配置 - $env 环境
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')
[server]
host = $host
port = $port
workers = $replicas
[logging]
level = $log_level
file = /var/log/myapp/$env.log
max_size = 100M
backup_count = 10
[database]
host = db-$env.internal
port = 5432
name = myapp_$env
pool_size = $((replicas * 5))
[cache]
host = cache-$env.internal
port = 6379
ttl = $((env == "production" ? 3600 : 300))
[features]
debug_mode = $([[ "$env" == "dev" ]] && echo "true" || echo "false")
metrics = true
profiling = $([[ "$env" == "dev" ]] && echo "true" || echo "false")
EOF
}
# 生成所有环境的配置
for env in "${ENVIRONMENTS[@]}"; do
mkdir -p "$CONFIG_DIR/$env"
generate_config "$env" > "$CONFIG_DIR/$env/config.ini"
echo "✅ 生成配置: $CONFIG_DIR/$env/config.ini"
done
echo ""
echo "配置生成完成!"
14.8 注意事项
| 陷阱 | 说明 | 解决方案 |
|---|
| «- 只去 Tab | 不去空格 | 使用 sed 或统一用 Tab |
| 变量在引号标记中不展开 | 'EOF' 阻止展开 | 需要展开时不加引号 |
| 行尾空格问题 | EOF 标记后不能有空格 | 确保 EOF 独占一行且无多余字符 |
| 缩进不一致 | 混用 Tab 和空格 | 统一使用 Tab + <<- |
| 特殊字符 | $、`、\ 需要转义 | 使用单引号标记 |
14.9 扩展阅读