在 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
这与上一个示例的输出相同,因此你可以选择读起来更顺手的形式。
当你想一直循环,直到某个值匹配正则表达式时,可结合 =~ 运算符使用 [[ ]]。下面的脚本会不断提示,直到用户输入单个字母 y 或 Y:
reply="" until [[ $reply =~ ^[Yy]$ ]] do read -r -p "Continue? [y/N] " reply done echo "Confirmed."
任何其它回答都会让条件保持为假,因此提示会一直重复,直到读者输入一个被接受的值。
break 与 continue 语句
break 和 continue 语句让你可以改变 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 在你的脚本中一直保持打开(sh 与 dash 下同样适用)。
限制最大重试次数
为了避免死循环,你可以添加一个重试计数器,在达到指定尝试次数后退出:
#!/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 速查表。
| Pattern | Description |
|---|---|
| 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。
常见问题
while 和 until 有什么区别?while 循环在条件为真时运行,until 循环则在条件为假时运行。两者互为逻辑反操作:until [ condition ] 的行为与 while [ ! condition ] 完全相同。
能否用 until 判断命令的退出状态,而不是测试条件?可以。until 循环会检查 until 之后那条命令的退出状态。任何返回非零退出状态(失败)的命令都会让循环继续,而第一个返回零(成功)的命令则会停止循环。
如何创建无限 until 循环?使用 until false; do ... done。false 命令总是返回非零退出状态,因此条件永远不会变为真。在循环内部使用 break 即可在需要时退出。
在等待某事物时如何避免死循环?添加一个重试计数器,在每次迭代时递增,并在达到最大尝试次数时使用 break 或 exit 退出。参见上文的「限制最大重试次数」一节。
总结
until 循环会在条件为假时重复执行命令,非常适合用于等待服务可用性、网络连接等外部事件。如果需要相反的行为,则使用 while 循环。