强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 扩展阅读