1. Shell脚本条件测试基础与核心逻辑
在Linux系统管理和自动化运维中,Shell脚本的条件测试语句就像交通信号灯一样,控制着程序执行的流向。作为脚本逻辑判断的基础设施,条件测试语句让静态的代码具备了动态决策能力。我至今记得刚入行时,通过一个简单的目录检测脚本解决了服务器日志归档问题,从此深刻认识到条件测试在运维工作中的价值。
条件测试本质上是对系统状态的布尔判断,在Shell中主要通过test命令或[]/[[]]语法实现。这些测试可以检查文件属性、字符串关系、数值比较等各类条件。比如-e检测文件存在性,-eq比较数值相等,=~进行正则匹配等。理解这些测试操作符是掌握条件语句的前提。
经验提示:在早期的Shell版本中,
[是test命令的符号链接,两者完全等效。而[[ ]]是bash等现代Shell的增强版本,支持更多特性如模式匹配和逻辑运算符,建议在新脚本中优先使用。
2. 单分支结构:基础但不可或缺
2.1 目录检测与创建的经典实现
单分支if结构是最简单的条件语句形式,其标准语法为:
bash复制if 条件测试; then
执行语句
fi
文中给出的目录检测示例非常典型:
bash复制DIR="/home/jacun"
if [ ! -e "$DIR" ]; then
mkdir -p "$DIR"
fi
这个脚本片段展示了几个关键点:
- 变量定义与引用:
DIR变量存储路径,通过$DIR引用 - 文件测试运算符:
-e检查文件/目录是否存在,!表示逻辑非 - mkdir的
-p参数:自动创建父目录,避免层级缺失导致的错误
2.2 实际应用中的增强实践
在实际生产环境中,我会对这个基础脚本做以下增强:
bash复制LOG_DIR="/var/log/app"
MAX_RETRY=3
current_retry=0
while [ $current_retry -lt $MAX_RETRY ]; do
if [[ ! -d "$LOG_DIR" ]]; then
if mkdir -p "$LOG_DIR" && chmod 750 "$LOG_DIR"; then
echo "$(date '+%Y-%m-%d %H:%M:%S') - 成功创建目录: $LOG_DIR" >> /var/log/setup.log
break
else
((current_retry++))
echo "目录创建失败,重试 $current_retry/$MAX_RETRY..."
sleep 1
fi
else
break
fi
done
if [ $current_retry -eq $MAX_RETRY ]; then
echo "错误:无法创建目录 $LOG_DIR" >&2
exit 1
fi
这个增强版增加了:
- 重试机制处理临时性故障
- 目录权限设置
- 完善的日志记录
- 错误处理和退出状态码
避坑指南:在检测目录时,明确使用
-d而非-e更为准确,因为-e对文件也返回真。同时要注意权限问题,即使目录存在,当前用户可能没有访问权限。
3. 双分支结构:网络检测的专业方案
3.1 基础网络连通性检测
双分支结构通过if-then-else-fi提供了二元选择路径。文中给出的ping检测示例:
bash复制ping -c 3 -i 0.2 -W 3 $1 > /dev/null
if [ $? -eq 0 ]; then
echo "$1 is OK!"
else
echo "$1 is down"
fi
这个脚本有几个值得注意的技术点:
ping参数优化:-c 3:发送3个包(平衡准确性和速度)-i 0.2:包间隔0.2秒(避免洪水攻击嫌疑)-W 3:超时3秒(防止长时间阻塞)
$?获取上条命令退出状态- 输出重定向到
/dev/null避免干扰
3.2 生产级网络检测脚本
在实际运维中,我会这样扩展基础功能:
bash复制#!/bin/bash
TARGET_HOST=${1:-8.8.8.8} # 默认检测Google DNS
LOG_FILE="/var/log/network_monitor.log"
TIMESTAMP=$(date '+%Y%m%d-%H%M%S')
# 检测函数
check_connectivity() {
local latency=$(ping -c 4 -i 0.25 -W 2 $1 2>/dev/null | awk -F'/' 'END{print $5}')
if [[ -n "$latency" ]]; then
echo "$TIMESTAMP $1 OK 平均延迟:${latency}ms" | tee -a $LOG_FILE
return 0
else
echo "$TIMESTAMP $1 DOWN" | tee -a $LOG_FILE
# 触发备用检测
if check_dns_resolution $1; then
echo "$TIMESTAMP $1 DNS解析正常但ICMP不通" | tee -a $LOG_FILE
fi
return 1
fi
}
check_dns_resolution() {
if host "$1" &>/dev/null; then
return 0
fi
return 1
}
# 主执行流程
if [[ -z "$1" ]]; then
echo "使用方法: $0 [IP或域名]"
echo "未指定目标,默认检测8.8.8.8"
fi
check_connectivity $TARGET_HOST
exit $?
这个增强版本包含:
- 默认参数和用法提示
- 延迟测量而不仅是连通性
- DNS解析备用检测
- 完整的日志记录
- 更精确的错误分类
专业建议:在网络检测中,不要仅依赖ICMP(ping)。企业网络可能禁止ICMP但服务仍可用。应该结合端口检测(如telnet/nc)和实际业务请求(如curl)进行综合判断。
4. 多分支结构:复杂条件处理的艺术
4.1 成绩评级系统实现
多分支结构通过if-elif-else链实现多元选择。文中的成绩评级示例:
bash复制read -p "请输入分数(0-100): " GRADE
if [ $GRADE -ge 85 ] && [ $GRADE -le 100 ]; then
echo "$GRADE 优秀"
elif [ $GRADE -ge 70 ] && [ $GRADE -le 84 ]; then
echo "$GRADE 合格"
else
echo "$GRADE 不及格"
fi
这个脚本有几个关键学习点:
read命令的-p参数提供提示信息- 复合条件测试使用
&&连接 - 数值比较使用
-ge(大于等于)、-le(小于等于) - 多条件分支的层级关系
4.2 企业级应用:服务状态监控
在实际系统监控中,多分支结构可以这样应用:
bash复制#!/bin/bash
# 获取服务状态函数
get_service_status() {
local status=$(systemctl is-active "$1")
case $status in
active)
echo "服务 $1 正在运行"
return 0
;;
inactive)
echo "服务 $1 已停止"
return 1
;;
failed)
echo "服务 $1 启动失败"
return 2
;;
*)
echo "服务 $1 状态未知: $status"
return 3
;;
esac
}
# 主处理逻辑
process_service() {
local service=$1
get_service_status "$service"
local result=$?
if [ $result -eq 0 ]; then
# 运行中服务的处理
local cpu_usage=$(ps -C "$service" -o %cpu --no-headers | awk '{sum+=$1} END{print sum}')
if (( $(echo "$cpu_usage > 90" | bc -l) )); then
echo "警告:$service CPU使用率过高 ($cpu_usage%)"
return 101
elif (( $(echo "$cpu_usage > 70" | bc -l) )); then
echo "注意:$service CPU使用率偏高 ($cpu_usage%)"
return 100
else
return 0
fi
elif [ $result -eq 1 ]; then
# 停止服务的处理
read -p "检测到$service未运行,是否启动?[y/N] " choice
if [[ "$choice" =~ ^[Yy] ]]; then
if systemctl start "$service"; then
echo "成功启动$service"
return 0
else
echo "启动$service失败"
return 201
fi
else
return 200
fi
else
# 其他状态的统一处理
echo "需要管理员干预的服务状态"
return 300
fi
}
# 示例调用
process_service nginx
这个脚本展示了:
- 使用
case处理离散状态值 - 多层级的状态判断
- 交互式处理
- 详细的返回状态码设计
- 实际资源监控(CPU使用率)
调试技巧:在复杂多分支脚本中,建议使用
set -x开启调试模式,或添加echo语句输出判断路径。对于关键分支,应该记录详细的日志以便事后分析。
5. 条件测试的高级应用技巧
5.1 复合条件与逻辑运算
现代Shell支持更强大的逻辑组合:
bash复制# 检查文件是否可读且大小非空
if [[ -r "$file" && -s "$file" ]]; then
process_file "$file"
fi
# 使用正则匹配检查IP格式
ip="192.168.1.1"
if [[ "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
echo "有效的IP格式"
else
echo "无效的IP格式"
fi
5.2 测试命令的替代语法
除了[]和[[]],还有这些条件测试方式:
bash复制# 使用test命令
if test -f "/etc/passwd"; then
echo "密码文件存在"
fi
# 命令列表的退出状态
if grep -q "error" /var/log/syslog; then
echo "系统日志中包含错误"
fi
# 算术比较
count=10
if (( count > 5 )); then
echo "计数大于5"
fi
5.3 性能优化建议
在性能敏感场景中:
- 避免在循环中重复测试相同条件
- 使用
case代替大量elif(当测试离散值时) - 将频繁使用的测试结果缓存到变量
- 考虑使用
(( ))进行数值比较而非[ ]
bash复制# 优化前
while read line; do
if [[ "$line" == "start" ]]; then
start_process
elif [[ "$line" == "stop" ]]; then
stop_process
# ...更多elif
fi
done < commands.txt
# 优化后
while read line; do
case "$line" in
start) start_process ;;
stop) stop_process ;;
*) handle_other ;;
esac
done < commands.txt
6. 错误处理与防御性编程
6.1 常见陷阱与解决方案
-
未引用的变量问题
bash复制# 危险:如果DIR包含空格会出错 if [ -d $DIR ]; then # 安全:始终引用变量 if [ -d "$DIR" ]; then -
字符串与数值比较混淆
bash复制# 错误:使用字符串比较运算符比较数字 if [ "$count" -gt "5" ]; then # 正确 if [[ "$count" > "5" ]]; then # 错误,这是字符串比较 -
测试运算符误用
bash复制# 检查文件是否存在且可读 if [ -e "$file" -a -r "$file" ]; then # 旧式写法 if [[ -e "$file" && -r "$file" ]]; then # 现代写法
6.2 防御性编程实践
-
启用严格模式
bash复制#!/bin/bash set -euo pipefail -
输入验证
bash复制if [[ -z "$1" ]]; then echo "错误:缺少参数" >&2 exit 1 fi -
错误处理
bash复制if ! mkdir -p "$dir"; then echo "无法创建目录 $dir" >&2 exit 1 fi -
清理处理
bash复制tempfile=$(mktemp) trap 'rm -f "$tempfile"' EXIT ERR
7. 实际案例:自动化部署脚本
下面是一个综合应用各种条件测试的实际案例:
bash复制#!/bin/bash
set -euo pipefail
# 配置
APP_NAME="myapp"
DEPLOY_DIR="/opt/$APP_NAME"
BACKUP_DIR="/var/backups/$APP_NAME"
CONFIG_FILE="/etc/$APP_NAME.conf"
# 检查root权限
if [[ $EUID -ne 0 ]]; then
echo "错误:必须使用root权限运行" >&2
exit 1
fi
# 验证配置文件
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "错误:缺少配置文件 $CONFIG_FILE" >&2
exit 1
elif [[ ! -r "$CONFIG_FILE" ]]; then
echo "错误:无法读取配置文件 $CONFIG_FILE" >&2
exit 1
fi
# 创建备份目录
if [[ ! -d "$BACKUP_DIR" ]]; then
if ! mkdir -p "$BACKUP_DIR"; then
echo "错误:无法创建备份目录 $BACKUP_DIR" >&2
exit 1
fi
chmod 700 "$BACKUP_DIR"
fi
# 检查部署目录
if [[ -d "$DEPLOY_DIR" ]]; then
# 备份现有部署
backup_file="$BACKUP_DIR/$APP_NAME-$(date +%Y%m%d%H%M%S).tar.gz"
if ! tar -czf "$backup_file" -C "$DEPLOY_DIR" .; then
echo "错误:备份失败" >&2
exit 1
fi
echo "已创建备份: $backup_file"
# 清理旧部署
if ! rm -rf "$DEPLOY_DIR"/*; then
echo "错误:清理旧部署失败" >&2
exit 1
fi
else
if ! mkdir -p "$DEPLOY_DIR"; then
echo "错误:无法创建部署目录 $DEPLOY_DIR" >&2
exit 1
fi
fi
# 执行部署
echo "开始部署..."
if rsync -a --exclude='*.tmp' ./build/ "$DEPLOY_DIR"/; then
# 验证部署
if [[ -f "$DEPLOY_DIR/bin/start.sh" ]]; then
chmod +x "$DEPLOY_DIR/bin/start.sh"
echo "部署成功完成"
# 检查服务状态
if systemctl is-active --quiet "$APP_NAME"; then
echo "检测到服务正在运行,执行热重载"
systemctl reload "$APP_NAME"
else
echo "启动服务"
systemctl start "$APP_NAME"
fi
else
echo "错误:部署文件不完整" >&2
exit 1
fi
else
echo "错误:部署同步失败" >&2
exit 1
fi
这个脚本展示了:
- 全面的前置条件检查
- 完善的错误处理
- 备份与恢复机制
- 服务状态感知
- 详细的执行日志
在15年的Shell脚本编写经历中,我总结出一个核心原则:好的条件测试应该像优秀的门卫,严格但不苛刻,全面但不繁琐。它应该能够准确识别各种边界情况,同时保持代码的可读性和可维护性。