在 Bash 中,until 循环会在给定条件为假(false)时反复执行一组命令。一旦条件变为真(true),循环就会停止。

在 Bash 脚本中,有三种基本的循环结构:for 循环、while 循环和 until 循环。until 循环与 while 相反:它在条件为假时运行,而不是在条件为真时运行。

本指南讲解 until 循环的语法,并通过实用示例演示其用法。

语法

Bash 中 until 循环的写法如下:

until [CONDITION]

do

  [COMMANDS]

done

条件会在每次迭代之前进行求值。如果结果为假(非零退出状态),循环体内的命令就会执行;如果结果为真(零退出状态),循环则终止。

基础示例

在下面的例子中,循环会打印 counter 的当前值,并在每次迭代时将该变量加 1。条件使用 -gt 比较运算符来判断 counter 是否大于 5。

#!/bin/bash

counter=0

until [ $counter -gt 5 ]

do

  echo Counter: $counter

  ((counter++))

done

只要 counter 不大于 5,循环就会一直运行。当 counter 达到 6 时,条件 [ 6 -gt 5 ] 求值为真,循环停止。输出如下:

Counter: 0
Counter: 1
Counter: 2
Counter: 3
Counter: 4
Counter: 5

注意,循环体永远不会在 counter 等于 6 时执行。由于条件在每次迭代前都会被检查,所以最终让条件成立的值不会被打印出来。

当你直接在终端中操作时,可以用分号分隔各个关键字,把同样的循环写成一行:

counter=0; until [ $counter -gt 5 ]; do echo "Counter: $counter"; ((counter++)); done

其它条件形式

到目前为止的示例都使用了 [ ](单中括号测试)。Bash 也接受 (( )) 用于算术求值,以及 [[ ]] 用于字符串模式和正则表达式。

当条件纯属数值比较时,使用 (( ))(( )) 内部的变量不需要 $ 前缀,而且比较运算符是我们熟悉的 <>>=,而不是 -lt-gt-ge

counter=0

until (( counter > 5 ))

do

  echo "Counter: $counter"

  ((counter++))

done

这与上一个示例的输出相同,因此你可以选择读起来更顺手的形式。

当你想一直循环,直到某个值匹配正则表达式时,可结合 =~ 运算符使用 [[ ]]。下面的脚本会不断提示,直到用户输入单个字母 yY

reply=""

until [[ $reply =~ ^[Yy]$ ]]

do

  read -r -p "Continue? [y/N] " reply

done

echo "Confirmed."

任何其它回答都会让条件保持为假,因此提示会一直重复,直到读者输入一个被接受的值。

break 与 continue 语句

breakcontinue 语句让你可以改变 until 循环的执行流程,而不必等待条件发生变化。

break 语句会立即终止循环。对于那种等待某个可能永远不出现的事物的循环,通常用它来设置上限:

attempt=0

until [ -f /tmp/ready.flag ]

do

  ((attempt++))

  if [ $attempt -ge 3 ]; then

    echo "Giving up after $attempt attempts."

    break

  fi

  echo "Attempt $attempt ..."

  sleep 1

done

如果文件始终没有出现,循环会在第三次尝试后停止,而不是永远运行下去:

Attempt 1 ...
Attempt 2 ...
Giving up after 3 attempts.

continue 语句会跳过当前迭代的剩余部分,并跳回条件判断处。下面我们计数到 5,但只打印奇数:

n=0

until [ $n -ge 5 ]

do

  ((n++))

  if (( n % 2 == 0 )); then

    continue

  fi

  echo "Odd: $n"

done
Odd: 1
Odd: 3
Odd: 5

实战示例

等待 Git 主机就绪

下面的脚本会不断重试 git pull,直到成功为止。当 Git 主机宕机、而你不希望反复手动输入该命令时,这非常有用:

#!/bin/bash

until git pull &> /dev/null

do

    echo "Waiting for the git host ..."

    sleep 1

done

echo -e "\nThe git repository is pulled."

sleep 命令会在每次尝试之间暂停一秒。一旦仓库被成功拉取,脚本就会打印一条确认信息:

Waiting for the git host ...
Waiting for the git host ...
Waiting for the git host ...

The git repository is pulled.

等待服务启动

这个脚本会一直等待,直到某个服务在指定端口上开始监听才继续往下执行:

#!/bin/bash

echo "Waiting for port 3306 to become available..."

until nc -z localhost 3306 2>/dev/null

do

    sleep 1

done

echo "MySQL is ready."

-z 选项会让 nc 在不发送任何数据的情况下扫描监听中的服务。精简的容器镜像通常不包含 netcat,而且不同 nc 实现之间的可用选项也不尽相同。当 netcat 不可用时,Bash 内置的 /dev/tcp 也能完成同样的任务:

#!/bin/bash

echo "Waiting for port 3306 to become available..."

until (exec 3<>/dev/tcp/localhost/3306) 2>/dev/null

do

    sleep 1

done

echo "MySQL is ready."

只有当有进程接受连接时,该重定向才会成功,因此循环会在第一次成功尝试时退出。在子 shell 中运行它可以避免文件描述符 3 在你的脚本中一直保持打开(shdash 下同样适用)。

限制最大重试次数

为了避免死循环,你可以添加一个重试计数器,在达到指定尝试次数后退出:

#!/bin/bash

max_retries=10

attempt=0

until curl -s http://localhost:8080/health > /dev/null

do

    ((attempt++))

    if [ $attempt -ge $max_retries ]; then

        echo "Service did not start after $max_retries attempts."

        exit 1

    fi

    echo "Attempt $attempt of $max_retries..."

    sleep 2

done

echo "Service is up."

倒计时器

until 实现一个简单的倒计时:

#!/bin/bash

seconds=10

until [ $seconds -eq 0 ]

do

    echo "$seconds..."

    ((seconds--))

    sleep 1

done

echo "Done!"

快速参考

如需可打印的快速参考,请参阅 Bash 速查表。

PatternDescription
until [ condition ]; do ... done当条件为假时循环
until command; do ... done循环至命令成功(退出码 0)
until (( i > 5 )); do ... done不带 $ 或 -gt 的算术条件
until [[ $var =~ regex ]]循环至正则表达式匹配
until false; do ... done无限循环(用 break 退出)
until [ -f file ]; do ... done等待文件出现
break立即退出循环
continue跳到下一次迭代

故障排查

循环永不终止:循环体内没有任何内容改变条件所测试的值。请确保计数器已被递增,或者正在重试的命令确实能够成功;如有需要,可加上带最大尝试次数的 break

integer expression expected(需要整数表达式)错误:与 -gt-ge-lt 配合使用的变量为空,或其中包含非数字文本。请在循环前初始化计数器(例如 counter=0),并在将任何从输入读取的值用于测试之前,先确认它是数字。

循环被跳过,或报 unary operator expected(需要一元运算符)错误:未加引号的空变量会在 [ ] 看到它之前就消失。写成 until [ -n $reply ] 时,空的 reply 会留下 [ -n ],它会针对空值测试 -n 标志,从而触发 unary operator expected 错误。应始终为变量加上引号:until [ -n "$reply" ]

条件在终端中可用,但在脚本中却失败:(( ))[[ ]] 是 Bash 的特性。以 #!/bin/sh 开头的脚本会在 POSIX shell 下运行,而那里并不支持它们,因此请将脚本的 shebang 改为 #!/bin/bash

常见问题

whileuntil 有什么区别?while 循环在条件为真时运行,until 循环则在条件为假时运行。两者互为逻辑反操作:until [ condition ] 的行为与 while [ ! condition ] 完全相同。

能否用 until 判断命令的退出状态,而不是测试条件?可以。until 循环会检查 until 之后那条命令的退出状态。任何返回非零退出状态(失败)的命令都会让循环继续,而第一个返回零(成功)的命令则会停止循环。

如何创建无限 until 循环?使用 until false; do ... donefalse 命令总是返回非零退出状态,因此条件永远不会变为真。在循环内部使用 break 即可在需要时退出。

在等待某事物时如何避免死循环?添加一个重试计数器,在每次迭代时递增,并在达到最大尝试次数时使用 breakexit 退出。参见上文的「限制最大重试次数」一节。

总结

until 循环会在条件为假时重复执行命令,非常适合用于等待服务可用性、网络连接等外部事件。如果需要相反的行为,则使用 while 循环。