Linux Shell for 循环完全指南:从基础遍历到实战运维
概述
在 Shell 脚本中,for 循环是处理 “批量重复操作” 的核心工具 —— 它能自动遍历一组值(如列表、文件、命令输出),对每个值执行相同或相似的命令,避免手动写重复代码。
日常开发中,for 循环常用于:
- 批量处理文件(如遍历目录下所有 .log 文件并压缩);
- 循环执行命令(如对多个服务器执行 ping 测试);
- 解析结构化数据(如读取 CSV 文件批量创建用户);
- 数值迭代(如从 1 循环到 100 执行计算)。
Shell for 循环主要分为两种风格:基础列表遍历型(适合处理字符串、文件列表)和 C 语言风格型(适合数值计数),下文将从基础到进阶,覆盖所有核心用法与实战场景。
一、基础列表型 for 循环:遍历固定值 / 变量 / 命令输出
基础列表型 for 循环是 Shell 中最常用的形式,语法简洁,核心是 “遍历一组预定义的值”,每次迭代将值赋给变量并执行命令。
1. 标准语法格式
for 变量名 in 待遍历的列表
do
# 每次迭代执行的命令(可使用 $变量名 引用当前值)
命令1
命令2
done
- 关键说明:
- in:分隔 “变量名” 和 “待遍历列表”,表示 “变量从列表中依次取值”;
- 列表:可是固定值、变量、命令输出、通配符匹配结果等;
- do...done:包裹循环体命令,每次迭代执行一次;
- 循环结束后,变量仍保留最后一次迭代的值(可在循环外继续使用)。
2. 常见遍历场景(5 种核心用法)
场景 1:遍历固定值列表
直接在 in 后写多个值(用空格分隔,值含空格需加双引号),适合已知固定值的场景:
# 遍历美国州名列表,输出每个州
for state in Alabama Alaska "North Dakota" Arizona # 含空格的值需加双引号
do
echo "下一步访问的州:$state"
done
# 循环结束后,变量仍保留最后一个值
echo "最后访问的州:$state"
执行效果:
下一步访问的州:Alabama
下一步访问的州:Alaska
下一步访问的州:North Dakota # 含空格的值正确识别
下一步访问的州:Arizona
最后访问的州:Arizona
场景 2:遍历变量中的列表
将待遍历的值存入变量,再通过变量遍历(适合值较多或需动态修改的场景):
# 1. 定义初始列表变量
state_list="Alabama Alaska Arizona"
# 2. 向列表追加值(字符串拼接)
state_list="$state_list Colorado Connecticut"
# 3. 遍历变量中的列表
for state in $state_list
do
echo "你去过 $state 吗?"
done
执行效果:
你去过 Alabama 吗?
你去过 Alaska 吗?
你去过 Arizona 吗?
你去过 Colorado 吗?
你去过 Connecticut 吗?
场景 3:遍历命令输出结果
通过命令替换($(命令))将命令输出作为列表,适合动态生成值的场景(如读取文件内容、查询系统信息):
# 示例:读取 states.txt 文件内容(每行一个州名),遍历输出
# states.txt 内容:
# Alabama
# Alaska
# Arizona
for state in $(cat states.txt) # $(cat states.txt) 获取文件内容,按空格/换行分割
do
echo "推荐访问:$state"
done
执行效果:
推荐访问:Alabama
推荐访问:Alaska
推荐访问:Arizona
场景 4:遍历目录文件(通配符匹配)
用文件名通配符(*、?、[])匹配目录中的文件 / 目录,适合批量处理文件的场景(如遍历所有 .sh 脚本、所有日志文件):
# 遍历 /home/roger/test 目录下的所有文件/目录,判断类型
for file in /home/roger/test/* # * 匹配所有非隐藏文件/目录
do
if [ -d "$file" ]; then # 判断是否为目录(变量加双引号,处理含空格的文件名)
echo "$file → 是目录"
elif [ -f "$file" ]; then # 判断是否为普通文件
echo "$file → 是文件"
fi
done
执行效果:
/home/roger/test/dir1 → 是目录
/home/roger/test/my script.sh → 是文件 # 含空格的文件名正确处理
/home/roger/test/log.txt → 是文件
关键注意:遍历文件时,$file 必须加双引号!否则含空格的文件名会被拆分为多个值,导致判断错误。
场景 5:遍历多个通配符匹配结果
在列表中写多个通配符,同时遍历不同规则的文件(如匹配 .sh 和 .txt 文件):
# 同时遍历 /home/roger 下的 .bash* 隐藏文件和 badtest 文件
for file in /home/roger/.bash* /home/roger/badtest
do
if [ -e "$file" ]; then # -e 判断文件是否存在
echo "$file → 存在"
else
echo "$file → 不存在"
fi
done
执行效果:
/home/roger/.bashrc → 存在
/home/roger/.bash_profile → 存在
/home/roger/badtest → 不存在
二、C 语言风格 for 循环:数值计数与多变量迭代
如果需要 “按数值范围循环”(如从 1 到 10 计数),基础列表型 for 循环会很繁琐,此时推荐 C 语言风格 for 循环—— 支持初始化变量、循环条件、迭代操作,适合数值迭代场景。
1. 标准语法格式
for (( 初始化表达式; 循环条件; 迭代表达式 ))
do
命令1
命令2
done
- 语法特点:
- 用 ((...)) 包裹表达式(区别于基础列表型的 in);
- 初始化表达式:首次循环前执行(如 i=1);
- 循环条件:每次迭代前判断(为真则继续,如 i <= 10);
- 迭代表达式:每次迭代后执行(如 i++ 递增、i-- 递减);
- 支持数值运算(无需 $ 符号,直接写变量名)。
2. 常见用法(3 种核心场景)
场景 1:单变量数值循环(从 1 到 10)
最基础的计数场景,如输出 1 到 10 的数字:
# 从 1 循环到 10,每次 i 递增 1
for (( i=1; i <= 10; i++ ))
do
echo "当前数字:$i"
done
执行效果:
当前数字:1
当前数字:2
...
当前数字:10
场景 2:多变量迭代(同步增减)
支持同时初始化多个变量,分别定义迭代规则(如 a 递增、b 递减),适合需要同步处理两个数值的场景:
# 初始化 a=1、b=10;条件 a<=10;a 递增、b 递减
for (( a=1, b=10; a <= 10; a++, b-- ))
do
echo "$a - $b"
done
执行效果:
1 - 10
2 - 9
3 - 8
...
10 - 1
场景 3:嵌套循环(循环内套循环)
在一个 for 循环内嵌套另一个 for 循环,适合 “多维遍历” 场景(如矩阵打印、批量创建多层目录):
# 外层循环:1-3,内层循环:1-3
for (( a=1; a <= 3; a++ ))
do
echo "外层循环 $a:"
for (( b=1; b <= 3; b++ ))
do
echo " 内层循环 $b"
done
done
执行效果:
外层循环 1:
内层循环 1
内层循环 2
内层循环 3
外层循环 2:
内层循环 1
内层循环 2
内层循环 3
外层循环 3:
内层循环 1
内层循环 2
内层循环 3
注意:嵌套循环的执行次数是 “外层次数 × 内层次数”(如 3×3=9 次),避免过多层级导致性能问题。
三、进阶技巧:处理特殊数据与循环控制
实际开发中,常遇到 “含空格的文件行”“循环中途退出” 等场景,需掌握以下进阶技巧。
1. 处理含空格的文件行(修改 IFS)
默认情况下,Shell 会按空格、制表符、换行符分割列表(由 IFS 环境变量控制),导致 “含空格的文件行” 被拆分为多个值(如 North Dakota 拆为 North 和 Dakota)。
解决方法:临时修改 IFS 为换行符($'\n'),让 for 循环按 “行” 遍历,而非按 “空格” 分割:
# 示例:读取 /etc/passwd 文件(每行一个用户,含冒号分隔的字段),按行遍历
# 1. 保存原始 IFS(后续恢复,避免影响其他命令)
IFS_OLD=$IFS
# 2. 设置 IFS 为换行符(仅当前脚本生效)
IFS=$'\n'
# 3. 按行遍历 /etc/passwd
for line in $(cat /etc/passwd)
do
echo "用户信息行:$line"
# 4. 内层循环:按冒号分割当前行,提取字段(临时修改 IFS 为冒号)
IFS=:
for field in $line
do
echo " 字段:$field"
done
# 5. 恢复内层循环的 IFS 为换行符
IFS=$'\n'
done
# 6. 恢复原始 IFS(重要!避免影响后续系统命令)
IFS=$IFS_OLD
执行效果(截取部分):
用户信息行:roger:x:1000:1000:roger:/home/roger:/usr/bin/zsh
字段:roger
字段:x
字段:1000
字段:1000
字段:roger
字段:/home/roger
字段:/usr/bin/zsh
2. 循环控制:break 与 continue
默认情况下,for 循环会遍历所有值或执行到条件不满足,但可通过 break 和 continue 控制循环流程:
break:强制退出循环
- 作用:立即退出当前循环(嵌套循环中默认退出最内层循环);
- 场景:满足特定条件时停止循环(如找到目标文件后退出)。
示例 1:退出单个循环
# 遍历 1-10,当 i=5 时退出循环
for (( i=1; i <= 10; i++ ))
do
if [ $i -eq 5 ]; then # -eq 表示“等于”(数值比较)
break # 退出循环
fi
echo "当前迭代:$i"
done
echo "循环已退出"
执行效果:
当前迭代:1
当前迭代:2
当前迭代:3
当前迭代:4
循环已退出 # i=5 时触发 break,未执行 i=5 及以后
示例 2:退出外层循环(指定层级)
嵌套循环中,用 break n 指定退出 “第 n 层循环”(n=1 为内层,n=2 为外层):
# 外层循环 1-3,内层循环 1-100,当内层 b=5 时退出外层循环
for (( a=1; a <= 3; a++ ))
do
echo "外层循环 $a:"
for (( b=1; b <= 100; b++ ))
do
if [ $b -eq 5 ]; then
break 2 # 退出第 2 层循环(外层循环)
fi
echo " 内层循环 $b"
done
done
执行效果:
外层循环 1:
内层循环 1
内层循环 2
内层循环 3
内层循环 4
# b=5 时触发 break 2,直接退出外层循环,未执行外层 2、3
continue:跳过当前迭代
- 作用:跳过当前迭代的剩余命令,直接进入下一次迭代;
- 场景:满足特定条件时不执行当前迭代(如跳过空文件、跳过错误数据)。
示例:跳过 6-9 的迭代
# 遍历 1-14,跳过 6-9 的迭代
for (( i=1; i <= 14; i++ ))
do
if [ $i -gt 5 ] && [ $i -lt 10 ]; then # 当 i 在 6-9 之间时
continue # 跳过当前迭代的 echo 命令
fi
echo "当前迭代:$i"
done
执行效果:
当前迭代:1
当前迭代:2
当前迭代:3
当前迭代:4
当前迭代:5
当前迭代:10 # 跳过 6-9,直接执行 10
当前迭代:11
...
当前迭代:14
3. 循环输出重定向 / 管道
将 for 循环的所有输出重定向到文件,或通过管道传给其他命令(如 sort 排序、grep 过滤),适合批量处理输出结果。
示例 1:输出重定向到文件
# 遍历州名列表,输出到 output.txt 文件(覆盖写入)
for state in "North Dakota" Connecticut Illinois Alabama
do
echo "$state 是下一个目的地"
done > output.txt # > 表示覆盖写入,>> 表示追加写入
echo "循环输出已保存到 output.txt"
查看结果:
cat output.txt
# 输出:
# North Dakota 是下一个目的地
# Connecticut 是下一个目的地
# Illinois 是下一个目的地
# Alabama 是下一个目的地
示例 2:输出通过管道排序
bash
# 遍历州名列表,输出通过管道传给 sort 命令排序
for state in "North Dakota" Connecticut Illinois Alabama
do
echo "$state 是下一个目的地"
done | sort # 按字母顺序排序输出
echo "排序完成"
执行效果:
Alabama 是下一个目的地 # 按字母 a 开头排序
Connecticut 是下一个目的地
Illinois 是下一个目的地
North Dakota 是下一个目的地
排序完成
四、实战案例:2 个运维高频场景
掌握 for 循环后,通过实战案例巩固用法,解决实际运维问题。
案例 1:批量查找系统中的可执行文件(扫描 PATH 路径)
需求:遍历 PATH 环境变量中的所有目录,查找其中的可执行文件(-x 表示可执行),帮助管理员了解系统可用命令:
#!/bin/bash
# 批量查找 PATH 中的可执行文件
# 1. 保存原始 IFS,设置 IFS 为冒号(PATH 目录用冒号分隔)
IFS_OLD=$IFS
IFS=:
# 2. 遍历 PATH 中的每个目录
for folder in $PATH
do
echo "=== 正在扫描目录:$folder ==="
# 3. 遍历目录中的所有文件,判断是否为“可执行普通文件”
for file in $folder/*
do
# -x:判断文件是否可执行;-f:判断是否为普通文件(排除目录)
if [ -x "$file" ] && [ -f "$file" ]; then
# basename $file:提取文件名(去掉路径,如 /bin/ls → ls)
echo " 可执行文件:$(basename $file)"
fi
done
echo -e "\n" # 空行分隔不同目录的结果
done
# 4. 恢复原始 IFS,避免影响后续命令
IFS=$IFS_OLD
执行效果(截取部分):
=== 正在扫描目录:/bin ===
可执行文件:ls
可执行文件:cat
可执行文件:mkdir
=== 正在扫描目录:/usr/bin ===
可执行文件:git
可执行文件:python3
可执行文件:ssh
实用价值:快速排查系统中异常的可执行文件,或统计特定目录下的命令数量。
案例 2:批量创建用户(读取 CSV 文件)
需求:运维场景中需批量创建多个用户,将用户名和全名存入 CSV 文件(格式:用户名,全名),通过 for 循环读取文件并执行 useradd 命令:
步骤 1:准备 CSV 文件(users.csv)
zhangsan,张三
lisi,李四
wangwu,王五
zhaoliu,赵六
步骤 2:编写批量创建用户脚本
#!/bin/bash
# 从 CSV 文件批量创建用户
# 用法:sudo ./create_users.sh(需管理员权限)
# 1. 定义 CSV 文件路径(确保文件存在)
input_file="users.csv"
# 2. 检查文件是否存在,不存在则退出
if [ ! -f "$input_file" ]; then
echo "错误:文件 $input_file 不存在!"
exit 1 # 退出脚本,状态码 1 表示错误
fi
# 3. 读取 CSV 文件:按逗号分割,每次读取一行的“用户名”和“全名”
# IFS=,:设置分隔符为逗号;-r:禁止反斜杠转义;read loginname name:读取两个字段
while IFS=, read -r loginname name
do
# 4. 检查用户是否已存在(id 命令无输出则不存在)
if id -u "$loginname" > /dev/null 2>&1; then
echo "警告:用户 $loginname 已存在,跳过创建"
else
# 5. 创建用户:-c 备注(全名);-m 自动创建家目录
sudo useradd -c "$name" -m "$loginname"
# 6. 设置初始密码(示例:123456,实际场景建议用 passwd --stdin 或随机密码)
echo "$loginname:123456" | sudo chpasswd
echo "成功:创建用户 $loginname(全名:$name)"
fi
done < "$input_file" # < "$input_file":将 CSV 文件作为 while 循环的输入
执行效果:
sudo ./create_users.sh # 需管理员权限
# 输出:
成功:创建用户 zhangsan(全名:张三)
成功:创建用户 lisi(全名:李四)
警告:用户 wangwu 已存在,跳过创建
成功:创建用户 zhaoliu(全名:赵六)
验证结果:
# 查看用户家目录
ls /home/
# 输出:zhangsan lisi wangwu zhaoliu
# 查看用户信息
id zhangsan
# 输出:uid=1001(zhangsan) gid=1001(zhangsan) 组=1001(zhangsan)
注意事项:
- 实际生产环境中,初始密码不建议明文写在脚本中,可改用 openssl rand -base64 8 生成随机密码;
- 执行脚本需 sudo 权限(useradd 是系统命令,普通用户无权限)。
五、常见问题与避坑指南
for 循环的使用中,新手常因 “分隔符设置”“变量引号”“通配符匹配” 等问题导致脚本异常,以下是高频问题的解决方案:
问题现象 | 可能原因 | 解决方案 |
---|---|---|
含空格的文件名 / 值被拆分为多个元素 | 未修改 IFS,默认按空格分割;变量未加双引号 | 1. 处理文件行时,临时设置 IFS=$'\n'(按行分割); 2. 遍历文件时,$file 必须加双引号(如 [ -f "$file" ])。 |
通配符匹配不到文件时,直接输出通配符本身 | 目录中无匹配文件,Shell 未触发通配符扩展 | 1. 开启 shopt -s nullglob(无匹配时通配符扩展为空,而非保留原样); 2. 遍历前先用 [ -e "$file" ] 判断文件是否存在。 |
C 语言风格 for 循环报错 “syntax error” | 表达式中用了 $ 符号;分号遗漏 | 1. C 风格循环中变量无需 $(如 i=1 而非 $i=1); 2. 确保 ((...)) 内的三个表达式用分号分隔(如 ((i=1; i<=10; i++)))。 |
循环输出重定向后无内容 | 重定向符号位置错误(写在循环体内) | 将重定向符号写在 done 之后(如 done > output.txt),确保整个循环的输出被重定向。 |
嵌套循环中 break 无法退出外层循环 | 未指定 break 的层级 | 用 break n 指定退出层级(n=2 退出外层循环,如 break 2)。 |
六、总结:for 循环的选择与优化建议
Shell 提供两种 for 循环风格,需根据场景灵活选择,同时注意优化技巧以提升脚本效率:
1. 循环风格选择指南
循环类型 | 核心优势 | 适用场景 | 示例场景 |
---|---|---|---|
基础列表型 | 1. 无需关注数值,直接遍历列表; 2. 支持通配符、命令输出 | - 遍历文件 / 目录; - 处理字符串列表; - 解析命令结果 | 批量压缩 .log 文件、遍历服务器 IP 列表 |
C 语言风格 | 1. 精确控制数值范围; 2. 支持多变量同步迭代 | - 数值计数(如 1-100); - 多变量同步处理; - 嵌套计数 | 打印九九乘法表、生成指定范围的随机数 |
2. 脚本优化技巧
- 减少循环内的外部命令调用:外部命令(如 ls、cat)执行效率低,优先用内置命令替代(如用 for file in /dir/* 替代 for file in $(ls /dir));
- 避免无意义的迭代:遍历前先过滤无效数据(如用 if [ -f "$file" ] 排除目录,避免后续判断);
- 使用 nullglob 处理通配符:在遍历文件前执行 shopt -s nullglob,避免无匹配时通配符被当作普通字符串;
- 及时恢复 IFS 环境变量:临时修改 IFS 后(如处理 CSV 文件、按行遍历),务必恢复原始值(IFS=$IFS_OLD),避免影响其他命令。
通过本文的学习,你已掌握 Shell for 循环的所有核心用法 —— 从基础的列表遍历到进阶的循环控制,再到实际运维场景的实战脚本。for 循环是 Shell 编程的 “基石”,灵活运用它能大幅减少重复工作,提升运维效率,后续可结合 while 循环、函数等知识点,编写更复杂的自动化脚本。