Bash for 循环:语法与示例

循环是编程语言中的一个基础概念。它们允许你重复执行一系列命令,直到满足特定条件为止。

在 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

实际应用示例

注意:接下来两个示例会重命名文件。请先在副本上测试。-n 选项可防止 mv 覆盖已存在的目的地文件。

重命名含空格的文件名

下面的示例会将当前目录中含空格的非隐藏普通文件重命名,把空格替换为下划线:

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 ... doneC 风格循环
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 脚本」。