Shell脚本中until循环语句的用法详解
引言
在 Linux Shell 编程世界中,循环结构是控制程序流程、实现重复任务的核心工具之一。我们常听到 for 和 while 循环,但有一个低调却实用的“反向选手”——until 循环。它不像 while 那样“条件为真时执行”,而是“条件为假时才执行”,这种逆向思维往往能在特定场景下带来意想不到的简洁与高效。
今天,我们将深入探索 Shell 脚本中的 until 循环语句,从最基础的语法结构讲起,逐步过渡到复杂嵌套、文件处理、错误重试机制等实战应用,并穿插 Java 代码对比,帮助你建立跨语言的编程思维。无论你是刚入门 Shell 的新手,还是希望深化脚本能力的老手,这篇文章都将为你打开一扇新的大门。
什么是 until 循环?
until 是 Shell 脚本中的一种循环控制结构,其基本思想是:
只要条件不成立(返回非零退出状态),就一直执行循环体;一旦条件成立(返回零退出状态),则退出循环。
这与 while 循环正好相反。while 是“条件为真时执行”,而 until 是“条件为假时执行”。
基本语法结构
until [ 条件表达式 ]
do
# 循环体:当条件为假时执行的命令
command1
command2
...
done
或者使用双括号或 test 命令:
until (( 表达式 )); do
# 执行语句
done
until test 条件; do
# 执行语句
done
简单示例:倒计时器
让我们从一个简单的例子开始 —— 实现一个倒计时器,从 5 数到 0:
#!/bin/bash
counter=5
until [ $counter -lt 0 ]
do
echo "倒计时: $counter"
sleep 1
counter=$((counter - 1))
done
echo "💥 发射!"
运行结果:
倒计时: 5 倒计时: 4 倒计时: 3 倒计时: 2 倒计时: 1 倒计时: 0 💥 发射!
在这个例子中,循环会持续执行,直到 $counter 小于 0(即条件为真)才停止。每次循环减少 1,模拟倒计时效果。
与 while 循环的对比
为了更清楚地理解 until 的独特之处,我们将其与 while 对比:
使用 while 实现相同逻辑:
counter=5
while [ $counter -ge 0 ]
do
echo "倒计时: $counter"
sleep 1
counter=$((counter - 1))
done
echo "💥 发射!"
对比分析:
| 特性 | until | while |
|---|---|---|
| 条件判断 | 条件为假 → 执行 | 条件为真 → 执行 |
| 语义倾向 | “直到…为止” | “当…的时候” |
| 适用场景 | 等待某事发生 | 满足条件时持续操作 |
关键区别在于语义和心理模型 —— until 更适合描述“等待某个目标达成”的场景。
实战场景一:等待服务启动
在自动化部署或系统初始化过程中,常常需要等待某个服务(如数据库、Web 服务器)启动完成后再继续后续操作。这时 until 就派上用场了!
#!/bin/bash
echo "⏳ 正在等待 MySQL 服务启动..."
until mysqladmin ping --silent; do
echo "⚠️ MySQL 未就绪,3 秒后重试..."
sleep 3
done
echo "✅ MySQL 已启动,继续执行后续任务..."
这段脚本会不断尝试连接 MySQL,直到成功为止。mysqladmin ping 成功时返回 0,失败时返回非 0 —— 这正是 until 所需的“终止条件”。
提示:你可以将 mysqladmin ping 替换为 curl -f http://localhost:8080/health 来检测 Web 服务健康状态。
实战场景二:用户输入验证
有时我们需要用户输入特定格式的数据,比如邮箱、数字、Y/N 确认等。使用 until 可以优雅地实现“直到输入合法才继续”的逻辑:
#!/bin/bash
read -p "请输入您的年龄: " age
until [[ "$age" =~ ^[0-9]+$ ]] && [ $age -ge 1 ] && [ $age -le 120 ]; do
echo "❌ 输入无效,请输入 1 到 120 之间的整数。"
read -p "请重新输入年龄: " age
done
echo "✅ 年龄已确认: $age 岁"
这个例子中,循环会持续提示用户输入,直到满足三个条件:
- 输入是纯数字
- 数字 ≥ 1
- 数字 ≤ 120
流程图展示 until 循环工作原理
下面是一个用 Mermaid 绘制的 until 循环执行流程图,帮助你直观理解其控制流:

从图中可以看出,until 循环在每次迭代前都会检查条件。只有当条件为真(TRUE)时才会跳出循环,否则反复执行循环体。
与 Java 的类比:do-while vs until
虽然 Java 中没有直接对应的 until 关键字,但我们可以通过 do-while 或 while 模拟类似行为。
Java 示例:模拟倒计时
public class Countdown {
public static void main(String[] args) throws InterruptedException {
int counter = 5;
// 使用 while 模拟 until 逻辑
while (!(counter < 0)) {
System.out.println("倒计时: " + counter);
Thread.sleep(1000);
counter--;
}
System.out.println("💥 发射!");
}
}或者更贴近 until 语义的方式 —— 使用 do-while 加反转条件:
public class ServiceWaiter {
public static void main(String[] args) {
boolean isServiceReady = false;
// 模拟服务未准备好
int attempts = 0;
do {
attempts++;
System.out.println("🔁 第 " + attempts + " 次检查服务状态...");
// 模拟服务在第 3 次尝试时准备就绪
if (attempts >= 3) {
isServiceReady = true;
System.out.println("✅ 服务已就绪!");
} else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} while (!isServiceReady); // 直到服务准备好为止
System.out.println("🚀 继续执行后续任务...");
}
}Java 与 Shell 的思维映射:
- Shell
until condition≈ Javawhile (!condition) - Shell 更注重“命令执行状态”(退出码)
- Java 更注重“布尔表达式值”
嵌套 until 循环:多层控制结构
有时候单一循环无法满足需求,我们需要嵌套循环来处理更复杂的逻辑。例如:逐行读取文件,对每行内容再进行字符级处理。
#!/bin/bash
# 创建测试数据
cat > test.txt <<EOF
apple
banana
cherry
EOF
line_num=1
while IFS= read -r line; do
echo "第 $line_num 行: $line"
char_index=0
len=${#line}
until [ $char_index -ge $len ]; do
char="${line:$char_index:1}"
echo " 字符 $((char_index + 1)): $char"
char_index=$((char_index + 1))
done
line_num=$((line_num + 1))
echo "---"
done < test.txt
输出:
第 1 行: apple 字符 1: a 字符 2: p 字符 3: p 字符 4: l 字符 5: e --- 第 2 行: banana 字符 1: b 字符 2: a 字符 3: n 字符 4: a 字符 5: n 字符 6: a --- ...
这个例子展示了如何在外层使用 while read 读取文件,在内层使用 until 遍历字符串中的每个字符。
实战场景三:文件锁轮询机制
在多进程或多脚本协作环境中,经常需要通过文件锁机制避免资源冲突。我们可以用 until 实现“等待锁文件消失后再继续”的逻辑:
#!/bin/bash
LOCKFILE="/tmp/myapp.lock"
echo "🔒 检查锁文件..."
until [ ! -f "$LOCKFILE" ]; do
echo "⏳ 锁文件存在,等待 5 秒..."
sleep 5
done
echo "🔓 锁已释放,获取资源中..."
# 创建自己的锁
touch "$LOCKFILE"
# 执行关键操作
echo "📂 正在处理数据..."
sleep 10
# 释放锁
rm -f "$LOCKFILE"
echo "✅ 任务完成,锁已移除。"
这种模式非常适合批处理任务、定时任务或分布式脚本协调。
实战场景四:网络重连机制
在网络不稳定或远程服务偶发故障的环境中,自动重试是保障程序鲁棒性的关键。until 循环非常适合实现带最大重试次数的重连逻辑:
#!/bin/bash
MAX_RETRIES=5
retry_count=0
until curl -f https://httpbin.org/status/200 > /dev/null 2>&1; do
retry_count=$((retry_count + 1))
if [ $retry_count -gt $MAX_RETRIES ]; then
echo "❌ 超过最大重试次数 ($MAX_RETRIES),放弃连接。"
exit 1
fi
echo "📶 第 $retry_count 次连接失败,5 秒后重试..."
sleep 5
done
echo "🌐 连接成功!"
数学计算场景:求最小公倍数
until 不仅适用于系统管理,也能用于算法实现。比如,我们可以用它来找两个数的最小公倍数(LCM):
#!/bin/bash
read -p "请输入第一个正整数: " num1
read -p "请输入第二个正整数: " num2
# 确保输入合法
until [[ "$num1" =~ ^[0-9]+$ ]] && [ $num1 -gt 0 ]; do
read -p "第一个数必须是正整数,请重新输入: " num1
done
until [[ "$num2" =~ ^[0-9]+$ ]] && [ $num2 -gt 0 ]; do
read -p "第二个数必须是正整数,请重新输入: " num2
done
# 计算 LCM 的简单方法:从较大数开始递增,直到能被两数整除
lcm=$num1
if [ $num2 -gt $lcm ]; then
lcm=$num2
fi
until [ $((lcm % num1)) -eq 0 ] && [ $((lcm % num2)) -eq 0 ]; do
lcm=$((lcm + 1))
done
echo "🔢 $num1 和 $num2 的最小公倍数是: $lcm"
虽然这不是最高效的算法(推荐用 GCD 方法),但它清晰展示了 until 在数值计算中的应用。
单元测试风格:验证多个条件
在脚本开发中,有时需要确保多个前置条件都满足后才执行主逻辑。我们可以用 until + 逻辑组合实现“等待所有条件就绪”:
#!/bin/bash
check_condition_1() {
# 模拟检查磁盘空间
df / | awk 'NR==2 {print $4}' | grep -qE '^[0-9]{5,}$'
}
check_condition_2() {
# 模拟检查网络连通性
ping -c 1 google.com > /dev/null 2>&1
}
check_condition_3() {
# 模拟检查配置文件存在
[ -f "/etc/myapp/config.conf" ]
}
echo "🧪 正在验证系统环境..."
until check_condition_1 && check_condition_2 && check_condition_3; do
echo "⚠️ 环境未准备好,5 秒后重试..."
sleep 5
done
echo "✅ 所有前置条件已满足,启动主程序..."
这种方式特别适合 CI/CD 环境、容器启动脚本或云平台初始化脚本。
高级技巧:结合函数与 until
将 until 与函数结合,可以写出高度模块化、可复用的脚本组件:
#!/bin/bash
# 定义重试函数
retry_until_success() {
local max_attempts=$1
shift
local cmd=("$@")
local attempt=0
until "${cmd[@]}"; do
attempt=$((attempt + 1))
if [ $attempt -ge $max_attempts ]; then
echo "❌ 命令执行失败,已达到最大重试次数: $max_attempts"
return 1
fi
echo "🔁 第 $attempt 次重试: ${cmd[*]}"
sleep 2
done
echo "✅ 命令执行成功!"
}
# 使用示例
retry_until_success 3 curl -f http://example.com
retry_until_success 5 ls /nonexistent/path
retry_until_success 2 echo "Hello World"
这个 retry_until_success 函数接受最大重试次数和要执行的命令,通用性极强,可复用于各种场景。
性能考量:避免无限循环
虽然 until 很强大,但如果条件永远不成立,就会陷入无限循环,导致脚本挂起、资源耗尽。因此,务必设置超时或最大重试次数。
危险示例(可能死循环):
# 如果 /tmp/flag 永远不会被创建,脚本将永远等待
until [ -f /tmp/flag ]; do
sleep 1
done
安全改进版:
timeout=60
start_time=$(date +%s)
until [ -f /tmp/flag ] || [ $(($(date +%s) - start_time)) -gt $timeout ]; do
echo "⏳ 等待标志文件,已等待 $(( $(date +%s) - start_time )) 秒..."
sleep 5
done
if [ ! -f /tmp/flag ]; then
echo "⏰ 超时!标志文件未在 $timeout 秒内出现。"
exit 1
fi
echo "✅ 标志文件已找到,继续执行..."
与其他 Shell 结构的配合
until 可以和 if、case、break、continue 等结构灵活组合,构建复杂逻辑。
示例:带中断机制的交互式菜单
#!/bin/bash
selected=false
until $selected; do
echo "=== 主菜单 ==="
echo "1) 查看系统信息"
echo "2) 清理临时文件"
echo "3) 退出"
read -p "请选择操作 (1-3): " choice
case $choice in
1)
echo "🖥️ 系统信息:"
uname -a
echo "---"
;;
2)
echo "🧹 正在清理 /tmp 下的临时文件..."
rm -rf /tmp/*
echo "✅ 清理完成。"
;;
3)
echo "👋 再见!"
selected=true
;;
*)
echo "❌ 无效选择,请输入 1、2 或 3。"
;;
esac
if ! $selected; then
read -p "按回车键返回菜单..." _
clear
fi
done
这个例子中,until 控制整个菜单循环,case 处理具体选项,selected 变量作为退出条件。
网络请求状态监控(结合 API)
现代运维脚本常需与 REST API 交互。我们可以用 until 监控异步任务的状态,直到完成:
#!/bin/bash
TASK_ID="task_12345"
API_URL="https://api.example.com/tasks/$TASK_ID/status"
echo "📡 查询任务 $TASK_ID 状态..."
until status=$(curl -s "$API_URL" | jq -r '.status') && [ "$status" = "completed" ]; do
case "$status" in
"pending"|"running")
echo "🕒 任务状态: $status,10 秒后重查..."
;;
"failed")
echo "❌ 任务失败!"
exit 1
;;
*)
echo "⚠️ 未知状态: $status"
;;
esac
sleep 10
done
echo "🎉 任务 $TASK_ID 已完成!"
Java 对比:模拟 API 轮询
下面是上述 Shell 脚本的 Java 版本,使用 HttpClient 和 ObjectMapper:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TaskPoller {
private static final String TASK_ID = "task_12345";
private static final String API_URL = "https://api.example.com/tasks/" + TASK_ID + "/status";
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newHttpClient();
ObjectMapper mapper = new ObjectMapper();
System.out.println("📡 查询任务 " + TASK_ID + " 状态...");
String status;
do {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
JsonNode json = mapper.readTree(response.body());
status = json.get("status").asText();
switch (status) {
case "pending", "running" -> {
System.out.println("🕒 任务状态: " + status + ",10 秒后重查...");
Thread.sleep(10000);
}
case "failed" -> {
System.out.println("❌ 任务失败!");
System.exit(1);
}
default -> System.out.println("⚠️ 未知状态: " + status);
}
} while (!"completed".equals(status));
System.out.println("🎉 任务 " + TASK_ID + " 已完成!");
}
}注意:Java 版本需要引入 Jackson 依赖(如 com.fasterxml.jackson.core:jackson-databind)来解析 JSON。
实用工具函数库:封装 until 逻辑
为了提高脚本复用性,我们可以创建一个工具函数库,集中管理常见的 until 模式:
#!/bin/bash
# utils.sh - until 循环工具库
wait_for_file() {
local file_path="$1"
local timeout="${2:-60}"
local interval="${3:-5}"
local start_time=$(date +%s)
echo "⏳ 等待文件: $file_path"
until [ -f "$file_path" ]; do
local elapsed=$(( $(date +%s) - start_time ))
if [ $elapsed -gt $timeout ]; then
echo "⏰ 超时!文件 $file_path 未在 $timeout 秒内出现。"
return 1
fi
echo "⏱️ 已等待 $elapsed 秒,$interval 秒后重试..."
sleep $interval
done
echo "✅ 文件 $file_path 已就绪。"
}
wait_for_port() {
local host="$1"
local port="$2"
local timeout="${3:-30}"
local interval="${4:-3}"
local start_time=$(date +%s)
echo "🔌 等待端口 $host:$port 开放..."
until nc -z "$host" "$port" 2>/dev/null; do
local elapsed=$(( $(date +%s) - start_time ))
if [ $elapsed -gt $timeout ]; then
echo "⏰ 超时!端口 $host:$port 未在 $timeout 秒内开放。"
return 1
fi
echo "⏱️ 已等待 $elapsed 秒,$interval 秒后重试..."
sleep $interval
done
echo "✅ 端口 $host:$port 已开放。"
}
wait_for_command() {
local cmd="$1"
local timeout="${2:-60}"
local interval="${3:-5}"
local start_time=$(date +%s)
echo "⚙️ 等待命令成功执行: $cmd"
until eval "$cmd" >/dev/null 2>&1; do
local elapsed=$(( $(date +%s) - start_time ))
if [ $elapsed -gt $timeout ]; then
echo "⏰ 超时!命令未在 $timeout 秒内成功。"
return 1
fi
echo "⏱️ 已等待 $elapsed 秒,$interval 秒后重试..."
sleep $interval
done
echo "✅ 命令执行成功: $cmd"
}
然后在主脚本中引用:
#!/bin/bash source ./utils.sh wait_for_file "/var/log/app.log" 120 10 wait_for_port "localhost" 8080 45 5 wait_for_command "systemctl is-active myservice" 60 3
这种模块化设计极大提升了脚本的可维护性和可测试性。
测试驱动开发(TDD)风格脚本
虽然 Shell 脚本通常不强调 TDD,但我们可以借鉴其思想 —— 先写“期望条件”,再写实现。
#!/bin/bash
# test_database_ready.sh
expect_database_ready() {
# 期望:数据库应能响应查询
mysql -e "SELECT 1;" > /dev/null 2>&1
}
echo "🧪 运行测试:数据库是否就绪?"
until expect_database_ready; do
echo "🔁 数据库未就绪,重试中..."
sleep 5
done
echo "✅ 测试通过:数据库已就绪!"
这种方式让脚本逻辑更清晰,也便于后期扩展测试用例。
最佳实践总结
在使用 until 循环时,请牢记以下最佳实践:
- ✅ 始终设置超时或最大重试次数 —— 避免死循环。
- ✅ 提供清晰的日志输出 —— 方便调试和监控。
- ✅ 将复杂条件封装成函数 —— 提高可读性和复用性。
- ✅ 优先使用内置命令或轻量工具 —— 如
test、[[ ]]、nc、curl -f。 - ✅ 考虑并发安全 —— 在多实例环境下使用文件锁或信号量。
- ✅ 记录执行时间 —— 用于性能分析和告警。
- ✅ 提供退出码 —— 便于父脚本或调度系统判断执行结果。
高级应用:动态条件生成
有时条件不是静态的,而是根据上下文动态变化。我们可以结合数组、配置文件等实现灵活控制:
#!/bin/bash
# 从配置文件读取需检查的服务列表
mapfile -t services < services.conf
echo "🔍 检查以下服务状态: ${services[*]}"
all_ready=false
until $all_ready; do
all_ready=true
for service in "${services[@]}"; do
if ! systemctl is-active --quiet "$service"; then
echo "⚠️ 服务 $service 未运行"
all_ready=false
fi
done
if ! $all_ready; then
echo "⏳ 仍有服务未就绪,10 秒后重试..."
sleep 10
fi
done
echo "✅ 所有服务均已启动!"
配置文件 services.conf:
nginx postgresql redis-server myapp-backend
监控脚本:资源使用率阈值控制
企业级脚本常需监控 CPU、内存、磁盘等资源。我们可以用 until 实现“资源低于阈值才继续”:
#!/bin/bash
CPU_THRESHOLD=80
MEM_THRESHOLD=90
DISK_THRESHOLD=85
echo "📊 监控系统资源使用率..."
until \
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') && \
mem_usage=$(free | grep Mem | awk '{print $3/$2 * 100.0}') && \
disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//') && \
(( $(echo "$cpu_usage < $CPU_THRESHOLD" | bc -l) )) && \
(( $(echo "$mem_usage < $MEM_THRESHOLD" | bc -l) )) && \
[ $disk_usage -lt $DISK_THRESHOLD ]; do
echo "📈 当前资源使用率 — CPU: ${cpu_usage%.*}%, 内存: ${mem_usage%.*}%, 磁盘: ${disk_usage}%"
echo "⏳ 资源使用过高,等待 30 秒..."
sleep 30
done
echo "✅ 资源使用率已降至安全范围,继续执行任务..."
自动化部署流水线中的 until
在 CI/CD 流水线中,until 常用于等待构建产物、镜像推送完成、服务健康检查通过等环节:
#!/bin/bash
IMAGE_NAME="myapp:latest"
DEPLOYMENT_NAME="myapp-prod"
echo "🚀 开始部署 $IMAGE_NAME 到 $DEPLOYMENT_NAME"
# 1. 构建并推送镜像
docker build -t $IMAGE_NAME .
docker push $IMAGE_NAME
# 2. 更新 Kubernetes Deployment
kubectl set image deployment/$DEPLOYMENT_NAME myapp-container=$IMAGE_NAME
# 3. 等待 Pod 就绪
echo "⏳ 等待新 Pod 就绪..."
until kubectl rollout status deployment/$DEPLOYMENT_NAME --timeout=5s 2>/dev/null; do
echo "🔄 检查部署状态..."
sleep 5
done
# 4. 验证服务端点
echo "🔌 验证服务端点可达..."
until curl -f http://myapp.prod.svc.cluster.local:8080/health; do
echo "🔁 服务未响应,5 秒后重试..."
sleep 5
done
echo "✅ 部署成功!🎉"
这种模式确保了部署过程的原子性和可靠性。
总结:为什么你应该掌握 until 循环
until 循环虽然不如 for 和 while 那么常用,但在特定场景下具有不可替代的优势:
- ✅ 语义清晰:表达“直到…为止”的意图非常自然。
- ✅ 逻辑简洁:避免双重否定或复杂条件反转。
- ✅ 健壮性强:特别适合重试、轮询、等待类任务。
- ✅ 易于调试:条件判断直观,日志输出明确。
- ✅ 跨领域适用:从系统管理到 DevOps,从数据处理到网络监控。
掌握 until,不仅是学会一种语法结构,更是掌握了一种“逆向控制流”的思维方式 —— 有时候,从“失败”出发,反而更容易抵达“成功”。
最后的小贴士
- 在脚本开头加上
set -euo pipefail,提升健壮性。 - 使用
shellcheck工具检查脚本语法和潜在问题。 - 为长时间运行的
until循环添加进度指示或日志轮转。 - 考虑使用
timeout命令包裹整个until块作为兜底保护:
timeout 300 bash -c '
until condition; do
sleep 5
done
' || echo "⏰ 整体超时!"
无论你是系统管理员、DevOps 工程师、数据分析师,还是单纯热爱自动化脚本的开发者,until 循环都值得你收入工具箱。它或许低调,但从不缺席关键时刻。
现在,打开你的终端,写一个 until 循环,让机器为你“等到天荒地老”吧!
以上就是Shell脚本中until循环语句的用法详解的详细内容,更多关于Shell脚本until循环语句的资料请关注脚本之家其它相关文章!
相关文章
linux shell中单引号、双引号、反引号、反斜杠的区别
shell可以识别4种不同类型的引字符号: 单引号字符' 双引号字符" 反斜杠字符\ 反引号字符`的区别,学习shell编程的朋友可以看下2013-01-01
jenkins pipeline中获取shell命令的标准输出或者状态的方法小结
这篇文章主要介绍了jenkins pipeline中获取shell命令的标准输出或者状态,工作中需要获取shell 命令的执行状态,返回0或者非0,本文给大家介绍的非常详细,需要的朋友可以参考下2024-02-02


最新评论