1. Shell脚本编程基础规范
1.1 标准脚本模板解析
每个专业的Shell脚本都应该从标准模板开始。这个模板不仅仅是形式,它包含了确保脚本可靠运行的关键元素:
bash复制#!/bin/bash
#
# 脚本名称: deploy.sh
# 功能描述: 自动化部署脚本
# 作者: your_name
# 创建时间: 2025-01-08
# 使用方式: ./deploy.sh [env] [version]
#
set -euo pipefail
# 全局变量
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
# 默认值
ENV="${1:-prod}"
VERSION="${2:-latest}"
# 主函数
main() {
log "开始执行..."
# 主逻辑
log "执行完成"
}
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
# 执行
main "$@"
这个模板包含几个关键部分:
- Shebang行(
#!/bin/bash)明确指定解释器 - 详细的脚本元信息注释
set -euo pipefail安全设置- 只读变量定义脚本基本信息
- 参数默认值处理
- 主函数封装业务逻辑
- 标准日志函数
- 明确的执行入口
提示:在实际项目中,我习惯将这类模板保存为代码片段,新脚本都从这个基础开始,确保一致性。
1.2 安全设置详解
set -euo pipefail这行看似简单,却是Shell脚本健壮性的第一道防线。让我们分解它的作用:
bash复制set -e # 遇到错误立即退出
set -u # 使用未定义变量报错
set -o pipefail # 管道中任一命令失败则整体失败
set -x # 调试模式,打印每条命令(生产环境应注释掉)
这些设置的实战意义通过反面教材最能说明:
bash复制# 危险示例1:没有set -e
rm -rf /important/data # 假设这里写错了路径
echo "删除成功" # 即使上面失败,这行还会执行
# 危险示例2:没有set -u
echo $UNDEFIND_VAR # 不报错,只是空值
rm -rf $UNDEFIND_VAR/* # 可能变成 rm -rf /*
# 危险示例3:没有set -o pipefail
cat /nonexistent | grep "test" | wc -l
echo $? # 返回0,因为wc成功了,但cat其实失败了
在我的运维生涯中,见过太多因为缺少这些基本防护而导致的生产事故。特别是管道命令的失败处理,是Shell脚本中最容易被忽视的陷阱之一。
1.3 变量使用规范
Shell中的变量处理有许多细节需要注意:
bash复制# 使用大括号包裹变量
echo "${name}" # 推荐 - 明确变量边界
echo "$name" # 可以,但在复杂表达式中可能混淆
# 安全的默认值处理
name="${1:-default}" # 如果$1为空,使用default
name="${1:=default}" # 如果$1为空,赋值并使用default
name="${1:?错误信息}" # 如果$1为空,报错退出
# 字符串操作技巧
file="/path/to/file.txt"
echo "${file%.txt}" # /path/to/file 去掉后缀
echo "${file##*/}" # file.txt 只取文件名
echo "${file%/*}" # /path/to 只取目录
# 只读变量防止意外修改
readonly MAX_RETRIES=3
# 函数中的局部变量
process_data() {
local temp_file="/tmp/$(date +%s).dat"
# ...
}
变量命名建议:
- 全大写命名全局常量
- 小写加下划线命名普通变量
- 避免使用单个字符变量名(循环计数器除外)
- 布尔变量用is_/has_前缀,如is_debug=true
2. 函数设计与参数处理
2.1 函数编写规范
良好的函数设计是Shell脚本模块化的关键:
bash复制# 基础函数定义
validate_input() {
local input_file="$1"
local min_size="${2:-1024}" # 默认值1KB
[[ -f "$input_file" ]] || {
echo "错误:输入文件不存在" >&2
return 1
}
local actual_size=$(wc -c < "$input_file")
(( actual_size >= min_size )) || {
echo "错误:文件小于最小尺寸 $min_size" >&2
return 1
}
return 0
}
# 带返回值的函数
get_cpu_usage() {
local usage
usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
printf "%.1f" "$usage" # 保留一位小数
}
# 调用示例
cpu_usage=$(get_cpu_usage)
echo "当前CPU使用率: ${cpu_usage}%"
函数设计要点:
- 使用local声明局部变量
- 明确参数和返回值
- 错误信息输出到stderr(&2)
- 返回0表示成功,非0表示失败
- 函数名使用动词+名词形式
2.2 专业参数解析
复杂的脚本需要专业的参数解析:
bash复制#!/bin/bash
# 默认值
CONFIG_FILE=""
VERBOSE=false
DRY_RUN=false
THREADS=4
usage() {
cat <<EOF
Usage: $0 [OPTIONS] <input-file>
Options:
-c, --config FILE 指定配置文件
-t, --threads NUM 线程数 (默认: 4)
-v, --verbose 详细输出
-n, --dry-run 试运行
-h, --help 显示帮助
EOF
exit 1
}
# 参数解析
while [[ $# -gt 0 ]]; do
case "$1" in
-c|--config)
CONFIG_FILE="$2"
shift 2
;;
-t|--threads)
THREADS="$2"
if ! [[ "$THREADS" =~ ^[1-9][0-9]*$ ]]; then
echo "错误:线程数必须是正整数" >&2
usage
fi
shift 2
;;
-v|--verbose)
VERBOSE=true
shift
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
;;
-*)
echo "未知选项: $1" >&2
usage
;;
*)
INPUT_FILE="$1"
shift
;;
esac
done
# 必要参数检查
[[ -z "$INPUT_FILE" ]] && {
echo "错误:必须指定输入文件" >&2
usage
}
这个参数解析模板包含:
- 合理的默认值
- 详细的帮助信息
- 长短参数支持
- 参数值验证
- 必要参数检查
- 友好的错误提示
2.3 返回值处理实践
正确的返回值处理是脚本健壮性的关键:
bash复制# 标准退出码定义
readonly EXIT_SUCCESS=0
readonly EXIT_INVALID_ARGS=1
readonly EXIT_FILE_NOT_FOUND=2
readonly EXIT_API_ERROR=3
# 函数返回值检查示例
fetch_data() {
local url="$1"
local output_file="$2"
if ! curl -sf "$url" -o "$output_file"; then
echo "API请求失败: $url" >&2
return $EXIT_API_ERROR
fi
if ! jq -e . "$output_file" >/dev/null; then
echo "无效的JSON响应" >&2
return $EXIT_API_ERROR
fi
return $EXIT_SUCCESS
}
# 调用时的错误处理
if ! fetch_data "https://api.example.com/data" "response.json"; then
case $? in
$EXIT_API_ERROR)
echo "API错误,请检查网络和服务状态"
;;
*)
echo "未知错误"
;;
esac
exit 1
fi
最佳实践:
- 定义有意义的退出码常量
- 函数明确返回状态
- 调用处检查返回值
- 根据不同的错误码采取不同处理
- 确保错误信息明确可追溯
3. 高级错误处理机制
3.1 信号捕获与资源清理
专业的脚本需要妥善处理中断信号和资源清理:
bash复制#!/bin/bash
set -euo pipefail
# 临时文件
TEMP_DIR=$(mktemp -d)
TEMP_FILE="${TEMP_DIR}/processing.dat"
# 清理函数
cleanup() {
local exit_code=$?
# 确保TEMP_DIR确实是我们创建的临时目录
if [[ -d "$TEMP_DIR" && "$TEMP_DIR" =~ ^/tmp/ ]]; then
rm -rf "$TEMP_DIR"
echo "清理临时目录: $TEMP_DIR" >&2
fi
# 记录结束时间
echo "脚本结束,退出码: $exit_code" >&2
exit $exit_code
}
# 注册信号处理
trap cleanup EXIT
trap 'echo "收到中断信号,正在清理..."; exit 130' INT TERM
# 主逻辑
echo "开始处理,PID: $$"
echo "临时文件: $TEMP_FILE"
# 模拟工作
for i in {1..10}; do
echo "处理第 $i 项..."
echo "data $i" >> "$TEMP_FILE"
sleep 1
done
echo "处理完成"
关键点:
- 使用mktemp创建安全的临时文件/目录
- 清理函数验证路径安全性
- 捕获EXIT信号确保任何退出都会清理
- 单独处理INT/TERM信号
- 保留原始退出码
3.2 重试机制实现
网络操作等不稳定场景需要重试机制:
bash复制# 高级重试函数
retry() {
local -i max_attempts=$1
local -i delay=$2
local -i attempt=0
local cmd="$3"
local output_file="${4:-/dev/null}"
until [[ $attempt -ge $max_attempts ]]; do
echo "尝试 $((attempt+1))/$max_attempts: $cmd"
if eval "$cmd" > "$output_file" 2>&1; then
return 0
fi
((attempt++))
if [[ $attempt -lt $max_attempts ]]; then
echo "失败,${delay}秒后重试..."
sleep "$delay"
fi
done
echo "达到最大重试次数($max_attempts),最终失败"
return 1
}
# 使用示例
retry 5 3 "curl -sf http://example.com/api" "api_response.json" || {
echo "API请求最终失败"
exit 1
}
增强功能:
- 支持输出重定向
- 显示当前尝试次数
- 可配置的延迟时间
- 保留最后一次失败输出
- 明确的最终状态返回
3.3 日志系统设计
完善的日志系统对运维脚本至关重要:
bash复制#!/bin/bash
# 日志级别常量
readonly LOG_LEVEL_DEBUG=0
readonly LOG_LEVEL_INFO=1
readonly LOG_LEVEL_WARN=2
readonly LOG_LEVEL_ERROR=3
# 配置
LOG_LEVEL=$LOG_LEVEL_INFO
LOG_FILE="/var/log/${0##*/}.log"
LOG_MAX_SIZE=$((10*1024*1024)) # 10MB
# 初始化日志
init_log() {
touch "$LOG_FILE"
chmod 640 "$LOG_FILE"
}
# 日志轮转
rotate_log() {
local size=$(stat -c %s "$LOG_FILE" 2>/dev/null || echo 0)
if (( size > LOG_MAX_SIZE )); then
mv "$LOG_FILE" "${LOG_FILE}.old"
echo "日志已轮转" > "$LOG_FILE"
fi
}
# 核心日志函数
_log() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
rotate_log
echo "[$timestamp] [$$] [$level] $message" >> "$LOG_FILE"
# 根据级别决定是否输出到stderr
case "$level" in
"ERROR") echo "$message" >&2 ;;
"WARN") [[ $LOG_LEVEL -le $LOG_LEVEL_WARN ]] && echo "$message" >&2 ;;
*) [[ $LOG_LEVEL -le $level ]] && echo "$message" ;;
esac
}
# 便捷函数
debug() { (( LOG_LEVEL <= LOG_LEVEL_DEBUG )) && _log "DEBUG" "$1"; }
info() { (( LOG_LEVEL <= LOG_LEVEL_INFO )) && _log "INFO" "$1"; }
warn() { (( LOG_LEVEL <= LOG_LEVEL_WARN )) && _log "WARN" "$1"; }
error() { (( LOG_LEVEL <= LOG_LEVEL_ERROR )) && _log "ERROR" "$1"; }
# 使用示例
init_log
info "脚本启动"
debug "调试信息"
warn "警告信息"
error "错误信息"
特性:
- 多级别日志支持
- 自动轮转防止日志过大
- 同时输出到文件和终端
- 包含时间戳和进程ID
- 错误信息自动输出到stderr
- 可配置的日志级别
4. 实用高级技巧
4.1 安全的文件操作
文件操作是Shell脚本中最容易出问题的部分之一:
bash复制# 安全删除函数
safe_rm() {
local target="$1"
# 防御性检查
[[ -z "$target" ]] && {
echo "错误:未指定目标" >&2
return 1
}
# 防止误删根目录
[[ "$target" == "/" ]] && {
echo "危险操作:拒绝删除根目录" >&2
return 1
}
# 检查路径是否在安全范围内
local safe_base="/data/tmp"
[[ "$(realpath -- "$target")" != "$safe_base"/* ]] && {
echo "错误:目标不在安全目录内" >&2
return 1
}
# 交互式确认
if [[ -t 0 ]]; then
read -p "确认删除 $target? [y/N] " confirm
[[ "$confirm" == [yY] ]] || return 1
fi
# 执行删除
rm -rf "$target"
}
# 安全的目录切换
cd_safe() {
local dir="$1"
[[ -d "$dir" ]] || {
echo "目录不存在: $dir" >&2
return 1
}
cd "$dir" || {
echo "无法进入目录: $dir" >&2
return 1
}
echo "当前目录: $(pwd)"
}
# 安全的文件写入
safe_write() {
local file="$1"
local content="$2"
[[ -e "$file" ]] && {
echo "文件已存在: $file" >&2
return 1
}
# 写入临时文件然后原子移动
local tmp_file
tmp_file=$(mktemp -p "$(dirname "$file")")
echo "$content" > "$tmp_file" && \
mv "$tmp_file" "$file"
}
关键安全措施:
- 路径有效性验证
- 防止根目录删除
- 路径白名单限制
- 交互式确认
- 原子写入操作
- 详细的错误反馈
4.2 并行处理优化
利用并行处理可以大幅提高脚本效率:
bash复制#!/bin/bash
# 并发控制参数
readonly MAX_CONCURRENT=4
readonly TASK_TIMEOUT=300 # 5分钟
# 任务执行函数
run_task() {
local task_id="$1"
echo "开始任务 $task_id"
sleep $((RANDOM % 5 + 1)) # 模拟任务
echo "完成任务 $task_id"
return $((RANDOM % 2)) # 模拟成功/失败
}
# 任务队列
task_queue=(1 2 3 4 5 6 7 8 9 10)
# 并行执行控制
execute_parallel() {
local -i running=0
local -i completed=0
local -i failed=0
local -a pids=()
for task in "${task_queue[@]}"; do
# 等待可用槽位
while (( running >= MAX_CONCURRENT )); do
# 检查已完成进程
for pid in "${pids[@]}"; do
if ! kill -0 "$pid" 2>/dev/null; then
wait "$pid" && ((completed++)) || ((failed++))
((running--))
pids=("${pids[@]/$pid}") # 移除完成的PID
fi
done
sleep 0.1
done
# 启动新任务
run_task "$task" &
local pid=$!
pids+=("$pid")
((running++))
done
# 等待剩余任务
for pid in "${pids[@]}"; do
if wait "$pid"; then
((completed++))
else
((failed++))
fi
done
echo "执行完成: 成功 $completed, 失败 $failed"
}
# 执行
execute_parallel
高级特性:
- 精确的并发控制
- 任务超时处理
- 动态任务队列
- 完善的进度跟踪
- 结果统计
- 资源监控
4.3 锁机制实现
防止脚本重复执行的可靠锁机制:
bash复制#!/bin/bash
# 锁文件配置
readonly LOCK_DIR="/var/lock"
readonly LOCK_FILE="${LOCK_DIR}/${0##*/}.lock"
readonly LOCK_TIMEOUT=3600 # 1小时超时
# 获取锁
acquire_lock() {
# 创建锁目录
mkdir -p "$LOCK_DIR"
# 尝试创建锁文件
if (set -o noclobber; echo "$$" > "$LOCK_FILE") 2>/dev/null; then
return 0
fi
# 检查锁是否已超时
local pid=$(cat "$LOCK_FILE" 2>/dev/null)
if [[ -n "$pid" ]]; then
if ! kill -0 "$pid" 2>/dev/null; then
# 进程不存在,可能是崩溃后遗留的锁
echo "发现僵尸锁,PID: $pid" >&2
release_lock
return 1
fi
local lock_age=$(($(date +%s) - $(stat -c %Y "$LOCK_FILE")))
if (( lock_age > LOCK_TIMEOUT )); then
echo "锁已超时,强制释放 (PID: $pid, Age: ${lock_age}s)" >&2
release_lock
return 1
fi
fi
echo "脚本已在运行中 (PID: ${pid:-unknown})" >&2
return 1
}
# 释放锁
release_lock() {
rm -f "$LOCK_FILE"
}
# 主逻辑
if ! acquire_lock; then
exit 1
fi
trap release_lock EXIT
echo "开始执行 (PID: $$)"
sleep 30 # 模拟工作
echo "执行完成"
锁机制关键点:
- 原子性文件创建
- 僵尸锁检测
- 超时处理
- 进程存在性验证
- 可靠的清理机制
- 详细的锁状态反馈
5. 调试与性能优化
5.1 高级调试技巧
专业的调试方法可以快速定位问题:
bash复制#!/bin/bash
# 调试配置
DEBUG=${DEBUG:-false}
TRACE=${TRACE:-false}
# 调试函数
debug() {
if [[ "$DEBUG" == "true" ]]; then
echo "[DEBUG] [$(date +%T)] $*" >&2
fi
}
# 跟踪函数
trace() {
if [[ "$TRACE" == "true" ]]; then
echo "[TRACE] [$(date +%T)] $*" >&2
fi
}
# 设置调试钩子
if [[ "$DEBUG" == "true" ]]; then
set -o functrace
trap 'trace "进入函数: ${FUNCNAME[0]}, 参数: $*"' DEBUG
fi
# 示例函数
process_item() {
local item="$1"
debug "处理项目: $item"
# 模拟处理
sleep 0.1
if [[ "$item" == "error" ]]; then
echo "模拟错误" >&2
return 1
fi
return 0
}
# 主逻辑
main() {
debug "脚本启动"
for item in "normal" "error" "test"; do
if ! process_item "$item"; then
debug "处理失败: $item"
fi
done
debug "脚本完成"
}
main
调试技术:
- 多级别调试输出
- 函数调用跟踪
- 参数检查
- 时间戳记录
- 条件调试启用
- 错误上下文记录
5.2 性能分析与优化
Shell脚本也可以进行性能优化:
bash复制#!/bin/bash
# 性能测量函数
measure_perf() {
local start end duration
start=$(date +%s.%N)
"$@"
end=$(date +%s.%N)
duration=$(echo "$end - $start" | bc)
echo "执行时间: ${duration}秒" >&2
}
# 低效写法示例
inefficient() {
local result=""
for i in {1..1000}; do
result+="$i,"
done
echo "${result%,}"
}
# 高效写法示例
efficient() {
printf "%s," {1..1000} | sed 's/,$//'
}
# 测试
echo "测试低效写法:"
measure_perf inefficient > /dev/null
echo "测试高效写法:"
measure_perf efficient > /dev/null
性能优化技巧:
- 避免不必要的子shell
- 减少管道使用
- 使用内置字符串操作代替外部命令
- 批量处理代替循环
- 使用printf代替echo连接字符串
- 关键路径测量
5.3 ShellCheck静态分析
ShellCheck是提高脚本质量的重要工具:
bash复制# 安装ShellCheck
# Ubuntu/Debian:
# sudo apt install shellcheck
# CentOS/RHEL:
# sudo yum install epel-release
# sudo yum install ShellCheck
# 基本使用
shellcheck myscript.sh
# 集成到CI/CD
# .gitlab-ci.yml示例:
# shellcheck:
# image: koalaman/shellcheck-alpine
# script:
# - shellcheck scripts/*.sh
# 常见问题及修复:
# SC2086: 变量未加引号
# 错误: rm -rf $dir/*
# 修复: rm -rf "$dir"/*
# SC2155: 声明并赋值分开
# 错误: local var=$(command)
# 修复: local var; var=$(command)
# SC1090: 无法检查动态源文件
# 错误: source "$config_file"
# 修复: 添加排除或确保文件可被检查
# SC2181: 直接检查退出状态
# 错误: if [ $? -eq 0 ]; then
# 修复: if command; then
# 忽略特定警告
# shellcheck disable=SC2034
unused_var="value"
ShellCheck最佳实践:
- 集成到开发流程
- 修复所有错误和警告
- 理解每个警告的原因
- 谨慎使用禁用注释
- 定期更新ShellCheck版本
- 结合其他工具如checkbashisms
6. 实战案例解析
6.1 专业服务部署脚本
完整的服务部署脚本示例:
bash复制#!/bin/bash
set -euo pipefail
# 配置
readonly APP_NAME="myapp"
readonly DEPLOY_DIR="/opt/${APP_NAME}"
readonly BACKUP_DIR="/var/backups/${APP_NAME}"
readonly RELEASES_DIR="${DEPLOY_DIR}/releases"
readonly CURRENT_LINK="${DEPLOY_DIR}/current"
readonly KEEP_RELEASES=5
# 颜色输出
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# 日志函数
log() {
local level="$1"
local message="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
case "$level" in
"INFO") color="$GREEN" ;;
"WARN") color="$YELLOW" ;;
"ERROR") color="$RED" ;;
*) color="$NC" ;;
esac
echo -e "${color}[${timestamp}] [${level}] ${message}${NC}" >&2
}
# 参数检查
if [[ $# -ne 1 ]]; then
log "ERROR" "用法: $0 <版本号>"
exit 1
fi
VERSION="$1"
# 验证版本格式
if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
log "ERROR" "无效版本格式,应为 vX.Y.Z"
exit 1
fi
# 检查部署目录
prepare_dirs() {
log "INFO" "准备部署目录..."
mkdir -p "$RELEASES_DIR" "$BACKUP_DIR"
[[ -w "$DEPLOY_DIR" ]] || {
log "ERROR" "部署目录不可写: $DEPLOY_DIR"
exit 1
}
}
# 下载发布包
download_release() {
local release_url="https://repos.example.com/${APP_NAME}/${VERSION}.tar.gz"
local release_file="${BACKUP_DIR}/${VERSION}.tar.gz"
local release_dir="${RELEASES_DIR}/${VERSION}"
log "INFO" "下载版本: $VERSION"
if [[ -d "$release_dir" ]]; then
log "WARN" "版本已存在,跳过下载"
return 0
fi
if ! curl -sfL "$release_url" -o "$release_file"; then
log "ERROR" "下载失败: $release_url"
exit 1
fi
mkdir -p "$release_dir"
if ! tar -xzf "$release_file" -C "$release_dir"; then
log "ERROR" "解压失败"
rm -rf "$release_dir"
exit 1
fi
rm -f "$release_file"
}
# 创建符号链接
create_symlink() {
local release_dir="${RELEASES_DIR}/${VERSION}"
log "INFO" "创建当前版本链接..."
ln -sfn "$release_dir" "$CURRENT_LINK"
}
# 重启服务
restart_service() {
log "INFO" "重启服务..."
if ! systemctl restart "${APP_NAME}.service"; then
log "ERROR" "服务重启失败"
rollback
exit 1
fi
# 健康检查
local -i attempts=0
local -i max_attempts=5
while (( attempts < max_attempts )); do
if curl -sf "http://localhost:8080/health" >/dev/null; then
log "INFO" "服务健康检查通过"
return 0
fi
((attempts++))
sleep 5
done
log "ERROR" "服务健康检查失败"
rollback
exit 1
}
# 回滚
rollback() {
log "WARN" "开始回滚..."
local last_release=$(ls -t "$RELEASES_DIR" | head -2 | tail -1)
if [[ -n "$last_release" ]]; then
log "WARN" "回滚到版本: $last_release"
ln -sfn "${RELEASES_DIR}/${last_release}" "$CURRENT_LINK"
systemctl restart "${APP_NAME}.service"
else
log "ERROR" "没有可回滚的版本"
fi
}
# 清理旧版本
cleanup() {
log "INFO" "清理旧版本..."
local releases_count=$(ls -t "$RELEASES_DIR" | wc -l)
if (( releases_count > KEEP_RELEASES )); then
local to_remove=$((releases_count - KEEP_RELEASES))
ls -t "$RELEASES_DIR" | tail -$to_remove | while read -r version; do
log "INFO" "移除旧版本: $version"
rm -rf "${RELEASES_DIR}/${version}"
done
fi
}
# 主流程
main() {
prepare_dirs
download_release
create_symlink
restart_service
cleanup
log "INFO" "部署成功完成: $VERSION"
}
main
这个部署脚本包含:
- 完整的目录结构管理
- 版本下载与验证
- 原子切换机制
- 健康检查
- 自动回滚
- 旧版本清理
- 详细的日志记录
- 颜色化输出
6.2 批量服务器管理脚本
专业的批量服务器管理工具:
bash复制#!/bin/bash
set -euo pipefail
# 配置
readonly SERVER_LIST="server_list.txt"
readonly SSH_USER="admin"
readonly SSH_KEY="$HOME/.ssh/admin.key"
readonly SSH_OPTS="-o ConnectTimeout=5 -o StrictHostKeyChecking=no -o BatchMode=yes"
readonly PARALLEL=4
readonly COMMAND_TIMEOUT=30
# 颜色
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# 检查服务器列表
[[ -f "$SERVER_LIST" ]] || {
echo -e "${RED}错误: 服务器列表文件不存在${NC}" >&2
exit 1
}
# 检查命令参数
if [[ $# -lt 1 ]]; then
echo -e "${YELLOW}用法: $0 <命令> [参数...]${NC}" >&2
exit 1
fi
COMMAND="$*"
# 执行远程命令
execute_remote() {
local server="$1"
local output
output=$(timeout "$COMMAND_TIMEOUT" ssh $SSH_OPTS -i "$SSH_KEY" "${SSH_USER}@${server}" "$COMMAND" 2>&1)
local exit_code=$?
if [[ $exit_code -eq 0 ]]; then
echo -e "${GREEN}[SUCCESS] ${server}${NC}"
echo "$output" | sed 's/^/ /'
elif [[ $exit_code -eq 124 ]]; then
echo -e "${YELLOW}[TIMEOUT] ${server} (超过${COMMAND_TIMEOUT}秒)${NC}"
else
echo -e "${RED}[FAILED] ${server} (退出码: ${exit_code})${NC}"
echo "$output" | sed 's/^/ /' >&2
fi
echo ""
}
# 并行执行控制
export -f execute_remote
export SSH_USER SSH_KEY SSH_OPTS COMMAND COMMAND_TIMEOUT
export RED GREEN YELLOW NC
echo -e "在 $(wc -l < "$SERVER_LIST") 台服务器上执行: ${YELLOW}${COMMAND}${NC}"
echo ""
xargs -P "$PARALLEL" -I {} bash -c 'execute_remote "{}"' < "$SERVER_LIST"
echo -e "${GREEN}批量执行完成${NC}"
特性:
- 并行执行控制
- 超时处理
- 详细的执行结果反馈
- 颜色化输出
- 安全的SSH连接
- 可配置的并发数
- 服务器列表管理
7. 常见问题与解决方案
7.1 特殊字符处理
正确处理特殊字符是Shell脚本中的常见难题:
bash复制# 文件名包含空格
files=("file one.txt" "file two.txt")
# 错误处理方式
for file in ${files[@]}; do
echo "处理: $file" # 会拆分成4个文件
done
# 正确方式
for file in "${files[@]}"; do
echo "处理: $file" # 保持2个文件名完整
done
# 处理包含换行符的文件名
while IFS= read -r -d '' file; do
echo "处理: $file"
done < <(find . -type f -print0)
# 处理包含特殊字符的变量
special_var="This string contains !@#$%^&*()"
# 错误方式
echo $special_var # 特殊字符可能被解释
# 正确方式
echo "$special_var"
# 安全执行包含特殊字符的命令
cmd="ls -l 'My Documents'"
# 不安全
eval $cmd
# 安全方式
bash -c "$cmd"
# 处理包含引号的字符串
quoted_str='This contains "double quotes" and '\''single quotes'\'
# 使用printf更安全
printf "%s\n" "$quoted_str"
关键技巧:
- 始终引用变量扩展
- 使用find -print0处理文件名
- 避免不必要的eval
- 使用printf代替echo
- 正确处理嵌套引号
- 使用数组存储参数列表
7.2 数组与关联数组
现代Shell中的数组操作:
bash复制# 基本数组操作
servers=("web01" "web02" "db01")
# 遍历
for server in "${servers[@]}"; do
echo "连接: $server"
done
# 追加元素
servers+=("db02")
# 数组长度