第 9 章 · 03 循环:for、while 与 until 本节摘要:脚本有了变量和分支,还差一个「重复」。本节讲 Shell 的三种循环: 、 、 。 用来遍历一个明确的列表(一组字符串、一批文件、一段命令的输出); 在条件成立时反复执行; 是 的反面,条件不成立时反复执行。重点要建立两个心智模型:一是 遍历的是「列表项」而不是「计数器」(C 风格 是例外);二是 和 的「条件」其实是一条命令,命令返回 0 就继续——这和第 02 节的 判断退出码是一回事。配上 / ,循环就能覆盖日常 90% 的重复任务。最后会提醒你:Shell 循环每跑一轮都要 fork 子进程,处理大数据时该换成 。 内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
本节摘要:脚本有了变量和分支,还差一个「重复」。本节讲 Shell 的三种循环:
for、while、until。for x in list用来遍历一个明确的列表(一组字符串、一批文件、一段命令的输出);while cond在条件成立时反复执行;until cond是while的反面,条件不成立时反复执行。重点要建立两个心智模型:一是for遍历的是「列表项」而不是「计数器」(C 风格for ((i=0;i<n;i++))是例外);二是while和until的「条件」其实是一条命令,命令返回 0 就继续——这和第 02 节的if判断退出码是一回事。配上break/continue,循环就能覆盖日常 90% 的重复任务。最后会提醒你:Shell 循环每跑一轮都要 fork 子进程,处理大数据时该换成awk。
内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
阅读完本节,你应当能够:
for x in list 遍历字符串列表,理解 Shell 怎么把列表拆成项。for f in *.txt 遍历文件,并说清为什么文件名必须加引号。for ((i=0; i<n; i++)) 写 C 风格的计数循环。while cond 和 until cond 写「条件循环」,区分两者的判断时机。break 跳出、用 continue 跳过本轮。while read line 逐行处理文件,并解释为什么要 IFS= 和加引号。在很多语言里,for 是「从 i=0 到 i=n」的计数循环。Shell 的 for 不是——它遍历的是一个**列表**:
for color in red green blue; do echo "颜色: $color" done # 输出: # 颜色: red # 颜色: green # 颜色: blue
for 后面的变量(color)依次取列表里的每个值,每取一个就执行一次 do...done 之间的循环体。列表用空格分隔,所以你看到的 red green blue 不是一个字符串,而是三个独立的项。
需要计数时,Bash 提供了 C 风格的 for(不是 POSIX 标准,但 Bash/zsh 都支持):
for ((i=0; i<5; i++)); do echo "第 $i 次" done
while 条件命令; do ... done until 条件命令; do ... done
和 if 一样,while/until 后面跟的是一条命令,不是布尔表达式。命令返回 0(成功)就被视为「真」,非 0 视为「假」。
while:条件命令返回 0 就继续循环(条件为真则循环)。until:条件命令返回非 0 就继续循环(条件为假则循环)。# while:每秒检查一次,直到文件出现 while [[ ! -f /tmp/ready ]]; do sleep 1 done echo "就绪了" # until:写法更直观(「直到 ready 出现才停」) until [[ -f /tmp/ready ]]; do sleep 1 done echo "就绪了"
这两段等价。until 是 while 的语义糖,写「等待某条件成立」时更顺。
关键概念:
while [[ ... ]]里的[[ ]]也是一条命令——它返回 0 或 1,被while当成循环条件。所以while和if共享同一套判断机制(第 02 节学的全部都能用)。
# 慢:每行都要启动 awk 子进程 while read line; do echo "$line" | awk '{print $1}' done < big.log # 快:一次 awk 处理完 awk '{print $1}' big.log
Shell 每跑一轮循环、每调用一次外部命令,都要 fork 一个子进程。处理几千行没感觉,处理几十万行就慢得难以忍受。所以循环小数据用 Shell,大数据用 awk/sed——这条原则贯穿全书。
# 遍历一组服务器名 for host in web01 web02 web03; do ssh "$host" "uptime" done # 遍历扩展名 for ext in txt md log; do echo "处理 *.$ext" done
# 处理所有 .log 文件 for f in *.log; do echo "压缩 $f" gzip "$f" # 必须加引号,防文件名含空格 done # 递归遍历(用 find) while IFS= read -r f; do echo "处理 $f" done < <(find . -name '*.txt')
⚠️ 注意:
for f in *.log有个隐患——如果没有匹配文件,*.log会保持字面值,循环跑一次处理一个名为*.log的不存在的文件。Bash 用shopt -s nullglob可以让无匹配时列表为空。
# 遍历所有用户 for user in $(cut -d: -f1 /etc/passwd); do echo "用户: $user" done # 遍历日期 for d in $(seq -w 1 31); do echo "2026-08-$d" done
注意:命令替换的输出会按空白拆分,所以只适合「输出是空白分隔的简单单词」。带空格的文件名千万别这么用(要用 find ... -print0 | while read -r -d '')。
# 计数 0 到 9 for ((i=0; i<10; i++)); do echo $i done # 双变量同时遍历 for ((i=0, j=10; i<5; i++, j--)); do echo "i=$i, j=$j" done
这是 Shell 脚本里最常用、也最容易写错的模式:
# 正确的逐行读取 while IFS= read -r line; do echo "处理: $line" done < input.txt
四个细节:
IFS= 防止行首尾的空白被吞。read -r 防止反斜杠被解释(不加 -r,\t 会被处理)。line 加引号使用。< input.txt 用重定向喂给 while,比 cat file | while 更高效(后者会开子进程,循环里改的变量在循环外失效)。# 经典场景:累计文件大小 total=0 while IFS= read -r line; do size=$(stat -c %s "$line" 2>/dev/null || echo 0) total=$((total + size)) done < file_list.txt echo "总大小: $total 字节"
# 重试 3 次连接数据库 attempts=0 until pg_isready -h db; do attempts=$((attempts + 1)) if [[ $attempts -ge 3 ]]; then echo "数据库连接失败" >&2 exit 1 fi sleep 2 done echo "数据库就绪"
# break:找到第一个匹配就停 for f in *.log; do if grep -q ERROR "$f"; then echo "第一个出错的日志: $f" break fi done # continue:跳过 .bak 文件 for f in *; do [[ $f == *.bak ]] && continue process "$f" done
cat file | while 里改的变量外面看不到count=0 cat file | while read line; do count=$((count + 1)) done echo $count # 0!管道右侧的 while 在子进程里,count 改动不传出来
原因:管道右侧的命令在子 Shell 里执行,子 Shell 的变量改动不会影响父进程。改用重定向:
count=0 while read line; do count=$((count + 1)) done < file echo $count # 正确
while read 不加 IFS= 和 -r,丢字符echo ' hello world ' | while read line; do echo "[$line]"; done # [hello world] ← 行首尾的空格被吞了 echo ' hello world ' | while IFS= read -r line; do echo "[$line]"; done # [ hello world ] ← 完整保留
逐行读文件的标准写法永远是 while IFS= read -r line,一个字符都不能省。
# 文件名带空格时,这样会出错 for f in $(find . -name '*.txt'); do ... done # 错!文件名按空格拆开 # 正确:用 -print0 配合 read -d '' while IFS= read -r -d '' f; do echo "$f" done < <(find . -name '*.txt' -print0)
while [[ ... ]] 条件永远为真,死循环i=0 while [[ $i -lt 10 ]]; do echo $i # 忘了 i=$((i+1)),死循环 done
养成习惯:写 while 第一件事先在循环体里确保能改变条件,否则就是死循环。
until 和 while 记反while 是「当条件真」、until 是「当条件假」。最简单的记法:while = while(当……时),until = until(直到……)。until cond 等价于 while ! cond。
for x in list 遍历的是列表项,不是计数器;需要计数用 C 风格 for ((i=0; i<n; i++))。while/until 后面跟的是一条命令,靠退出码判断真假,和 if 同源。while 是「条件真就循环」,until 是「条件假就循环」(等价于 while ! cond)。while IFS= read -r line; do ... done < file——IFS= 防吞空白、-r 防解释反斜杠、重定向而非管道。| while 里改的变量外面看不到(子进程问题),改用重定向 < file。find -print0 | while read -d '',绝不能用 for f in $(find ...)。awk/sed——这是性能常识。break 跳出、continue 跳过本轮,但死循环的根因往往是忘了改变条件。循环和条件让脚本有了完整的控制流。下一节我们把代码组织起来——「函数:让命令可复用」。函数是「脚本里的脚本」:它有名字、有参数、有返回值,让你把重复的逻辑抽出来复用。理解函数后,你就能写出结构清晰、可维护的 Shell 程序。