条件与循环
这一课你将能做到
- 用 if / [[ ]] / test 写出文件判断、数值比较、字符串比较三类条件,并解释 [ ] 与 [[ ]] 的区别
- 用 for / while 循环批量处理服务器列表和文件列表,用 case 写出带子命令的脚本骨架
- 组合条件与循环,写出一个「循环检测所有节点健康状态」的运维脚本
为什么要学
脚本没学到条件与循环,就只是「命令的录像回放」;学会它们,脚本才开始「思考」:部署前先判断旧进程在不在、重启后循环探测健康检查直到成功、对三台服务器挨个执行同一个检查。后端面试里「写个脚本批量检测服务是否存活」是真实出现过的手写题,本课结束时你能直接默写出来。
人话:if 是「路口」,根据路况决定走哪条道;for/while 是「传送带」,把一批东西挨个送进同一台机器。运维日常的「批量」二字,全靠这两个语法撑着。
先建立图像
条件与循环的执行流:
┌─────────┐
│ 检测条件 │ [ -f app.jar ] / $count -gt 0 / ...
└────┬────┘
真 │ 假
┌─────┴─────┐
▼ ▼
干这件事 干那件事(或跳过)
for host in web1 web2 web3 ← 传送带 1:已知名单
while ! curl 健康检查 ← 传送带 2:条件满足前一直转
判断条件速查表(先混个脸熟,跟着做里逐个练):
| 写法 | 含义 |
|---|
| -f 文件 | 是普通文件吗 |
| -d 路径 | 是目录吗 |
| -z "$s" | 字符串为空吗 |
| $a -eq $b | 数值相等(还有 -ne -gt -lt -ge -le) |
| "$a" = "$b" | 字符串相等 |
| [[ $s == prod ]] | 通配匹配(仅 [[ ]]) |
跟着做
步骤 1:if 三种形态
#!/bin/bash
# 单分支:文件存在才备份
if [ -f /opt/myapp/app.jar ]; then
echo "发现 jar 包,开始备份"
fi
# 双分支
count=$(who | wc -l)
if [ "$count" -gt 3 ]; then
echo "在线用户偏多:$count"
else
echo "在线用户正常:$count"
fi
# 多分支
score=75
if [ "$score" -ge 90 ]; then
echo "优秀"
elif [ "$score" -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
语法三铁律:if 和 then 要换行或分号;[ ] 内侧必须有空格;fi 收尾。
步骤 2:[ ] 与 [[ ]] 的差别
s="hello world"
# [ ](POSIX test):含空格必须加引号,不支持通配和 && ||
[ "$s" = "hello world" ] && echo "相等"
# [[ ]](bash 扩展):不用引号也不拆词,支持通配和正则
[[ $s == hello* ]] && echo "以 hello 开头"
[[ "$s" == *world* ]] && echo "包含 world"
[[ $s =~ ^hello ]] && echo "正则匹配"
经验法则:日常写 bash 脚本直接用 [[ ]](更宽容、功能强);追求跨 sh 兼容的脚本才用 [ ]。
步骤 3:数值与字符串比较的坑
a=10
b=9
[ "$a" -gt "$b" ] && echo "10 > 9" # 数值用 -gt
[ "10" \> "9" ] 2>/dev/null || echo "字符串比较 10 < 9!" # 字符串序里 "10" < "9"
str=""
[ -z "$str" ] && echo "str 是空串"
[ -n "$str" ] || echo "str 不是非空"
记忆口诀:数值比较用字母缩写(eq/ne/gt/lt),字符串比较用符号(=、!=),两者别混用。
步骤 4:for 循环两种形态
# 形态 1:列表遍历(最常用)
for host in 192.168.1.10 192.168.1.11 192.168.1.12; do
echo "ping $host"
ping -c 1 -W 1 "$host" > /dev/null 2>&1 && echo "$host 存活" || echo "$host 不通"
done
# 形态 2:C 风格计数
for ((i=1; i<=5; i++)); do
echo "第 $i 次探测"
done
# 遍历文件列表(注意引号防空格路径)
for f in /opt/myapp/logs/*.log; do
echo "$f : $(wc -l < "$f") 行"
done
第一个例子已经是一个「批量存活检测」雏形,第 12 课会扩展成正式脚本。
步骤 5:while 循环与健康检查轮询
# 经典组合:读文件逐行处理
cat > hosts.txt << 'EOF'
192.168.1.10
192.168.1.11
EOF
while read -r ip; do
echo "处理节点:$ip"
done < hosts.txt
# 重启后轮询健康检查:最多等 30 秒
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health)
if [ "$code" = "200" ]; then
echo "服务已就绪(等了 ${i} 秒)"; break
fi
sleep 1
done
while read -r 是 Shell 界处理「按行文本」的标准姿势;重发探测 + break 是发布脚本的灵魂段落,务必练熟。
步骤 6:case 写脚本骨架
cat > appctl.sh << 'EOF'
#!/bin/bash
case "$1" in
start) echo "启动 $2 环境..." ;;
stop) echo "停止 $2 环境..." ;;
restart) echo "重启 $2 环境..." ;;
status) echo "查看 $2 状态..." ;;
*) echo "用法:$0 {start|stop|restart|status} <环境>"; exit 1 ;;
esac
EOF
chmod +x appctl.sh
./appctl.sh status prod
./appctl.sh foo # 看看 *) 分支的报错提示
case 比一长串 if-elif 清爽得多,Nginx、MySQL 的启动脚本内部就是这种结构。
讲清楚要点
[ ] 是命令(test 的别名),[[ ]] 是 bash 关键字:命令就有「空格、引号、转义」的规矩;关键字由 bash 直接解析,支持 == 通配、=~ 正则、&&/|| 连接。bash 脚本首选 [[ ]]。
- 数值与字符串比较分两套符号:数值
-eq -ne -gt -ge -lt -le;字符串 = != < >(后者在 [ ] 里要转义)。拿字符串符号比数字会得到字典序的诡异结果("9" > "10")。
&& 和 || 是短路径的 if:cmd && echo ok || echo fail 一行搞定二分支,读脚本时按「成功则…,失败则…」理解。但注意:echo ok 自身失败也会触发 fail,关键逻辑别依赖这个细节,老老实实写 if。
- for 的列表在遍历前已确定:
for f in *.log 是 Shell 先展开通配符再循环,循环里新建的 .log 文件不会进入本轮。要边处理边生成清单的场景用 while read。
break 和 continue 控制循环走向:break 跳出整层循环,continue 跳过本轮。健康检查里「成功就 break、失败就 sleep 后继续」是标准模式。
exit N 决定脚本的退出码:主流程最后一条命令的退出码就是脚本退出码,关键分支显式 exit 0/exit 1,让 crontab 和 CI 能判断你脚本的成败。
你可能会懵的地方
-
if [ ... ]; then 的分号能省吗?
换行能省,同一行不能省。if [ ... ]\nthen 和 if [ ... ]; then 等价,if [ ... ] then 会把 then 当 test 的参数,报错。
-
为什么条件里变量要写 "$var"?
[ ] 按参数个数判断语法,变量为空时 [ $var = x ] 只剩 [ = x ],直接语法错误;加引号后 "" 算一个合法空参数。[[ ]] 没这个问题,但养成引号习惯更稳。
-
seq 1 30 是什么?
生成 1 到 30 的数字序列,bash 也可以写 {1..30}。轮询上限用这种方式控制,避免脚本死循环——crontab 里死循环脚本是生产事故。
-
怎么同时判断多个条件?
[[ ]] 里用 &&、||:[[ -f app.jar && -d logs ]]。[ ] 里用 -a、-o 或多个 [ ] 用 && 连接。现代脚本用前者。
-
while read 为什么常写成 read -r?
-r 禁止反斜杠转义,保留原始内容。没有它,日志里的 \n、\t 字面量会被 read 吃掉变形。这是行业默认写法。
错法对照
| 错法 | 问题 | 正解 |
|---|
if[$# -eq 0] | if 和 [ 之间、[ 内侧都要空格 | if [ $# -eq 0 ]; then |
[ $a > $b ] 比数值 | > 被当重定向,建了个名为 b 的文件 | 数值用 -gt,或 ((a > b)) |
for x in "$@" 写成 in $* | 含空格参数被拆坏 | for x in "$@" |
| while 循环里改循环外变量 | 管道右侧是子 Shell,改动丢失 | 用 while read < 文件 或进程替换 |
for f in $(ls *.log) | 文件名带空格就断行错乱 | 直接 for f in *.log |
卡点急救
- 报
unary operator expected 或 integer expression expected:前者是变量为空导致 test 缺参数,加双引号 "$var";后者是拿字符串符号比了非数字,先确认值确实是数字或改用正确符号。
unexpected EOF while looking for matching:if 没配 fi、for 没配 done、引号没闭合。用 bash -n script.sh 只做语法检查不执行,快速定位。
- 循环里 echo 的中文/变量没生效:检查是否在子 Shell(管道、here-string)里执行。把状态写进临时文件或用
while read < file 保持同进程。
面试常问
-
[ ]、[[ ]]、(( )) 的区别?
[ ] 是 POSIX test 命令,兼容性最强但语法苛刻(空格、引号、转义);[[ ]] 是 bash 关键字,支持通配、正则、&&/||,推荐日常使用;(( )) 是算术运算,支持 ++、比较运算返回 0/1,专做数值逻辑。
-
写一个脚本:轮询检测某服务健康接口,最多 60 秒,成功输出 OK 退出 0,超时输出 FAIL 退出 1。
参考实现:for i in $(seq 1 60); do code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health); [ "$code" = "200" ] && { echo OK; exit 0; }; sleep 1; done; echo FAIL; exit 1。考点:curl 拿状态码、条件短路、break/exit、退出码约定。
-
while read line 遍历文件时最后无输出或不执行,可能是什么原因?
文件无换行结尾的最后一行 read 会读到但返回非 0 导致循环提前结束,可用 while read -r line || [ -n "$line" ] 兜底;或者是循环体里用了 ssh/读 stdin 的命令吃掉了输入,给这些命令加 < /dev/null。
自测三问
- 判断「文件存在且大小大于 0」,用 [[ ]] 怎么写?
-eq 和 = 分别用在什么场景?
- 如何让 while read 循环里调用的 ssh 不吃掉剩余输入?
小结一句话
if 让脚本会判断、for/while 让脚本会批量、case 让脚本有子命令——三者齐备,你的脚本才从「命令录像」升级成「自动化工人」。
延伸