linux shell

关注公众号 jb51net

关闭
首页 > 脚本专栏 > linux shell > Shell脚本until循环语句

Shell脚本中until循环语句的用法详解

作者:Jinkxs

在 Linux Shell 编程世界中,循环结构是控制程序流程、实现重复任务的核心工具之一,我们常听到 for 和 while 循环,但有一个低调却实用的反向选手:until 循环,今天,我们将深入探索 Shell 脚本中的 until 循环语句,需要的朋友可以参考下

引言

在 Linux Shell 编程世界中,循环结构是控制程序流程、实现重复任务的核心工具之一。我们常听到 forwhile 循环,但有一个低调却实用的“反向选手”——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 "💥 发射!"

对比分析:

特性untilwhile
条件判断条件为假 → 执行条件为真 → 执行
语义倾向“直到…为止”“当…的时候”
适用场景等待某事发生满足条件时持续操作

关键区别在于语义和心理模型 —— 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 岁"

这个例子中,循环会持续提示用户输入,直到满足三个条件:

流程图展示 until 循环工作原理

下面是一个用 Mermaid 绘制的 until 循环执行流程图,帮助你直观理解其控制流:

从图中可以看出,until 循环在每次迭代前都会检查条件。只有当条件为真(TRUE)时才会跳出循环,否则反复执行循环体。

与 Java 的类比:do-while vs until

虽然 Java 中没有直接对应的 until 关键字,但我们可以通过 do-whilewhile 模拟类似行为。

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 的思维映射:

嵌套 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 可以和 ifcasebreakcontinue 等结构灵活组合,构建复杂逻辑。

示例:带中断机制的交互式菜单

#!/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 版本,使用 HttpClientObjectMapper

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 循环时,请牢记以下最佳实践:

  1. 始终设置超时或最大重试次数 —— 避免死循环。
  2. 提供清晰的日志输出 —— 方便调试和监控。
  3. 将复杂条件封装成函数 —— 提高可读性和复用性。
  4. 优先使用内置命令或轻量工具 —— 如 test[[ ]]nccurl -f
  5. 考虑并发安全 —— 在多实例环境下使用文件锁或信号量。
  6. 记录执行时间 —— 用于性能分析和告警。
  7. 提供退出码 —— 便于父脚本或调度系统判断执行结果。

高级应用:动态条件生成

有时条件不是静态的,而是根据上下文动态变化。我们可以结合数组、配置文件等实现灵活控制:

#!/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 循环虽然不如 forwhile 那么常用,但在特定场景下具有不可替代的优势:

掌握 until,不仅是学会一种语法结构,更是掌握了一种“逆向控制流”的思维方式 —— 有时候,从“失败”出发,反而更容易抵达“成功”。

最后的小贴士

timeout 300 bash -c '
    until condition; do
        sleep 5
    done
' || echo "⏰ 整体超时!"

无论你是系统管理员、DevOps 工程师、数据分析师,还是单纯热爱自动化脚本的开发者,until 循环都值得你收入工具箱。它或许低调,但从不缺席关键时刻。

现在,打开你的终端,写一个 until 循环,让机器为你“等到天荒地老”吧!

以上就是Shell脚本中until循环语句的用法详解的详细内容,更多关于Shell脚本until循环语句的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文