
循环是编程语言中的一个基础概念。它们允许你重复执行一系列命令,直到满足特定条件为止。
在 Bash 这样的脚本语言中,循环对于自动化重复性任务尤其有用,可以避免重复编写代码。
Bash 脚本中有三种基本的循环结构:for 循环、while 循环和 until 循环。
本指南聚焦于 Bash 中的 for 循环,涵盖其语法变体、嵌套、break 与 continue 等流程控制语句,以及实用示例。
标准 Bash for 循环
for 循环会遍历一个列表中的各项,并对每一项执行给定的一组命令。
Bash for 循环的基本语法如下:
for item in [LIST] do [COMMANDS] done
列表 [LIST] 可以是一系列以空格分隔的字符串、一个数字范围、命令的输出、一个数组、命令行参数,等等。
遍历字符串
下面的示例展示了一个遍历字符串列表的循环。循环对列表中的每一项进行操作,变量 element 保存循环当前正在处理的那一项。
for element in Hydrogen Helium Lithium Beryllium
do
echo "Element: $element"
done
Element: Hydrogen Element: Helium Element: Lithium Element: Beryllium
遍历数字范围
你可以使用序列表达式,通过定义范围的起点和终点来指定一组数字或字符。序列表达式的形式如下:
{START..END}
下面是一个遍历从 0 到 3 所有数字的循环示例:
for i in {0..3}
do
echo "Number: $i"
done
Number: 0 Number: 1 Number: 2 Number: 3
Bash 还支持在使用范围时指定步长(增量):
{START..END..INCREMENT}
下面是一个以 5 为步长的示例:
for i in {0..20..5}
do
echo "Number: $i"
done
Number: 0 Number: 5 Number: 10 Number: 15 Number: 20
遍历数组元素
你可以使用 for 循环遍历 Bash 数组的元素。
在下面的示例中,我们定义了一个名为 BOOKS 的数组,并遍历数组中的每一个元素。
BOOKS=('In Search of Lost Time' 'Don Quixote' 'Ulysses' 'The Great Gatsby')
for book in "${BOOKS[@]}"; do
echo "Book: $book"
done
Book: In Search of Lost Time Book: Don Quixote Book: Ulysses Book: The Great Gatsby
遍历文件
列表中的通配符模式会让循环遍历当前目录中匹配的路径名。即使文件名包含空格,Bash 也会把每个匹配项当作独立的项。默认情况下,* 不会匹配以点开头的名称。
下面的示例启用了 nullglob,并遍历以 .txt 结尾的非隐藏路径名:
shopt -s nullglob
for file in *.txt
do
echo "File: $file"
done
File: report.txt File: sales data.txt
注意,"sales data.txt" 作为一个整体出现,而不是被拆成两项。当你把变量以 "$file" 的形式传给命令时,能始终保持文件名的完整性。启用 nullglob 后,未匹配到文件的 *.txt 模式会展开为空,而不会作为字面字符串传给循环。
遍历命令输出
列表也可以由某个命令的输出构成。对于未加引号的 $(command) 替换,Bash 会去掉末尾的换行符,使用 IFS 对结果进行分词,再对得到的单词进行文件名扩展。
下面的循环从 /etc/passwd 读取账户名,并为每个账户打印一行:
for user in $(cut -d: -f1 /etc/passwd)
do
echo "User: $user"
done
这种处理方式也正是该形式脆弱的原因。在默认 IFS 下,Bash 会按空格、制表符和换行符对输出进行分词,因此任何含空格的值都会被拆成独立的项:
for name in $(printf 'report.txt\nsales data.txt\n')
do
echo "Item: $name"
done
Item: report.txt Item: sales Item: data.txt
命令只打印了两个文件名,但循环却收到了三项。输出中的通配符还可能针对当前目录中的文件进行展开。遍历文件时请使用通配符模式;需要逐行处理输出时,请改用带 read 的 while 循环。
C 语言风格的 for 循环
Bash 也支持 C 语言风格的 for 循环语法:
for ((INITIALIZATION; TEST; STEP)) do [COMMANDS] done
INITIALIZATION(初始化)部分仅在循环启动时执行一次。随后使用比较运算符对 TEST(测试)部分求值。若结果为假,循环终止;若为真,则执行 for 循环体内的命令,并更新 STEP(步进)部分。
在下面的示例中,循环先将 i 初始化为 0,并在每次迭代前检查 i ≤ 10。若为真,则打印 i 的当前值并将 i 自增 1(i++);否则循环终止。
for ((i = 0 ; i <= 10 ; i++)); do
echo "Counter: $i"
done
该循环共迭代 11 次,产生如下输出:
Counter: 0 Counter: 1 Counter: 2 ... Counter: 8 Counter: 9 Counter: 10
嵌套循环
嵌套循环是指一个循环位于另一个循环内部。你可以将循环嵌套到任意深度。
在嵌套的 for 循环中,外层循环每迭代一次,内层循环都会完整地跑完它自己的所有迭代。
下面是一个将三个文件逐一复制到三台服务器的示例:
for f in file_{1..3}; do
for j in server_{1..3}; do
echo "Copying file $f to server $j"
# command to copy files
done
done
Copying file file_1 to server server_1 Copying file file_1 to server server_2 Copying file file_1 to server server_3 Copying file file_2 to server server_1 Copying file file_2 to server server_2 Copying file file_2 to server server_3 Copying file file_3 to server server_1 Copying file file_3 to server server_2 Copying file file_3 to server server_3
控制循环流程:break 与 continue
break 与 continue 语句用于控制 for 循环的执行。
break:终止循环
break 语句会终止当前循环,并将控制权交给其后的语句。它通常用于在满足特定条件时退出循环。
在下面的示例中,一旦当前项等于 Lithium,if 语句就会终止循环:
for element in Hydrogen Helium Lithium Beryllium; do
if [[ "$element" == 'Lithium' ]]; then
break
fi
echo "Element: $element"
done
echo 'All Done!'
Element: Hydrogen Element: Helium All Done!
continue:跳到下一次迭代
continue 语句会退出当前这次迭代,并把控制权交给下一次迭代。
在下面的示例中,当当前项等于 2 时,continue 语句会跳过 echo,直接进入下一次迭代:
for i in {1..5}; do
if [[ "$i" == '2' ]]; then
continue
fi
echo "Number: $i"
done
Number: 1 Number: 3 Number: 4 Number: 5
实际应用示例
重命名含空格的文件名
下面的示例会将当前目录中含空格的非隐藏普通文件重命名,把空格替换为下划线:
shopt -s nullglob
for file in *\ *; do
[[ -f "$file" ]] || continue
mv -n -- "$file" "${file// /_}"
done
- *\ * 匹配含空格的非隐藏文件名。
- ${file// /_} 使用 Shell 参数展开,将每个空格替换为下划线。
- [[ -f "$file" ]] 仅当匹配项解析为普通文件时才继续。
- mv -n -- 会保留已有的目标文件不变,并防止以短横线开头的文件名被当作选项。
更改文件扩展名
下面的示例会将当前目录中以 .jpeg 结尾的非隐藏普通文件重命名,把扩展名替换为 .jpg:
shopt -s nullglob
for file in *.jpeg; do
[[ -f "$file" ]] || continue
mv -n -- "$file" "${file%.jpeg}.jpg"
done
- *.jpeg 匹配以 .jpeg 结尾的非隐藏文件名。
- ${file%.jpeg} 使用 Shell 参数展开去掉 .jpeg 后缀,再追加 .jpg 组成新文件名。
- [[ -f "$file" ]] 仅当匹配项解析为普通文件时才继续。
- mv -n -- 会保留已有的 .jpg 文件不变,并防止以短横线开头的文件名被当作选项。
重启多个 Docker 容器
下面是一个在部署新代码后重启一组容器的示例:
for container in web db cache redis; do
docker restart "$container" && echo "Restarted $container"
done
容器以字符串列表的形式传给循环。
故障排查
循环把含空格的文件名或字符串拆成了多项:在默认 IFS 下,Bash 会对空格、制表符和换行符进行分词。请始终对变量和数组展开加引号:使用 "${ARRAY[@]}" 而非 ${ARRAY[@]},使用 "$file" 而非 $file。
列表中的通配符模式意外展开:当 *.jpeg 之类的通配符匹配不到文件时,Bash 会把字面字符串 *.jpeg 传给循环变量。在循环前加上 shopt -s nullglob,让未匹配的通配符展开为空而非留在字面量。
C 风格循环与 [[ ]] 表现不符:C 风格循环使用算术求值,(( )) 内部直接用 <、>、== 进行比较。不要在循环初始化或条件中混入 [[ ]] 测试语法。
含变量的范围无法展开:花括号展开发生在变量替换之前,因此 {1..$N} 无法生效。请改用 C 风格循环或 seq:for i in $(seq 1 "$N"); do。
速查表
如需可打印的速查表,请参阅 Bash 速查表。
| 语法 | 说明 |
|---|---|
for item in a b c; do ... done | 遍历字符串列表 |
for i in {1..10}; do ... done | 遍历数字范围 |
for i in {0..20..5}; do ... done | 按步长遍历范围 |
for item in "${ARRAY[@]}"; do ... done | 遍历数组元素 |
for file in *.txt; do ... done | 遍历匹配的非隐藏路径名(先启用 nullglob) |
for x in $(command); do ... done | 按空白字符拆分命令输出(对任意文本不安全) |
for ((i=0; i<10; i++)); do ... done | C 风格循环 |
break | 立即退出循环 |
continue | 跳到下一次迭代 |
常见问题
标准 for 循环与 C 风格 for 循环有什么区别?标准 for 循环遍历一个列表中的各项(字符串、数组元素、范围)。C 风格 for 循环使用算术初始化、条件和步进表达式,因此更适合需要显式控制起点、终点和步长的计数器式迭代。
如何逐行遍历文件?请改用带 read 的 while 循环:while IFS= read -r line; do echo "$line"; done < file.txt。for 循环不适合逐行读取文件,因为它按空白字符而非换行符进行拆分。
在嵌套循环中可以使用 break 或 continue 吗?可以。默认情况下,break 和 continue 只影响最内层的循环。若要从内层退出外层循环,可使用数字参数:break 2 会退出两层嵌套。
如何遍历命令行参数?使用 for arg in "$@"; do 来遍历传给脚本的所有位置参数。
总结
Bash 的 for 循环既可以遍历一个列表中的各项,也可以使用 C 风格语法运行指定次数的迭代。配合 break、continue、嵌套以及参数展开,它能为许多自动化任务提供优雅的解决方案。
要了解运行脚本的所有方式,请参阅「如何运行 Bash 脚本」。