正在加载文档...
文档内容较大,正在处理中,请稍候
正在加载文档...
文档内容较大,正在处理中,请稍候
#!/bin/bash # 推荐:使用 Bash
#!/bin/sh # 通用:POSIX Shell 兼容
#!/usr/bin/env bash # 灵活:自动查找 Bash选择建议:
#!/bin/bash#!/bin/sh#!/usr/bin/env bashset -e # 遇到错误立即退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中任何命令失败都返回失败
set -x # 调试模式,显示执行的命令
# 组合使用(推荐)
set -euo pipefail#!/bin/bash
# =============================================================================
# 脚本名称:自动部署脚本
# 功能描述:从 backup 目录自动部署 Docker 应用
# 作者:lyq
# 创建时间:2025-12-08
# 使用方法:./auto-deploy.sh [deploy|rollback|status] [version]
# =============================================================================
# 单行注释:解释代码逻辑
#
# 多行注释:
# 详细说明复杂逻辑
# 可以跨多行#!/bin/bash
# 脚本头部信息
set -euo pipefail
# ==============================================================================
# 配置区域
# ==============================================================================
# 获取脚本所在目录的绝对路径(项目实战技巧)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 配置变量
LOG_DIR="${PROJECT_DIR}/logs"
BACKUP_DIR="${PROJECT_DIR}/backup"
TIMEOUT=30
MAX_RETRIES=3
# ==============================================================================
# 函数定义区域
# ==============================================================================
# 日志函数
log() {
echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "${LOG_DIR}/deploy.log"
}
success() {
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] ✓ $1${NC}" | tee -a "${LOG_DIR}/deploy.log"
}
warning() {
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] ⚠ $1${NC}" | tee -a "${LOG_DIR}/deploy.log"
}
error() {
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ✗ $1${NC}" | tee -a "${LOG_DIR}/deploy.log"
}
# 工具函数
check_tools() {
# 检查必要工具
local tools=("docker" "curl" "jq")
for tool in "${tools[@]}"; do
if ! command -v "$tool" &> /dev/null; then
error "工具 $tool 未安装"
exit 1
fi
done
}
# 清理函数
cleanup() {
log "执行清理操作..."
# 清理临时文件
rm -f /tmp/deploy.* 2>/dev/null || true
}
# 退出时自动清理
trap cleanup EXIT
# ==============================================================================
# 主逻辑区域
# ==============================================================================
main() {
# 参数检查
if [ $# -eq 0 ]; then
echo "用法: $0 [command] [options]"
echo "命令:"
echo " deploy - 部署应用"
echo " rollback - 回滚应用"
echo " status - 查看状态"
exit 1
fi
local command=$1
case "$command" in
"deploy")
deploy_app "${2:-}"
;;
"rollback")
rollback_app "${2:-}"
;;
"status")
show_status
;;
*)
error "未知命令: $command"
exit 1
;;
esac
}
# 执行主函数
main "$@"# 正确的变量定义
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_NAME="my-app"
VERSION="1.0.0"
IS_DEBUG=true
# 变量引用(始终使用引号)
echo "${PROJECT_NAME}" # 推荐:使用花括号
echo "$PROJECT_NAME" # 可用:简单情况
echo $PROJECT_NAME # 不推荐:可能有空格问题
# 变量默认值设置
CONFIG_FILE="${1:-/etc/default.conf}" # 如果 $1 为空则使用默认值
TIMEOUT="${TIMEOUT:-30}" # 如果 TIMEOUT 为空则使用 30# 普通数组
SERVICES=("mysql" "redis" "backend")
FILES=("app.js" "package.json" "README.md")
# 访问数组元素
echo "${SERVICES[0]}" # 第一个元素
echo "${SERVICES[@]}" # 所有元素
echo "${#SERVICES[@]}" # 数组长度
# 遍历数组
for service in "${SERVICES[@]}"; do
log "处理服务: $service"
done
# 关联数组(Bash 4.0+)
declare -A CONFIG=(
["host"]="localhost"
["port"]="8888"
["debug"]="true"
)
echo "${CONFIG[host]}"# 字符串长度
str="hello world"
echo ${#str} # 输出: 11
# 字符串截取
echo ${str:0:5} # 输出: hello
echo ${str:6} # 输出: world
# 字符串替换
echo ${str/hello/hi} # 输出: hi world
echo ${str//l/L} # 全局替换
# 查找和提取
version="project-root-backend-v1.0.0.tar"
# 提取版本号
semantic_version=$(echo "$version" | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+')
echo "$semantic_version" # 输出: v1.0.0# 函数定义(两种方式)
function_name() {
# 函数体
echo "这是一个函数"
}
function function_name() {
# 函数体
echo "这是另一种定义方式"
}
# 带参数的函数
deploy_app() {
local version=$1 # local 变量,仅在函数内有效
local env=${2:-"prod"} # 默认参数
log "部署版本: $version"
log "环境: $env"
# 返回值
return 0 # 成功返回 0
}
# 调用函数
deploy_app "v1.0.0" "prod"
result=$? # 获取返回值# 返回数组
get_services() {
local services=("mysql" "redis" "backend")
echo "${services[@]}"
}
# 接收数组返回
services_array=($(get_services))
for service in "${services_array[@]}"; do
echo "服务: $service"
done
# 函数作为参数使用
execute_with_retry() {
local max_retries=$1
local command="$2"
local attempt=1
while [ $attempt -le $max_retries ]; do
if eval "$command"; then
success "命令执行成功"
return 0
fi
warning "第 $attempt 次尝试失败,重试中..."
((attempt++))
sleep 2
done
error "命令执行失败,已重试 $max_retries 次"
return 1
}
# 使用示例
execute_with_retry 3 "docker load -i app.tar"# 文件存在性检查
if [ -f "$file_path" ]; then
echo "文件存在"
fi
if [ ! -d "$dir_path" ]; then
echo "目录不存在"
fi
# 文件测试选项
[ -f "$file" ] # 文件存在且是普通文件
[ -d "$dir" ] # 目录存在
[ -r "$file" ] # 文件可读
[ -w "$file" ] # 文件可写
[ -x "$file" ] # 文件可执行
[ -s "$file" ] # 文件存在且不为空# 字符串比较
if [ "$version" = "v1.0.0" ]; then
echo "版本匹配"
fi
if [ "$name" != "admin" ]; then
echo "非管理员用户"
fi
# 字符串为空/非空
if [ -z "$variable" ]; then
echo "变量为空"
fi
if [ -n "$variable" ]; then
echo "变量非空"
fi
# 模式匹配
if [[ "$filename" == *.tar ]]; then
echo "是 tar 文件"
fi# 数值比较
if [ $count -gt 10 ]; then
echo "数量大于 10"
fi
if [ $port -eq 8080 ] || [ $port -eq 8888 ]; then
echo "端口号为 8080 或 8888"
fi
# 比较操作符
-eq # 等于
-ne # 不等于
-gt # 大于
-ge # 大于等于
-lt # 小于
-le # 小于等于# 逻辑与/或
if [ -f "$file" ] && [ -r "$file" ]; then
echo "文件存在且可读"
fi
if [ ! -d "$dir" ] || [ ! -w "$dir" ]; then
echo "目录不存在或不可写"
fi
# 使用 [[ ]] 进行复杂条件判断
if [[ -f "$file" && "$file" == *.sh ]]; then
echo "是 shell 脚本文件"
fi# 遍历列表
for service in mysql redis backend; do
log "处理服务: $service"
done
# 遍历数组
services=("mysql" "redis" "backend")
for service in "${services[@]}"; do
log "处理服务: $service"
done
# 数字范围
for i in {1..5}; do
log "第 $i 次尝试"
done
for i in $(seq 1 10); do
echo "数字: $i"
done
# C 风格 for 循环
for ((i=0; i<10; i++)); do
echo "索引: $i"
done# 条件循环
while [ $attempt -lt $MAX_RETRIES ]; do
if check_service; then
success "服务检查成功"
break
fi
warning "第 $attempt 次检查失败"
((attempt++))
sleep 2
done
# 读取文件
while IFS= read -r line; do
echo "处理行: $line"
done < config.txt
# 死循环(需有 break 条件)
while true; do
if check_condition; then
break
fi
sleep 1
done# 直到条件为真时停止
until docker ps | grep -q "mysql.*healthy"; do
log "等待 MySQL 启动..."
sleep 2
done
success "MySQL 已启动"# 检查命令执行结果
if docker load -i "$image_file"; then
success "镜像加载成功"
else
error "镜像加载失败"
exit 1
fi
# 检查命令是否存在
check_command() {
local cmd=$1
if ! command -v "$cmd" &> /dev/null; then
error "命令 $cmd 未找到"
exit 1
fi
}
# 检查必要权限
check_permissions() {
local dir=$1
if [ ! -r "$dir" ] || [ ! -w "$dir" ]; then
error "目录 $dir 权限不足"
exit 1
fi
}# 使用 trap 捕获信号
cleanup() {
log "执行清理操作..."
# 清理临时文件
rm -f /tmp/deploy.* 2>/dev/null || true
# 停止容器
docker stop "$CONTAINER_NAME" 2>/dev/null || true
}
# 捕获退出信号
trap cleanup EXIT
trap 'error "脚本被中断"; exit 1' INT TERM
# 错误处理函数
handle_error() {
local line_number=$1
error "脚本在第 $line_number 行出错"
cleanup
exit 1
}
# 设置错误陷阱
trap 'handle_error $LINENO' ERR# 带重试的操作
execute_with_retry() {
local max_attempts=$1
local delay=$2
local command="$3"
local attempt=1
while [ $attempt -le $max_attempts ]; do
log "尝试执行命令 (第 $attempt/$max_attempts 次)"
if eval "$command"; then
success "命令执行成功"
return 0
fi
if [ $attempt -eq $max_attempts ]; then
error "命令执行失败,已达到最大重试次数"
return 1
fi
warning "命令执行失败,${delay}秒后重试..."
sleep "$delay"
((attempt++))
done
}
# 使用示例
execute_with_retry 3 5 "docker pull nginx:latest"# 创建目录(自动创建父目录)
mkdir -p "$TARGET_DIR"
mkdir -p "${PROJECT_DIR}/logs/{deploy,backup}"
# 检查目录存在
if [ ! -d "$LOG_DIR" ]; then
mkdir -p "$LOG_DIR"
fi
# 复制目录
cp -r "$SOURCE_DIR" "$TARGET_DIR"
cp -rp "$SOURCE_DIR" "$TARGET_DIR" # 保留权限
# 删除目录
rm -rf "$TEMP_DIR"
# 获取脚本目录(项目实战)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"# 文件检查
if [ -f "$config_file" ]; then
source "$config_file"
else
error "配置文件不存在: $config_file"
exit 1
fi
# 文件备份
backup_file() {
local file=$1
local backup_dir="${2:-./backups}"
if [ -f "$file" ]; then
mkdir -p "$backup_dir"
cp "$file" "$backup_dir/$(basename "$file").$(date +%Y%m%d_%H%M%S).bak"
success "文件已备份: $file"
fi
}
# 处理受保护文件(项目实战)
remove_protected_files() {
local target_dir=$1
# 先尝试正常删除
if ! rm -rf "$target_dir" 2>/dev/null; then
log "检测到受保护文件,使用强制删除模式..."
# 进入目录,逐个删除文件,跳过受保护的文件
find "$target_dir" -type f -exec chattr -i {} \; 2>/dev/null || true
find "$target_dir" -type f -delete 2>/dev/null || true
find "$target_dir" -type d -empty -delete 2>/dev/null || true
# 最后尝试删除整个目录
rm -rf "$target_dir" 2>/dev/null || true
fi
}
# 软链接操作
create_symlink() {
local target=$1
local link_name=$2
# 先删除已存在的软链接
if [ -L "$link_name" ]; then
rm "$link_name"
fi
ln -sf "$target" "$link_name"
success "软链接已创建: $link_name -> $target"
}# 读取文件内容
read_file() {
local file=$1
if [ -f "$file" ]; then
cat "$file"
else
return 1
fi
}
# 写入文件内容
write_file() {
local file=$1
local content="$2"
echo "$content" > "$file"
}
# 追加文件内容
append_file() {
local file=$1
local content="$2"
echo "$content" >> "$file"
}
# 读取配置文件
read_config() {
local config_file=$1
local key=$2
if [ -f "$config_file" ]; then
grep "^${key}=" "$config_file" | cut -d'=' -f2-
fi
}# 基本搜索
grep "error" log.txt
grep -i "error" log.txt # 忽略大小写
grep -v "debug" log.txt # 排除包含 debug 的行
# 正则表达式搜索
grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+" version.txt
grep -E "^DB_.*=" .env # 扩展正则表达式
# 项目实战:版本号提取
extract_version() {
local version_file=$1
if [ -f "$version_file" ]; then
# 尝试多种版本格式
local version=$(grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+" "$version_file" | head -1)
if [ -z "$version" ]; then
version=$(grep "VERSION=" "$version_file" | cut -d'=' -f2-)
fi
echo "$version"
fi
}# 基本替换
sed 's/old/new/g' file.txt
# 项目实战:更新配置文件
update_config() {
local config_file=$1
local key=$2
local value=$3
if [ -f "$config_file" ]; then
# macOS 和 Linux 兼容性处理
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i "" "s/^${key}=.*/${key}=${value}/" "$config_file"
else
sed -i "s/^${key}=.*/${key}=${value}/" "$config_file"
fi
success "配置已更新: ${key}=${value}"
fi
}
# 使用示例
update_config ".env" "BACKEND_VERSION" "v1.0.0"# 提取列
awk '{print $1, $3}' file.txt
# 条件处理
awk '$1 > 100 {print $0}' numbers.txt
# 统计信息
awk '{sum+=$1} END {print "Sum:", sum}' numbers.txt# 方法 1:使用 set -x
set -x # 开启调试
# 你的代码
set +x # 关闭调试
# 方法 2:调试函数
debug() {
if [ "$DEBUG" = "true" ]; then
echo "[DEBUG] $*" >&2
fi
}
# 使用调试函数
debug "变量值: $variable"
debug "执行命令: $command"
# 方法 3:条件调试
if [ "$DEBUG" = "true" ]; then
echo "当前目录: $(pwd)"
echo "环境变量: $(env | grep -E '^(APP|DB)_')"
fi# 日志函数(项目实战)
log() {
local level=$1
local message="$2"
local timestamp=$(date +'%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $message" | tee -a "${LOG_FILE:-/tmp/script.log}"
}
# 不同级别的日志
info() { log "INFO" "$1"; }
warn() { log "WARN" "$1"; }
error() { log "ERROR" "$1"; }
debug() {
if [ "$DEBUG" = "true" ]; then
log "DEBUG" "$1"
fi
}# 执行时间测量
time_command() {
local start_time=$(date +%s)
"$@"
local end_time=$(date +%s)
local duration=$((end_time - start_time))
log "命令执行时间: ${duration}秒"
}
# 使用示例
time_command docker compose up -d
# 内存使用监控
monitor_memory() {
local pid=$1
while kill -0 "$pid" 2>/dev/null; do
ps -p "$pid" -o pid,ppid,%mem,%cpu,cmd
sleep 5
done
}# 1. 使用一致的缩进(4个空格或2个空格)
if [ -f "$file" ]; then
echo "文件存在"
fi
# 2. 变量名使用大写或小写,保持一致
PROJECT_NAME="my-app" # 常量用大写
current_version="1.0.0" # 变量用小写
# 3. 函数名使用动词或动词短语
create_directory()
backup_files()
check_permissions()
# 4. 引号的使用
echo "$variable" # 始终给变量加引号
echo "${array[@]}" # 数组使用花括号# 1. 使用 set -euo pipefail
set -euo pipefail
# 2. 验证输入参数
validate_version() {
local version=$1
if [[ ! "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
error "无效的版本格式: $version"
exit 1
fi
}
# 3. 避免使用 eval,使用更安全的方式
# 不推荐:eval "$command"
# 推荐:"$@"
# 4. 临时文件处理
create_temp_file() {
local prefix=${1:-script}
local temp_file=$(mktemp "/tmp/${prefix}.XXXXXX")
echo "$temp_file"
}
# 使用后自动清理
temp_file=$(create_temp_file)
trap "rm -f '$temp_file'" EXIT# 1. 模块化设计
# 将功能拆分为独立的函数
check_dependencies
load_configuration
execute_deployment
verify_deployment
# 2. 配置外部化
# 使用配置文件而不是硬编码
CONFIG_FILE="${CONFIG_FILE:-./config.conf}"
if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# 3. 错误处理标准化
handle_error() {
local exit_code=$?
local line_number=$1
error "脚本在第 $line_number 行失败,退出码: $exit_code"
cleanup
exit $exit_code
}
trap 'handle_error $LINENO' ERR
# 4. 提供帮助信息
show_help() {
cat << EOF
用法: $0 [选项] [命令]
命令:
deploy - 部署应用
rollback - 回滚应用
status - 查看状态
选项:
-h, --help 显示帮助信息
-v, --version 显示版本信息
-d, --debug 启用调试模式
示例:
$0 deploy v1.0.0
$0 rollback v1.0.0
$0 status
EOF
}# 1. 避免重复计算
cache_computation() {
local result=$(expensive_computation)
echo "$result"
}
# 2. 使用数组而不是多个变量
services=("mysql" "redis" "backend")
for service in "${services[@]}"; do
restart_service "$service"
done
# 3. 并行处理
start_services() {
local services=("$@")
for service in "${services[@]}"; do
start_service "$service" &
done
wait # 等待所有后台任务完成
}
# 4. 使用管道而不是临时文件
process_data | transform_data | save_data# 1. 智能路径处理
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
# 2. 命令兼容性检查
check_tools() {
# 检查 Docker Compose 版本兼容性
if docker compose version &> /dev/null; then
COMPOSE_CMD="docker compose"
else
COMPOSE_CMD="docker-compose"
fi
}
# 3. 版本检测和解析
detect_latest_version() {
local buckup_dir=$1
# 智能查找最新的备份目录
local latest_dir=$(find "$buckup_dir" -type d -name "*" | sort | tail -1)
if [ -n "$latest_dir" ]; then
local version_file="$latest_dir/version-info.txt"
if [ -f "$version_file" ]; then
# 支持多种版本格式
local version=$(grep -o "v[0-9]\+\.[0-9]\+\.[0-9]\+" "$version_file" | head -1)
if [ -z "$version" ]; then
version=$(grep "VERSION=" "$version_file" | cut -d'=' -f2-)
fi
echo "$version"
fi
fi
}
# 4. 前端版本管理
deploy_frontend() {
local version=$1
local source_dir="$BACKUP_DIR/$version/frontend"
local target_dir="$FRONTEND_DIR/versions/$version"
# 创建版本目录
mkdir -p "$target_dir"
# 复制前端文件
if [ -d "$source_dir" ]; then
cp -r "$source_dir"/* "$target_dir/"
# 同步 protect 目录
if [ -d "$FRONTEND_DIR/protect" ] && [ "$(ls -A "$FRONTEND_DIR/protect")" ]; then
rsync -av "$FRONTEND_DIR/protect/" "$target_dir/"
fi
# 更新软链接
create_symlink "$target_dir" "$FRONTEND_DIR/current"
success "前端部署完成: $version"
else
error "前端源码目录不存在: $source_dir"
return 1
fi
}
# 5. Docker 操作
deploy_backend() {
local version=$1
local image_file="$BACKUP_DIR/$version/backend/*.tar"
# 加载 Docker 镜像
for image in $image_file; do
if [ -f "$image" ]; then
log "加载 Docker 镜像: $image"
docker load -i "$image"
# 提取镜像名称和版本
local image_name=$(docker load -i "$image" | grep "Loaded image:" | cut -d' ' -f3)
local semantic_version=$(echo "$image_name" | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "$version")
# 更新环境变量
update_config "$PROJECT_DIR/.env" "BACKEND_VERSION" "$semantic_version"
success "Docker 镜像加载完成: $image_name"
fi
done
# 重启服务
restart_services
}
# 6. 服务管理
restart_services() {
log "重启 Docker 服务..."
cd "$PROJECT_DIR"
# 停止现有服务
if $COMPOSE_CMD ps | grep -q "Up"; then
$COMPOSE_CMD down
fi
# 启动服务
$COMPOSE_CMD up -d
# 等待服务启动
sleep 10
# 检查服务状态
if $COMPOSE_CMD ps | grep -q "Up"; then
success "服务启动成功"
else
error "服务启动失败"
return 1
fi
}# 环境变量配置管理
setup_environment() {
local env_file="$PROJECT_DIR/.env"
# 检查环境变量文件
if [ ! -f "$env_file" ]; then
if [ -f "$env_file.example" ]; then
cp "$env_file.example" "$env_file"
warning "已创建环境变量文件,请修改配置: $env_file"
else
error "环境变量文件不存在: $env_file"
exit 1
fi
fi
# 验证必要的环境变量
local required_vars=("DB_HOST" "DB_PORT" "REDIS_HOST")
for var in "${required_vars[@]}"; do
if ! grep -q "^${var}=" "$env_file"; then
error "缺少必要的环境变量: $var"
exit 1
fi
done
success "环境变量配置验证完成"
}
# 配置更新函数
update_config() {
local config_file=$1
local key=$2
local value=$3
if [ -f "$config_file" ]; then
# 备份原文件
backup_file "$config_file"
# 更新配置(兼容 macOS 和 Linux)
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i "" "s|^${key}=.*|${key}=${value}|" "$config_file"
else
sed -i "s|^${key}=.*|${key}=${value}|" "$config_file"
fi
# 如果配置不存在,则添加
if ! grep -q "^${key}=" "$config_file"; then
echo "${key}=${value}" >> "$config_file"
fi
success "配置已更新: ${key}=${value}"
else
error "配置文件不存在: $config_file"
return 1
fi
}# 备份当前版本
backup_current() {
local backup_dir=$1
local current_version=$(get_current_version)
if [ -n "$current_version" ]; then
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_path="$backup_dir/$current_version"
mkdir -p "$backup_path"
# 备份前端
if [ -L "$FRONTEND_DIR/current" ]; then
local current_path=$(readlink "$FRONTEND_DIR/current")
cp -r "$current_path" "$backup_path/frontend/"
fi
# 备份配置文件
cp "$PROJECT_DIR/.env" "$backup_path/"
# 创建版本信息
cat > "$backup_path/version-info.txt" << EOF
备份时间: $(date)
备份版本: $current_version
备份类型: 自动备份
EOF
success "当前版本已备份: $backup_path"
fi
}
# 回滚到指定版本
rollback_to_version() {
local target_version=$1
# 检查目标版本是否存在
if [ ! -d "$FRONTEND_DIR/versions/$target_version" ]; then
error "目标版本不存在: $target_version"
return 1
fi
log "开始回滚到版本: $target_version"
# 备份当前版本
backup_current
# 更新前端软链接
create_symlink "$FRONTEND_DIR/versions/$target_version" "$FRONTEND_DIR/current"
# 更新后端版本
update_config "$PROJECT_DIR/.env" "BACKEND_VERSION" "$target_version"
# 重启服务
restart_services
success "回滚完成: $target_version"
}# shellcheck - 静态分析工具
shellcheck script.sh
# 安装 shellcheck
# macOS: brew install shellcheck
# Ubuntu: sudo apt-get install shellcheck# shfmt - Shell 代码格式化工具
shfmt -i 4 -w script.sh
# 安装 shfmt
# macOS: brew install shfmt
# Go: go install mvdan.cc/sh/v3/cmd/shfmt@latest# bats - Bash 自动化测试
bats test_script.bats
# 测试示例
#!/usr/bin/env bats
@test "检查工具是否存在" {
run command -v docker
[ "$status" -eq 0 ]
}
@test "配置文件验证" {
run ./script.sh --check-config
[ "$status" -eq 0 ]
}# bashdb - Bash 调试器
bashdb script.sh
# 或者使用 bash 内置调试
bash -x script.sh我也在学