在 Shell 脚本编程中,循环是实现自动化和处理重复性任务的核心构建块。
通过循环,我们可以高效地遍历项目列表、重复执行命令直到满足特定条件,或与用户进行交互。
Bash (Bourne Again Shell) 作为最流行的 Shell 之一,提供了多种强劲的循环结构,其中 for、while、until 和 select 各有其独特的应用场景和工作原理。
本文将深入探讨这四种循环结构的原理,并通过全面的实验代码,从基础用法到高级并发处理,展示它们的实际应用。
01
for 循环:遍历列表的利器
for 循环是处理一组已知项目的理想选择。
它会遍历一个列表(如文件名、字符串、数字序列等),并在每次迭代中为变量赋予列表中的一个值,然后执行循环体内的命令。
1
原理
for 循环的基本语法是 for 变量 in 列表; do … done。
它的执行流程如下:
1)从列表中取出第一个项目,并将其赋值给指定的变量。
2)执行 do 和 done 之间的命令块。
3)返回列表,取出下一个项目并重复此过程,直到列表中的所有项目都被处理完毕。
2
基础实验
实验 1:遍历静态字符串列表
#!/bin/bash
echo "--- 遍历颜色列表 ---"
for color in "Red" "Green" "Blue"; do
echo "当前颜色: $color"
done
实验 2:遍历数字序列
#!/bin/bash
echo "--- 使用花括号扩展遍历数字 ---"
for i in {1..5}; do
echo "数字: $i"
done
实验 3:遍历文件和目录for 循环与通配符(如 *)结合,可以轻松地处理目录中的文件。
#!/bin/bash
touch file1.txt file2.log
echo "--- 遍历当前目录下的所有.txt文件 ---"
for file in *.txt; do
if [ -f "$file" ]; then
echo "找到文本文件: $file"
fi
done
实验 4:进行累加计算 for 循环超级适合进行固定次数的数值累加。
#!/bin/bash
echo "--- 计算1到100的累加和 ---"
sum=0
for i in {1..100}; do
sum=$((sum + i))
done
echo "1到100的累加和是: $sum"
实验 5:计算阶乘 for循环超级适合进行固定次数的数值累加。
#!/bin/bash
echo "--- 计算一个数的阶乘 ---"
num=5
factorial=1
# 使用C风格的for循环
for (( i=1; i<=num; i++ )); do
factorial=$((factorial * i))
done
echo "$num 的阶乘 (5!) 是: $factorial"
一个常见的陷阱:为何 for 不适合逐行读取文件?
一个常见的误区是尝试使用 for line in $(cat filename) 来逐行读取文件。
这种方法在遇到包含空格的行或空行时会彻底失效。
这是由于 for 循环依赖于 Shell 的单词分割 (Word Splitting) 机制,它会使用内部字段分隔符 IFS(默认为空格、制表符、换行符)来切分 $(cat filename) 的输出,导致行被拆成单词,空行则直接消失。
我们将在下一节 while 循环中展示处理这个问题的标准方法。
02
while 循环:当条件为真时执行
while 循环会在其指定的条件评估为真(退出状态码为 0)时,持续执行一个命令块。
它是处理未知次数迭代和 I/O 操作的理想选择。
1
原理
while 循环的语法是 while [ 条件 ]; do … done。
它的执行流程是:
1)在每次迭代开始前,检查 [ 条件 ] 是否为真。
2)如果条件为真,则执行 do 和 done 之间的命令块。
3)执行完毕后,再次回到步骤 1 进行检查,直到条件为假。
2
实验
实验 1:简单的计数器
#!/bin/bash
echo "--- while循环计数器 ---"
count=1
while [ $count -le 5 ]; do
echo "当前计数值: $count"
count=$((count + 1))
done
实验 2:逐行读取文件(标准最佳实践)
这是 while 循环最重大和最常见的用途。
通过与 read 命令结合,它可以高效、准确地处理任何文件。
1)准备测试文件 test_file.txt:
echo "第一行: 包含 空格" > test_file.txt
echo "" >> test_file.txt
echo "第三行" >> test_file.txt
echo " 第四行: 前有缩进" >> test_file.txt
2)编写 while read 脚本:
#!/bin/bash
echo "--- 使用 while read 逐行读取文件 ---"
while IFS= read -r line; do
echo "读取到: [$line]"
done < test_file.txt
3)结果与分析
输出将完美保留所有格式,包括空格和空行。
读取到: [第一行: 包含 空格]
读取到: []
读取到: [第三行]
读取到: [ 第四行: 前有缩进]
- while … < test_file.txt
通过输入重定向,高效地将文件作为循环的输入流。
- read -r line
-r 选项防止反斜杠被解释,保证内容原始性。
- IFS=
临时将 IFS 置空,是防止 read 命令裁剪行首和行尾空白的关键。
实验 3:猜数字互动游戏 while 循环超级适合创建需要反复与用户交互直到满足某个条件(如猜对数字)的程序。
#!/bin/bash
echo "--- 猜数字游戏 ---"
# 生成一个1-100之间的随机数
target=$((RANDOM % 100 + 1))
echo "我已经想好了一个1到100之间的数字,请你来猜。"
whiletrue; do
read -p "请输入你的猜测: " guess
# 检查输入是否为纯数字
if ! [[ "$guess" =~ ^[0-9]+$ ]]; then
echo "错误:请输入一个有效的数字。"
continue # 跳过本次循环的剩余部分,直接开始下一次循环
fi
if [ "$guess" -lt "$target" ]; then
echo "太小了!再试试。"
elif [ "$guess" -gt "$target" ]; then
echo "太大了!再试试。"
else
echo "祝贺你,猜对了!答案就是 $target。"
break # 猜对了,使用break退出while循环
fi
done
03
until 循环:直到条件为真时停止
until 循环与 while 循环逻辑相反。
它会持续执行一个命令块,直到其指定的条件变为真。
原理
until 循环的语法是 until [ 条件 ]; do … done。
当条件为假时执行循环体,当条件为真时终止。
它超级适合用于等待某个事件发生。
实验:等待文件创建
#!/bin/bash
file_path="/tmp/lockfile.lock"
echo "--- 等待文件 $file_path 被创建 ---"
echo "(请在另一个终端执行 'touch $file_path' 来继续)"
until [ -f "$file_path" ]; do
echo -n "."
sleep 2
done
echo -e "
文件已找到,脚本继续执行。"
rm "$file_path"
04
select 循环:创建交互式菜单
select 循环是 Bash 提供的一个独特功能,专门用于快速生成一个带编号的菜单,并获取用户的选择。
原理
select 变量 in 列表; do … done 会将列表中的项目显示为菜单,并等待用户输入数字。
用户的选择对应的项目内容会被赋值给变量。
循环会一直持续,直到遇到 break 命令。
实验:创建系统操作菜单
#!/bin/bash
PS3="请选择一个操作: "
select option in "显示日期" "查看磁盘空间" "退出脚本"; do
case $option in
"显示日期") echo "当前日期: $(date)";;
"查看磁盘空间") df -h;;
"退出脚本") echo "脚本退出。"; break;;
*) echo "无效选项 '$REPLY'";;
esac
done
05
高级主题一:for 循环读取文件与
空行处理的思路
在实际工作中,我们常常需要逐行读取文件内容。
一个常见的误区是直接使用 for 循环来处理。
让我们通过实验来揭示其问题所在,并找出正确的处理思路。
1
错误思路与实验
许多人会尝试用 for line in $(cat filename) 的方式来读取文件。
这种方法在处理简单、无空格的文本时似乎可行,但一旦遇到包含空格的行或空行,就会出现问题。
实验:使用 for 循环处理包含空行和空格的文件
1)第一,创建一个包含多种情况的测试文件 test_file.txt:
echo "第一行: 包含 空格" > test_file.txt
echo "" >> test_file.txt
echo "第三行" >> test_file.txt
echo " 第四行: 前有缩进" >> test_file.txt
2)编写并执行以下脚本:
#!/bin/bash
echo "--- 错误的for循环读取方法 ---"
for word in $(cat test_file.txt); do
echo "读取到: [$word]"
done
3)实验结果分析:
你会看到如下输出
读取到: [第一行:]
读取到: [包含]
读取到: [空格]
读取到: [第三行]
读取到: [第四行:]
读取到: [前有缩进]
问题显而易见
- 空行丢失
第二行的空行完全没有被读取到。
- 按词分割
for 循环并没有按行读取,而是被 Shell 的单词分割 (Word Splitting) 机制影响了。
它使用内部字段分隔符 IFS(默认为空格、制表符、换行符)将 $(cat test_file.txt) 的输出分割成一个个独立的单词。
因此,“第一行: 包含 空格”被拆分成了三个部分。
- 前导空格丢失
第四行的前导空格也被 IFS 当作分隔符处理掉了。
2
正确思路与实验
正确的思路是避免使用 for 循环进行行读取,而是采用 while 循环与 read 命令的组合。
这种方法是为逐行处理而设计的,可以准确控制读取过程。
实验:使用 while read 循环正确处理文件
1)使用上面创建的同一个 test_file.txt 文件。
2)编写并执行以下脚本:
#!/bin/bash
echo "--- 正确的while read读取方法 ---"
while IFS= read -r line; do
echo "读取到: [$line]"
done < test_file.txt
3)实验结果分析:
输出结果如下,完全符合预期:
读取到: [第一行: 包含 空格]
读取到: []
读取到: [第三行]
读取到: [ 第四行: 前有缩进]
思路解析:
- while … < test_file.txt
通过输入重定向,将整个文件作为 while 循环的输入。
这比 cat 更高效,由于它不会一次性将文件内容全部加载到内存中。
- read -r line
read 命令一次从输入中读取一行。
-r 选项至关重大,它能防止反斜杠 被解释为转义字符,保证了内容的原始性。
- IFS=
这是一个关键技巧。
临时将 IFS 设置为空,是为了告知 read 命令不要裁剪行首和行尾的空白字符(如空格和制表符)。
这就是为什么第四行的前导空格得以保留的缘由。
06
高级主题二:循环中的并发处理
当循环中的任务相互独立且耗时较长时,串行执行会超级慢。
通过并发处理,我们可以同时运行多个任务,大幅缩短总时间。
1
原理
Shell 实现并发的核心是后台执行 (&) 和等待 (wait)。
- 命令 &:将命令放入后台执行,脚本不等待其完成。
- wait:阻塞脚本,直到所有后台子进程执行完毕。
实验:串行 vs. 并发
#!/bin/bash
run_task() {
echo "任务 $1 开始..."
sleep 2
echo "任务 $1 完成。"
}
echo "--- 串行执行 (耗时约10秒) ---"
start_time=$(date +%s)
for i in {1..5}; do
run_task $i
done
echo "总耗时: $(( $(date +%s) - start_time )) 秒。"
echo -e "
--- 并发执行 (耗时约2秒) ---"
start_time=$(date +%s)
for i in {1..5}; do
run_task $i & # 在后台执行任务
done
wait # 等待所有后台任务完成
echo "总耗时: $(( $(date +%s) - start_time )) 秒。"
2
控制并发数量
无限制的并发会耗尽系统资源。
我们可以使用命名管道(FIFO)创建一个“任务池”来控制同时运行的任务数量。
实验:控制并发数为 2
#!/bin/bash
# 定义一个模拟耗时的函数
run_task() {
local task_id=$1
echo "任务 $task_id 开始... (将运行3秒)"
sleep 3
echo "任务 $task_id 完成。"
}
# 设置最大并发数
MAX_JOBS=2
echo "--- 控制并发数为 $MAX_JOBS (共5个任务,每个3秒) ---"
start_time=$(date +%s)
# 1. 创建一个命名管道(FIFO)作为任务槽位
# 使用mktemp确保临时文件名唯一且安全
FIFO_PATH=$(mktemp -u)
mkfifo "$FIFO_PATH"
# 2. 设置一个陷阱(trap),确保脚本退出时(无论正常或异常)都能删除管道文件
trap "rm -f '$FIFO_PATH'" EXIT
# 3. 将文件描述符3与命名管道进行读写绑定
exec 3<>"$FIFO_PATH"
# 4. 预先向管道中放入N个“令牌”(这里是换行符),数量等于最大并发数
for ((i=0; i<$MAX_JOBS; i++)); do
# echo写入一个换行符到文件描述符3
echo >&3
done
# 5. 循环启动所有任务
for i in {1..5}; do
# 6. 从管道中读取一个“令牌”。如果管道为空,read命令会阻塞,直到有令牌可用
read -u 3
# 7. 拿到令牌后,将任务放入后台执行
{
run_task "$i"
# 8. 任务完成后,归还“令牌”到管道,以便下一个等待的任务可以开始
echo >&3
} &
done
# 9. 等待所有由该脚本启动的后台任务完成
wait
end_time=$(date +%s)
# 预期耗时: ceil(5/2) * 3秒 = 3 * 3秒 = 9秒
echo "控制并发总耗时: $((end_time - start_time)) 秒。"
# 10. 关闭文件描述符
exec 3>&-
07
总结与比较
掌握这些循环结构及其高级用法,是从 Shell 新手迈向专家的关键一步。
通过在实际工作中根据场景选择最合适的工具,并结合并发处理等高级技巧,你可以编写出功能更强劲、执行更高效的自动化脚本。
写在最后
写代码是逻辑的艺术,循环是节奏的灵魂。
下次再看到“循环体”,请记得这篇文章哦~
要玩转 Oracle,离不开对 Linux 的熟悉与掌控。
毕竟,90% 的数据库性能问题,根源都在操作系统层面。
如果你还想系统补齐 Linux 的实战短板,推荐看看刘峰老师的 Linux 系列课程,循序渐进,从命令行到运维部署,一步到位。




















暂无评论内容