4. 流程控制(上):if-else判断、case多分支、test命令与[]的用法

脚本不是从头跑到尾的线性代码。你需要让它“思考”——根据不同的日志内容,走不同的处理路径。就像巡检时,遇到错误日志要告警,正常日志就归档。这套逻辑,就是流程控制。

我个人习惯把条件判断看成脚本的“大脑”。没有它,脚本就是个死板的打字员。今天我们先啃下最核心的三块:if-else、case,以及经常被搞混的test命令和[]。搞懂了它们,你的巡检脚本就能“活”起来。

4.1 if-else:脚本里的交通警察

if-else的作用很简单:检查一个条件,决定走哪条路。格式长这样:

if 条件测试; then
    # 条件成立时执行的指令
else
    # 条件不成立时执行的指令
fi

我刚开始写巡检脚本时,总觉得fi是打错了。后来被同事笑了一回,才记住它是if的反写——Shell的设计者挺调皮。记住这个,你永远不会漏掉fi。

举个实际场景:检查日志文件中是否出现了“ERROR”。

if grep -q "ERROR" /var/log/app.log; then
    echo "检测到错误,请立即处理"
    # 这里可以调用告警接口
else
    echo "日志正常,无错误"
fi

关键点:grep -q不输出结果,只返回退出码。0代表找到了,非0代表没找到。if直接吃退出码,这是Shell的神奇之处。

你还可以用elif做多重判断。比如按错误级别分流:

if grep -q "CRITICAL" /var/log/app.log; then
    echo "严重故障,立即关机"
elif grep -q "WARNING" /var/log/app.log; then
    echo "警告,需关注"
else
    echo "一切正常"
fi

我曾经写过一个巡检脚本,每天凌晨扫描Nginx错误日志。一开始只用了if-else,后来发现不同错误需要不同处理——磁盘写满要扩盘,权限错误要通知运维。加上elif之后,脚本才真正长成“能干活”的样子。

4.2 case多分支:比elif更清爽的选择

如果分支超过三个,elif会让代码像楼梯一样越缩越深。这时候我推荐用case。它的语法像switch,但更灵活:

case 变量 in
    模式1)
        指令1
        ;;
    模式2|模式3)
        指令2
        ;;
    *)
        默认指令
        ;;
esac

注意双分号;;是分支结束符。我见过新手把;;写成单个;,结果Shell把所有分支都执行了一遍——嗯,血的教训。

在日志巡检中,case特别好用来处理模糊匹配。比如找出低于阈值的各种异常:

read -p "输入日志级别: " level
case $level in
    error|ERROR|Error)
        echo "执行错误处理流程"
        ;;
    warn*|WARN*)
        echo "执行警告处理流程"
        ;;
    info|INFO)
        echo "仅记录,无操作"
        ;;
    *)
        echo "未知级别,忽略"
        ;;
esac

模式里可以用*通配符,warn*能匹配warning、warning_2等。你想想看,写4个elif要多少行?用case就舒服多了。

实战技巧:在审计日志巡检中,case经常配合日志分类代码使用。比如状态码200、404、500,直接写成200|201|204在一起,比if嵌套清爽十倍。

4.3 test命令与[]:条件测试的双胞胎

前面if和case里用的“条件测试”,本质都是调用test命令。test可以判断文件类型、数值大小、字符串是否为空。写起来有几种姿势:

test -f /tmp/log.txt       # 判断文件是否存在且为普通文件
test "$a" -eq "$b"         # 数值相等
[ -f /tmp/log.txt ]        # 等价写法,注意空格!
[ "$a" -eq "$b" ]          # 等价

[]其实就是test的简写,Shell会把它当作test命令来执行。核心原则:[]两边必须有空格。我见过最经典的错误:

# 错误的写法
[-f /tmp/log.txt]   # [缺乏空格
[ "$a"= "$b" ]      # 等号两边缺空格

曾经有个同事调试了一下午,就因为在[前面没加空格。Shell把[-f当成一个命令,报错说command not found。大家翻代码时都笑了——这个坑95%的新手都会踩一遍。

下面是test常用的比较操作符,我列个表方便你查阅:

分类 操作符 说明 示例
文件测试 -f 是否是普通文件 [ -f /tmp/a.log ]
-d 是否是目录 [ -d /var/log ]
-s 文件大小是否非0 [ -s /tmp/error.log ]
数值比较 -eq 等于 [ "$count" -eq 0 ]
-gt 大于 [ "$count" -gt 100 ]
-lt 小于 [ "$count" -lt 50 ]
字符串比较 = 字符串相等(注意只用单等号) [ "$user" = "root" ]
!= 字符串不等 [ "$user" != "nobody" ]
-z 字符串是否为空 [ -z "$var" ]
-n 字符串是否非空 [ -n "$var" ]

数值比较只能用-eq、-gt这些字母,不能用>、<。后者是重定向符号,用了会出大事。我刚开始时就犯过这个错:

# 错误!会创建一个名为"100"的空文件
if [ "$count" > 100 ]; then ...

正确写法是[ "$count" -gt 100 ]。记住了。

避坑指南:变量最好加双引号,比如"$count"。如果变量未定义或为空,不加引号会导致语法错误。我曾经因为一次变量为空,导致if判断炸了,日志巡检中断了一个小时。

4.4 三种条件判断的配合套路

在实际的日志巡检脚本里,这三个工具往往搭配使用。比如先判断文件是否存在(test),如果存在就读取内容(if-else),根据内容模式走不同分支(case)。

我写过一个检查系统日志的脚本,核心逻辑大致如下:

# 检查日志文件是否存在
[ -f /var/log/syslog ] || exit 1

# 按关键字判断
if grep -q "OOM" /var/log/syslog; then
    case "$(hostname)" in
        web*)
            echo "Web服务器OOM,重启nginx"
            systemctl restart nginx
            ;;
        db*)
            echo "数据库OOM,增加swap"
            fallocate -l 2G /swapfile
            ;;
        *)
            echo "未知服务器类型,记录告警"
            ;;
    esac
elif grep -q "disk full" /var/log/syslog; then
    echo "磁盘空间不足,清理日志"
    find /var/log -type f -mtime +7 -delete
else
    echo "系统日志正常"
fi

你看,[ -f ... ]做前置检查,if-elif-else处理不同错误类型,case根据主机名细化处理方式。三个工具拧在一起,脚本才真正有用。

Shell 流程控制(上)知识结构 条件判断入口 if-else / elif test 命令 文件 / 数值 / 字符串判断 case 多分支 模式匹配(含通配符) 各分支执行不同动作 两者都依赖于 test 或 [ ] 做条件判断;[ ] 是 test 的简写形式,需注意空格规则

这张图帮你理清今天讲的三样东西的关系。if-else和case是两路分支拓扑,而test/[]是它们共同的“判官”。记住这个结构,以后写条件判断时心里就有谱了。

小提示:如果条件里需要判断多个表达式(比如文件存在且非空),可以用-a(与)和-o(或)连接:[ -f /tmp/log.txt -a -s /tmp/log.txt ]。更现代化的写法是用[[ ]](双中括号),它支持&&和||,而且不用给变量加引号。不过这是进阶话题,我们在后面的章节再细聊。

好了,这一章的内容到这里。说白了,流程控制就是让脚本“变聪明”的基础。if-else让你能二选一,case让你能多选一,test和[]则是判断的按钮。把它们练熟了,日志巡检脚本就能按你设想的逻辑自动执行——不用人盯着,多省心。