麒麟 Linux|写Shell脚本前,一定要懂这4种循环!

在 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 系列课程,循序渐进,从命令行到运维部署,一步到位。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容