Bash 零基础教程

Bash 循环结构

循环结构使你能够自动化重复性任务、高效处理数据,并创建更具动态和更强大的脚本。

在本章中,我们将深入探讨 Bash 中可用的三种主要循环结构:forwhileuntil。我们将探索它们的语法、功能和实际应用,使你具备在 Bash 脚本中高效利用它们的技能。

1. for 循环

for 循环专为遍历一系列项目(序列)而设计。这个序列可以是字符串列表、数字范围或命令的输出结果。Bash 提供了几种不同形式的 for 循环,每种都适用于不同的场景。

1.1 基础 for 循环与列表

最常见的 for 循环形式是遍历一个项目列表。

for item in item1 item2 item3 ... itemN
do
  # 针对每个项目执行的代码
  echo "正在处理项目: $item"
done

语法解析:

  • for item in item1 item2 ... itemN: 这一行启动了循环。它会依次遍历提供的列表(item1item2 等)中的每个项目。在每次循环迭代时,变量 item 都会被赋予列表中当前元素的值。
  • do: 标记循环体(需要执行的代码块)的开始。
  • echo "正在处理项目: $item": 这是针对列表中的每个项目都要执行的代码。在这里,我们只是向控制台打印一条消息,但你可以包含任何有效的 Bash 命令。$item 属于变量替换,它会被变量 item 的当前值所取代。
  • done: 标记循环体的结束。

示例:

#!/bin/bash
for fruit in apple banana cherry
do
  echo "我喜欢 $fruit"
done

输出:

我喜欢 apple
我喜欢 banana
我喜欢 cherry

1.2 结合大括号扩展的 for 循环

大括号扩展(Brace expansion)提供了一种生成数字或字符序列的便捷方式。

for i in {1..5}
do
  echo "数字: $i"
done

语法解析:

  • {1..5}: 这就是大括号扩展。它会生成一个从 1 到 5(包含 1 和 5)的数字序列。Bash 会在循环开始之前就将它展开,因此这个循环实际上等同于 for i in 1 2 3 4 5

输出:

数字: 1
数字: 2
数字: 3
数字: 4
数字: 5

你还可以指定一个步长值(Step value):

for i in {1..10..2}
do
  echo "数字: $i"
done

语法解析:

  • {1..10..2}: 这会生成一个从 1 到 10 的数字序列,每次递增步长为 2。因此,生成的序列将是 1, 3, 5, 7, 9。

输出:

数字: 1
数字: 3
数字: 5
数字: 7
数字: 9

1.3 结合命令替换的 for 循环

命令替换(Command substitution)允许你将一个命令的输出结果作为 for 循环的遍历列表。

for file in $(ls *.txt)
do
  echo "正在处理文件: $file"
done

语法解析:

  • $(ls *.txt): 这是命令替换。Bash 会执行 ls *.txt 命令,并将代码的这一部分替换为该命令的输出。在这个例子中,ls *.txt 会列出当前目录下所有扩展名为 .txt 的文件。然后,for 循环会遍历这些文件名。

示例:
假设当前目录下有以下文件:file1.txtfile2.txtimage.pngdata.txt

输出:

正在处理文件: file1.txt
正在处理文件: file2.txt
正在处理文件: data.txt

重要提示: 虽然命令替换可以工作,但在 for 循环中直接使用通配符(Globbing)通常是更好的选择(参见下一节),因为它能更好地处理带有空格或特殊字符的文件名。

1.4 结合通配符的 for 循环

通配符(Globbing)提供了一种强大的方式,可以根据模式匹配文件名。

for file in *.txt
do
  echo "正在处理文件: $file"
done

语法解析:

  • *.txt: 这是一个通配符模式。* 匹配任何字符序列。因此,*.txt 匹配任何以 .txt 结尾的文件。在循环开始之前,Bash 会将这个模式扩展为匹配的文件列表。这种方法通常比使用 ls 进行命令替换更安全、更可靠,尤其是当处理包含空格或特殊字符的文件名时。

示例:
假设文件与上一个例子相同(file1.txtfile2.txtimage.pngdata.txt):

输出:

正在处理文件: file1.txt
正在处理文件: file2.txt
正在处理文件: data.txt

1.5 C 语言风格的 for 循环

Bash 也支持 C 语言风格的 for 循环,这在进行数值迭代时非常有用。

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

语法解析:

  • (( i=1; i<=5; i++ )): 这一部分定义了循环的控制结构。
    • i=1: 将循环计数器 i 初始化为 1。
    • i<=5: 这是循环条件。只要 i 小于或等于 5,循环就会继续执行。
    • i++: 在每次迭代之后,将循环计数器 i 的值递增 1。

输出:

数字: 1
数字: 2
数字: 3
数字: 4
数字: 5

1.6 课后练习:for 循环

  1. 打印数字列表: 使用 for 循环和大括号扩展打印从 10 到 20 的数字。
  2. 处理目录中的文件: 创建一个脚本,遍历目录中的所有 .jpg 文件,并打印它们的名称和大小(使用 ls -l 命令)。记住要正确处理带有空格的文件名。
  3. 计算总和: 使用 C 语言风格的 for 循环计算从 1 到 100 的数字总和。

2. while 循环

只要给定的条件为真(true),while 循环就会不断执行一个代码块。

while [ condition ]
do
  # 当条件为真时要执行的代码
  # 重要提示:一定要包含能让条件最终变为假的代码,以避免死循环
done

语法解析:

  • while [ condition ]: 这一行启动了 while 循环。只要方括号内的 condition(条件)为真,循环内的代码就会继续执行。[ 实际上是一个等同于 test 的命令,请确保方括号两边有空格。
  • do: 标记循环体的开始。
  • done: 标记循环体的结束。
  • condition: 这是一个计算结果为真或假的表达式。它可以包含变量比较、文件存在性检查或任何其他有效的 Bash 表达式。

2.1 示例:倒计时器

#!/bin/bash
count=5

while [ $count -gt 0 ]
do
  echo "倒计时: $count"
  count=$((count - 1)) # 递减 count 的值
  sleep 1             # 等待 1 秒
done

echo "发射!"

语法解析:

  • count=5: 将变量 count 初始化为 5。
  • while [ $count -gt 0 ]: 这是循环条件。-gt 是“大于”运算符。只要 count 的值大于 0,循环就会继续。
  • echo "倒计时: $count": 打印 count 的当前值。
  • count=$((count - 1)): 将 count 的值递减 1。$((...)) 在 Bash 中用于算术扩展,它会计算括号内的表达式。
  • sleep 1: 暂停脚本执行 1 秒钟。这是为了创造一个可见的倒计时效果。
  • echo "发射!": 这一行在循环完成后(当 count 不再大于 0 时)执行。

输出:

倒计时: 5
倒计时: 4
倒计时: 3
倒计时: 2
倒计时: 1
发射!

2.2 示例:逐行读取文件

#!/bin/bash
file="mydata.txt"

# 如果文件不存在,则创建它并写入一些内容
if [ ! -f "$file" ]; then
  echo "Line 1" > "$file"
  echo "Line 2" >> "$file"
  echo "Line 3" >> "$file"
fi

while IFS= read -r line
do
  echo "读取到: $line"
done < "$file"

语法解析:

  • file="mydata.txt": 将变量 file 设置为我们要读取的文件名。
  • if 语句检查文件是否存在,如果不存在则创建它并填充一些示例内容。
  • while IFS= read -r line:
    • IFS=: 将内部字段分隔符(Internal Field Separator, IFS)设置为空字符串。这非常重要,它可以防止 read 命令剥离行首和行尾的空白字符。
    • read -r line: 从输入中读取一行并将其存储在变量 line 中。-r 选项可以防止反斜杠转义被解释。
  • done < "$file": 这会将 mydata.txt 的内容重定向到 while 循环的标准输入中。循环随后会逐行读取该文件。

输出(假设 mydata.txt 在不同的行包含 "Line 1"、"Line 2" 和 "Line 3"):

读取到: Line 1
读取到: Line 2
读取到: Line 3

重要提示:while IFS= read -r line 结构是 Bash 中逐行读取文件的推荐方法。它能正确处理空白字符和反斜杠,防止出现意外行为。

3. until 循环

until 循环与 while 循环恰好相反。只要给定的条件为假(false),它就会执行代码块。当条件变为真时,循环终止。

until [ condition ]
do
  # 当条件为假时要执行的代码
  # 重要提示:一定要包含能让条件最终变为真的代码,以避免死循环
done

语法解析:

  • until [ condition ]: 这一行启动了 until 循环。只要方括号内的 condition 为假,循环内的代码就会继续执行。
  • do: 标记循环体的开始。
  • done: 标记循环体的结束。
  • condition: 这是一个计算结果为真或假的表达式。循环会一直继续,直到(until)这个条件变为真。

3.1 示例:等待文件被创建

#!/bin/bash
file="myfile.txt"

until [ -f "$file" ]
do
  echo "等待 $file 被创建..."
  sleep 5 # 等待 5 秒
done

echo "$file 已创建。继续执行。"

语法解析:

  • file="myfile.txt": 将变量 file 设置为我们正在等待的文件名。
  • until [ -f "$file" ]: 这是循环条件。-f 是一个测试运算符,用于检查文件是否存在。只要 myfile.txt 文件不存在,循环就会继续执行。
  • echo "等待 $file 被创建...": 打印一条消息,表明脚本正在等待文件创建。
  • sleep 5: 暂停脚本执行 5 秒钟。
  • echo "$file 已创建。继续执行。": 这一行在循环完成后(即 myfile.txt 存在时)执行。

输出(根据文件何时被创建,输出行数会有所不同):

等待 myfile.txt 被创建...
等待 myfile.txt 被创建...
myfile.txt 已创建。继续执行。

3.2 示例:向上递增计数

#!/bin/bash
target=10
current=1

until [ $current -gt $target ]
do
  echo "当前值: $current"
  current=$((current + 1))
done

echo "达到目标值!"

语法解析:

  • target=10: 设置目标值为 10。
  • current=1: 将当前值初始化为 1。
  • until [ $current -gt $target ]: 循环会一直继续,直到当前值大于目标值。
  • 循环在每次迭代中将 current 变量递增 1,并打印它的值。

输出:

当前值: 1
当前值: 2
当前值: 3
当前值: 4
当前值: 5
当前值: 6
当前值: 7
当前值: 8
当前值: 9
当前值: 10
达到目标值!

4. 实战案例:将循环结构应用于系统管理

回想一下模块 1 中介绍的系统管理脚本。我们可以利用循环结构对其进行增强。假设我们想要监控多个目录的磁盘空间使用情况,并在其中任何一个超过设定阈值时发出报告。

#!/bin/bash

# 定义要监控的目录和阈值
directories="/var /tmp /home"
threshold=90  # 百分比

# 遍历每个目录
for dir in $directories
do
  # 获取磁盘空间使用百分比
  usage=$(df -h "$dir" | awk 'NR==2 {print $5}' | tr -d '%')
  
  # 检查使用率是否超过阈值
  if [ "$usage" -gt "$threshold" ]; then
    echo "警告: $dir 的磁盘使用率为 $usage%,超过了 $threshold% 的阈值。"
  fi
done

语法解析:

  • directories="/var /tmp /home": 定义一个包含要监控的目录列表的变量,目录之间用空格分隔。
  • threshold=90: 将磁盘空间使用率的报警阈值设置为 90%。
  • for dir in $directories: 遍历 $directories 变量中的每个目录。
  • usage=$(df -h "$dir" | awk 'NR==2 {print $5}' | tr -d '%'):
    • df -h "$dir": 以人类可读的格式获取当前目录的磁盘空间使用信息。
    • awk 'NR==2 {print $5}': 从 df -h 输出的第二行中提取第五列内容,该列正是使用率百分比。NR==2 选中 df 命令输出的第二行(包含使用数据的那一行),{print $5} 打印该行的第五列。
    • tr -d '%': 移除使用率百分比中的 % 符号。
  • if [ "$usage" -gt "$threshold" ]: 检查使用率是否大于阈值。
  • echo "警告: ...": 如果超过阈值,则打印一条警告消息。