Skip to content

06|循环语句

上一篇的条件判断让脚本能做"二选一"。但运维里很多事要重复做——遍历一批文件逐个处理、对十台机器逐台执行命令、每隔几秒检查一次服务状态。这些靠循环。

本篇讲 Shell 的三种循环:forwhileuntil

一、for 遍历列表

for 遍历一组值,最常见的循环:

bash
for host in web-01 web-02 db-01; do
    echo "处理: $host"
    ssh "$host" "uptime"
done
text
处理: web-01
 14:30 up 47 days...
处理: web-02
 ...

遍历数字范围用 seq 或花括号:

bash
# seq 生成 1 到 5
for i in $(seq 1 5); do
    echo "第 $i 次"
done

# 花括号展开
for i in {1..5}; do
    echo "第 $i 次"
done

遍历文件列表——比如处理 /var/log 下所有 .log 文件:

bash
for logfile in /var/log/*.log; do
    echo "处理: $logfile"
    wc -l "$logfile"
done

如果匹配不到文件,/var/log/*.log 会原样传给循环体。要处理这种情况:

bash
shopt -s nullglob    # 匹配不到时展开为空,不传通配符字面值
for logfile in /var/log/*.log; do
    echo "$logfile"
done

二、while 条件循环

while 在条件为真时一直循环,适合"不知道要循环几次,直到某个条件不满足"的场景:

bash
# 每隔 2 秒检查一次 nginx 是否起来
while ! systemctl is-active --quiet nginx; do
    echo "等待 nginx 启动..."
    sleep 2
done
echo "nginx 已启动"

读文件逐行处理是 while 的经典用法:

bash
while IFS= read -r line; do
    echo "行内容: $line"
done < /etc/hosts

这个写法有几个细节:IFS= 防止行首尾空格被吃掉,-r 防止反斜杠被转义,< file 从文件读入。这是逐行读文件的标准写法,背下来。

三、until 反向条件循环

untilwhile 相反——条件为假时循环,直到条件为真:

bash
# 等到磁盘使用率降到 80% 以下
until [[ "$(df / | awk 'NR==2 {gsub(/%/,"");print $5}')" -lt 80 ]]; do
    echo "磁盘还高,等待清理..."
    sleep 10
done

until 用得比 while 少,但在"等到某事发生"的场景下比 while ! 更直观。

四、break 和 continue

break 跳出循环,continue 跳过本次进下一次:

bash
for host in web-01 web-02 db-01; do
    if ! ssh "$host" "true" 2>/dev/null; then
        echo "$host 连不上,跳过"
        continue
    fi

    echo "处理 $host"
    ssh "$host" "uptime"

    # 假设遇到某个条件就停止
    if ssh "$host" "test -f /tmp/stop"; then
        echo "$host 标记停止,中止"
        break
    fi
done

五、C 风格的 for

计数循环可以用 C 风格的 for

bash
for ((i=0; i<3; i++)); do
    echo "重试 $i"
done

或者用 while

bash
count=0
while [[ $count -lt 3 ]]; do
    echo "重试 $count"
    ((count++))
done

(( )) 做算术运算,((count++)) 自增。

六、循环里的常见坑

循环里读不到外部变量——管道会启动子 shell:

bash
count=0
echo -e "a\nb\nc" | while read line; do
    count=$((count + 1))
done
echo "count=$count"    # 0!

管道右边的 while 在子 shell 里跑,对 count 的修改不影响外层。解决办法用进程替换或重定向:

bash
count=0
while read line; do
    count=$((count + 1))
done < <(echo -e "a\nb\nc")    # 进程替换,不启动子 shell
echo "count=$count"    # 3

循环体里漏分号——dodone 要成对:

bash
for i in 1 2 3; do    # do 前要有分号
    echo $i
done

七、批量执行的实际例子

对一组机器执行相同命令,把循环和参数结合起来:

bash
#!/usr/bin/env bash
set -euo pipefail

command=${1:?用法: $0 <命令> [主机...]}
shift
hosts=("$@")

if [[ ${#hosts[@]} -eq 0 ]]; then
    echo "没有指定主机" >&2
    exit 1
fi

for host in "${hosts[@]}"; do
    echo "=== $host ==="
    if ssh "$host" "$command"; then
        echo "成功"
    else
        echo "失败" >&2
    fi
done

跑一下:

bash
./batch.sh "uptime" web-01 web-02 db-01

对三台机器逐个执行 uptime,成功失败都记录。这就是批量运维脚本的基本骨架。

学会了之后

for 遍历列表、while 条件循环、until 反向等待,加上 break/continue 控制——循环让脚本能批量处理、能重复执行、能等待状态。配合前面学的变量、条件、参数,已经能写出不少实用脚本了。

下一篇讲数组——一次存一组值、按索引访问。