MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Bash循环控制:for、while与until循环

2021-02-114.5k 阅读

Bash 循环控制:for、while 与 until 循环

for 循环

基本语法

在 Bash 中,for 循环用于对一组值进行迭代。其最常见的语法形式为:

for variable in list
do
    commands
done

这里的 variable 是循环变量,list 是一系列值的列表,commands 是每次迭代时要执行的命令。例如:

for num in 1 2 3 4 5
do
    echo "当前数字: $num"
done

在这个例子中,num 依次取 12345,每次迭代都会执行 echo "当前数字: $num" 这条命令,输出当前的数字。

更复杂的列表

列表不仅仅可以是简单的数字或字符串序列。它可以是命令的输出。例如,你可以遍历目录中的所有文件:

for file in $(ls)
do
    echo "文件: $file"
done

这里 $(ls) 命令列出当前目录中的所有文件和目录,for 循环对每个文件名进行迭代。不过,使用 ls 这种方式有一些潜在问题,比如文件名中有空格时可能会导致解析错误。更好的方式是使用 glob 模式:

for file in *
do
    echo "文件: $file"
done

* 是一个 glob 模式,表示当前目录下的所有文件和目录,这样就避免了文件名中有空格带来的问题。

C 风格的 for 循环

Bash 也支持类似 C 语言风格的 for 循环,语法如下:

for (( expression1; expression2; expression3 ))
do
    commands
done

expression1 通常用于初始化循环变量,expression2 是循环条件,expression3 用于更新循环变量。例如:

for (( i = 1; i <= 5; i++ ))
do
    echo "数字: $i"
done

在这个例子中,i 初始化为 1,只要 i 小于等于 5,循环就会继续,每次循环结束后 i 自增 1

嵌套 for 循环

你可以在一个 for 循环内部再嵌套一个 for 循环。这在处理二维数据结构等场景下非常有用。例如,要生成一个简单的乘法表:

for (( i = 1; i <= 9; i++ ))
do
    for (( j = 1; j <= 9; j++ ))
    do
        result=$((i * j))
        printf "%4d" $result
    done
    echo
done

这里外层循环控制行数,内层循环控制每行的列数。每次内层循环完成后,通过 echo 换行,从而生成一个完整的乘法表。

while 循环

基本语法

while 循环会在条件为真时不断执行一组命令。其语法为:

while condition
do
    commands
done

condition 是一个条件表达式,只要这个表达式返回真(通常返回值为 0 表示真),commands 就会被执行。例如,下面的代码实现了一个简单的计数器:

count=1
while [ $count -le 5 ]
do
    echo "计数: $count"
    count=$((count + 1))
done

这里 [ $count -le 5 ] 是条件表达式,-le 表示小于等于。只要 count 小于等于 5,循环就会继续,每次循环中 count 自增 1

读取文件内容

while 循环常用于逐行读取文件内容。例如,假设有一个文件 example.txt,内容如下:

line1
line2
line3

可以使用以下代码逐行读取并输出文件内容:

while read line
do
    echo "读取到: $line"
done < example.txt

这里 < example.txt 表示从文件 example.txt 中读取输入,read line 每次读取文件的一行并赋值给 line 变量,然后执行 echo 命令输出该行内容。

无限循环

你可以通过设置一个永远为真的条件来创建一个无限循环。例如:

while true
do
    echo "这是一个无限循环,按 Ctrl+C 停止"
    sleep 1
done

这里 while true 确保条件永远为真,循环会不断执行。sleep 1 命令使每次循环暂停 1 秒,这样不会过度占用系统资源。要停止这个循环,用户可以按下 Ctrl+C 组合键。

until 循环

基本语法

until 循环与 while 循环相反,它会在条件为假时执行一组命令,直到条件变为真时停止。其语法为:

until condition
do
    commands
done

例如,以下代码实现了一个递减计数器:

count=5
until [ $count -le 0 ]
do
    echo "计数: $count"
    count=$((count - 1))
done

这里 [ $count -le 0 ] 是条件表达式,只要 count 不小于等于 0,即条件为假,循环就会继续执行。每次循环 count 递减 1,当 count 小于等于 0 时,条件为真,循环停止。

与 while 循环的比较

whileuntil 循环本质上是可以相互转换的。例如,上述 until 循环的递减计数器示例,也可以用 while 循环实现:

count=5
while [ $count -gt 0 ]
do
    echo "计数: $count"
    count=$((count - 1))
done

这里 [ $count -gt 0 ]until 循环中的条件正好相反。while 循环在条件为真时执行,而 until 循环在条件为假时执行。

应用场景

until 循环在某些情况下更符合逻辑。比如,假设你要等待某个文件出现,直到文件存在才继续执行其他操作,就可以使用 until 循环:

until [ -f target_file.txt ]
do
    echo "目标文件不存在,等待..."
    sleep 5
done
echo "目标文件已存在,继续执行其他操作"

这里 [ -f target_file.txt ] 用于检查 target_file.txt 文件是否存在。如果文件不存在,条件为假,until 循环会不断执行,每隔 5 秒输出一次提示信息。当文件存在时,条件为真,循环停止,继续执行后续操作。

循环控制语句

break 语句

break 语句用于立即终止当前循环。在 forwhileuntil 循环中都可以使用。例如,在一个 for 循环中,当找到特定值时停止循环:

for num in 1 2 3 4 5 6 7 8 9 10
do
    if [ $num -eq 5 ]
    then
        break
    fi
    echo "数字: $num"
done

这里当 num 等于 5 时,break 语句被执行,循环立即终止,因此只会输出 14

在嵌套循环中,break 语句默认只终止最内层循环。例如:

for (( i = 1; i <= 3; i++ ))
do
    for (( j = 1; j <= 3; j++ ))
    do
        if [ $i -eq 2 -a $j -eq 2 ]
        then
            break
        fi
        echo "i: $i, j: $j"
    done
done

i 等于 2j 等于 2 时,内层循环被终止,但外层循环会继续执行。

continue 语句

continue 语句用于跳过当前循环的剩余部分,直接进入下一次迭代。例如,在一个 for 循环中,跳过特定值:

for num in 1 2 3 4 5 6 7 8 9 10
do
    if [ $num -eq 5 ]
    then
        continue
    fi
    echo "数字: $num"
done

这里当 num 等于 5 时,continue 语句被执行,跳过当前循环的 echo 语句,直接进入下一次迭代,因此输出中不会包含 5

在嵌套循环中,continue 同样只影响最内层循环。例如:

for (( i = 1; i <= 3; i++ ))
do
    for (( j = 1; j <= 3; j++ ))
    do
        if [ $i -eq 2 -a $j -eq 2 ]
        then
            continue
        fi
        echo "i: $i, j: $j"
    done
done

i 等于 2j 等于 2 时,内层循环跳过当前迭代的 echo 语句,继续下一次内层循环迭代。

循环与数组的结合使用

for 循环遍历数组

在 Bash 中,数组是一种常用的数据结构。可以使用 for 循环遍历数组。例如,定义一个数组并遍历:

fruits=("apple" "banana" "cherry")
for fruit in ${fruits[@]}
do
    echo "水果: $fruit"
done

这里 ${fruits[@]} 表示数组 fruits 的所有元素。for 循环依次将每个元素赋值给 fruit 变量,并执行 echo 命令输出。

while 循环结合数组索引遍历

除了直接遍历数组元素,还可以使用 while 循环结合数组索引来遍历数组。例如:

numbers=(10 20 30 40 50)
index=0
while [ $index -lt ${#numbers[@]} ]
do
    number=${numbers[$index]}
    echo "数字: $number"
    index=$((index + 1))
done

这里 ${#numbers[@]} 表示数组 numbers 的元素个数。while 循环通过比较索引 index 与数组元素个数来控制循环,每次循环获取当前索引对应的数组元素并输出,然后索引自增 1

until 循环处理数组特定条件

假设你有一个数组,要找到第一个大于 50 的数,可以使用 until 循环:

nums=(10 30 70 50 90)
index=0
until [ ${nums[$index]} -gt 50 -o $index -eq ${#nums[@]} ]
do
    index=$((index + 1))
done
if [ $index -lt ${#nums[@]} ]
then
    echo "第一个大于 50 的数: ${nums[$index]}"
else
    echo "没有找到大于 50 的数"
fi

这里 until 循环在当前数组元素大于 50 或者索引等于数组元素个数时停止。如果找到了大于 50 的数,就输出该数,否则输出提示信息。

循环性能优化

减少循环内部的命令执行

在循环内部尽量减少复杂命令的执行次数。例如,避免在每次循环中都执行文件系统操作或启动新的进程。假设你要对目录中的文件进行处理,不要在循环中每次都使用 ls 获取文件列表,而是先获取列表再进行循环:

files=(*)
for file in ${files[@]}
do
    # 对文件进行处理的命令
    echo "处理文件: $file"
done

相比在循环中使用 for file in $(ls),这种方式只获取一次文件列表,减少了 ls 命令的执行次数,提高了性能。

使用 C 风格 for 循环的性能优势

在一些情况下,C 风格的 for 循环可能具有更好的性能。例如,在进行简单的数值迭代时,C 风格的 for 循环不需要额外的变量赋值操作。比较以下两种方式:

# 传统 for 循环
for num in $(seq 1 1000000)
do
    :
done

# C 风格 for 循环
for (( i = 1; i <= 1000000; i++ ))
do
    :
done

在这个简单的计数循环中,C 风格的 for 循环避免了 seq 命令的执行和变量赋值操作,性能会更好一些,尤其是在迭代次数较多的情况下。

避免不必要的循环嵌套

尽量减少循环嵌套的层数。每增加一层循环嵌套,循环的时间复杂度会显著增加。例如,如果可以通过其他方式解决问题,就不要使用多层嵌套循环。假设你要在两个数组中查找匹配的元素,一种暴力的方式是使用两层嵌套循环:

array1=(1 2 3 4 5)
array2=(3 4 5 6 7)
for num1 in ${array1[@]}
do
    for num2 in ${array2[@]}
    do
        if [ $num1 -eq $num2 ]
        then
            echo "匹配: $num1"
        fi
    done
done

但如果数组较大,这种方式会非常慢。可以先对数组进行排序,然后使用更高效的算法(如双指针法)来查找匹配元素,这样可以避免不必要的循环嵌套,提高性能。

实际应用案例

批量文件处理

假设你有一个目录,里面包含很多文本文件,你要在每个文件中查找特定的字符串,并输出包含该字符串的行。可以使用 for 循环来实现:

search_string="特定字符串"
for file in *.txt
do
    if grep -q "$search_string" $file
    then
        echo "在文件 $file 中找到:"
        grep "$search_string" $file
    fi
done

这里 for 循环遍历当前目录下所有的 .txt 文件,grep -q "$search_string" $file 用于检查文件中是否包含特定字符串,-q 选项表示安静模式,不输出匹配行,只返回状态码。如果找到,就输出文件名并再次使用 grep 输出包含该字符串的行。

系统监控脚本

可以使用 while 循环实现一个简单的系统监控脚本,每隔一段时间检查系统的 CPU 使用率。例如:

while true
do
    cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}')
    echo "当前 CPU 使用率: $cpu_usage%"
    sleep 5
done

这里 while true 创建一个无限循环,top -bn1 命令获取一次系统状态信息,通过 grepawk 提取 CPU 使用率,然后输出。sleep 5 使循环每隔 5 秒执行一次。

自动化部署脚本

在自动化部署过程中,until 循环可以用于等待服务器准备好。例如,假设要在服务器上部署一个应用,需要等待服务器的端口开启:

server_ip="192.168.1.100"
port=8080
until nc -z $server_ip $port
do
    echo "服务器端口未开启,等待..."
    sleep 10
done
echo "服务器端口已开启,开始部署应用"
# 部署应用的命令

这里 nc -z $server_ip $port 用于检查指定服务器的指定端口是否开启,-z 选项表示零 I/O 模式,只报告连接状态。until 循环会不断检查,直到端口开启,然后开始部署应用。

循环中的错误处理

检查命令执行状态

在循环中执行命令时,要检查命令的执行状态。例如,在文件处理循环中,如果删除文件失败,应该有相应的处理:

for file in *.tmp
do
    if ! rm -f $file
    then
        echo "删除文件 $file 失败"
    fi
done

这里 ! rm -f $file 表示执行 rm -f $file 命令,如果命令执行失败(返回非零状态码),if 条件成立,输出错误信息。

处理循环终止异常

有时候循环可能会因为意外情况提前终止,比如在读取文件时文件损坏。可以使用 trap 命令来捕获信号并进行处理。例如,在读取文件的 while 循环中捕获 SIGINT 信号(用户按下 Ctrl+C):

trap 'echo "循环被中断,进行清理操作"; exit 1' SIGINT
while read line
do
    # 对读取到的行进行处理的命令
    echo "处理行: $line"
done < example.txt

这里 trap 'echo "循环被中断,进行清理操作"; exit 1' SIGINT 表示当捕获到 SIGINT 信号时,输出提示信息并退出脚本,同时可以在提示信息后添加相应的清理操作命令。

总结循环控制的要点

  • 选择合适的循环类型for 循环适用于已知迭代次数或遍历列表的场景;while 循环适用于条件判断决定循环次数的场景;until 循环则适用于条件为假时执行循环的场景。
  • 注意循环控制语句breakcontinue 可以灵活控制循环的执行流程,但要注意它们在嵌套循环中的作用范围。
  • 性能优化:减少循环内部的复杂操作,合理选择循环类型,避免不必要的循环嵌套,以提高脚本的执行效率。
  • 错误处理:在循环中要检查命令执行状态,处理可能出现的异常情况,确保脚本的稳定性和可靠性。

通过掌握这些要点,你可以在 Bash 脚本编写中更加灵活、高效地使用循环控制结构,完成各种复杂的任务。无论是系统管理、自动化脚本编写还是数据处理,循环控制都是非常重要的一部分。希望通过本文的介绍,你对 Bash 中的 forwhileuntil 循环有了更深入的理解和掌握,能够在实际工作中运用自如。