如果让你创建 10 个用户,你会写 10 行
useradd吗?如果让你检测 254 台主机是否在线,你会 ping 254 次吗?当然不会。这就是循环的意义——让计算机帮你做重复的事情。
本文将系统讲解 Shell 的三种循环结构(for、while、until),以及实现免交互自动化的 Expect 工具。通过实战案例(主机扫描、批量创建用户、批量修改密码、公钥推送)带你掌握循环与自动化运维的完整知识体系。
for 循环
语法结构
for 变量名 [in 取值列表]
do
循环体
done
| 组成部分 | 说明 |
|---|---|
for | 循环关键字 |
变量名 | 每次循环时,变量会被赋值为取值列表中的一个值 |
in 取值列表 | 可选,变量的取值范围 |
do...done | 循环体,每次循环执行的命令 |
取值列表的多种写法
# 方式 1:花括号展开
for i in {1..10}
# 方式 2:seq 命令
for i in $(seq 1 10)
# 方式 3:反引号 seq
for i in `seq 10`
# 方式 4:直接列举
for i in a b c d e
# 方式 5:命令结果
for i in $(ls /etc/)
案例 1:自动创建 10 个用户
需求:自动循环创建 10 个用户(user1 到 user10)。
#!/bin/bash
for i in {1..10}
do
useradd "user$i"
echo "user$i is created"
done
等价写法:
# 使用 seq 命令
for i in $(seq 1 10)
do
useradd "user$i"
done
# 使用反引号
for i in `seq 10`
do
useradd "user$i"
done
案例 2:ping 测试全网主机
需求:通过循环工具,探测全网主机。将在线主机记录在文本中。
IP 地址列表:
[root@localhost scripts]# cat ip.txt
172.16.8.100
172.16.8.4
172.16.100.254
思路
- 编写常规 ping 测试脚本(无循环)
- 添加循环语句
for i in {2..254} - 优化脚本(后台执行、清空旧记录、循环完成提示、wait 等待)
步骤 1:基础版本(无循环)
#!/bin/bash
ip=192.168.100.10
ping -c1 -W1 $ip &>/dev/null
if [ $? -eq 0 ]; then
echo "$ip" | tee -a ip.txt
fi
步骤 2:添加循环
#!/bin/bash
for i in {2..254}
do
ip=192.168.100.$i
ping -c1 -W1 $ip &>/dev/null
if [ $? -eq 0 ]; then
echo "$ip" | tee -a ip.txt
fi
done
步骤 3:优化版本(生产级)
#!/bin/bash
# 清空记录文本
>ip.txt
for i in {2..254}
do
{
ip=192.168.100.$i
ping -c1 -W1 $ip &>/dev/null
if [ $? -eq 0 ]; then
echo "$ip" | tee -a ip.txt
fi
}& # 后台执行
done
wait # 等待前一个程序执行完毕,再执行下一个命令
echo "finish...."
优化说明:
| 优化点 | 说明 |
|---|---|
>ip.txt | 清空旧记录,避免重复数据 |
{ ... }& | 后台执行,大幅提升扫描速度 |
wait | 等待所有后台任务完成后再输出提示 |
ping -c1 -W1 | 只 ping 1 次,超时等待 1 秒,加快速度 |
对比:串行 ping 254 台主机需要几分钟,后台并发只需要几秒钟。
案例 3:通过用户列表文件创建用户
需求:从文件中读取用户名列表,批量创建用户。
用户名文件:
[root@localhost scripts]# cat user.txt
zhangsan
lisi
wangwu
alice
基础版本
#!/bin/bash
for user in `cat user.txt`
do
useradd $user
echo "$user is created"
done
升级版本(生产级)
需求升级:
- 用户可以使用参数自定义用户名文件
- 如果用户没有输入用户名文件,提示用户输入
- 如果用户输入的不是文件,提示用户更正
- 启动循环创建用户
- 如果用户已经存在,提示存在
- 如果用户不存在,则创建成功,提示成功
#!/bin/bash
pass=123
# 判断脚本是否有参数
if [ $# -eq 0 ]; then
echo "usage: $0 filename"
exit 1
fi
# 判断用户输入的是否是文件
if [ ! -f $1 ]; then
echo "error filename"
exit 2
fi
# 设置循环,读取文件,创建用户
for user in `cat $1`
do
id $user &>/dev/null
# 如果用户不存在,则创建用户,否则提示已经存在
if [ $? -eq 0 ]; then
echo "user $user already exists"
else
useradd $user
echo "$pass" | passwd --stdin $user &>/dev/null
# 判断用户是否创建成功,并提示
if [ $? -eq 0 ]; then
echo "$user is created."
fi
fi
done
使用方式:
[root@localhost ~]# ./create_users.sh user.txt
退出码说明:
| 退出码 | 含义 |
|---|---|
0 | 正常退出 |
1 | 没有提供参数 |
2 | 提供的参数不是文件 |
案例 4:批量修改主机 root 密码
前提:
- 已经完成密钥登录配置(ssh-keygen)
- 定义主机地址列表
- 了解远程修改密码的方法
步骤:
- 邀请用户输入密码
- 循环调用 IP 地址
- 执行远程修改
- 记录执行结果
免密码登录准备:
# 生成密钥
ssh-keygen
# 分发公钥
ssh-copy-id IP地址
批量修改密码脚本:
#!/bin/bash
# 邀请用户输入新密码
read -p "请用户输入新密码: " password
# 循环调用 IP 地址
for i in $(cat ip.txt)
do
{
# 测试 IP 是否在线
ping -c1 -W1 $i &>/dev/null
# 在线 IP 进行修改
if [ $? -eq 0 ]; then
# 修改密码
ssh $i "echo $password | passwd --stdin root"
# 修改成功与否并记录
if [ $? -eq 0 ]; then
echo "$i" >> ok.txt
else
echo "$i" >> fail.txt
fi
# 不在线的主机记录
else
echo "$i" >> meigai.txt
fi
}&
done
wait
echo "批量密码修改完成!"
记录文件说明:
| 文件 | 含义 |
|---|---|
ok.txt | 修改成功的主机 IP |
fail.txt | 修改失败的主机 IP |
meigai.txt | 未在线、未修改的主机 IP |
while / until 循环
while 循环
特点:循环次数不一定是固定的。
语法:
while 条件测试
do
循环体
done
当条件测试成立(条件测试为真),执行循环体。
案例 1:死循环计数
#!/bin/bash
while :
do
let i++
sleep 1
echo $i
done
说明:
| 符号 | 说明 |
|---|---|
: | 永远为真,构成死循环 |
let i++ | 变量 i 自增 1 |
sleep 1 | 等待 1 秒 |
案例 2:条件循环
#!/bin/bash
while [ $i -lt 10 ]
do
let i++
echo $i
done
until 循环
语法:
until 条件测试
do
循环体
done
当条件测试成立(条件测试为假视为成立),执行循环体。
while 和 until 的区别:
| 循环类型 | 条件为真时 | 条件为假时 |
|---|---|---|
while | 执行循环体 | 退出循环 |
until | 退出循环 | 执行循环体 |
案例:循环到指定数字
需求:每秒循环一个数字,直到 14 停止。
#!/bin/bash
until [[ $i -eq 14 ]]
do
let i++
sleep 1
echo $i
done
等价 while 写法:
#!/bin/bash
while [[ $i -ne 14 ]]
do
let i++
sleep 1
echo $i
done
三种循环总结
| 循环类型 | 适用场景 | 特点 |
|---|---|---|
for | 循环次数固定 | 遍历列表,逐一处理 |
while | 循环次数不固定 | 条件为真时持续执行 |
until | 循环次数不固定 | 条件为假时持续执行 |
选择建议:
- 知道要循环多少次 → 使用
for- 不知道循环多少次,满足条件就继续 → 使用
while- 不知道循环多少次,满足条件就停止 → 使用
until
Expect 自动化交互
前言
观察 ssh 登录的交互现象:
[root@localhost ~]# ssh root@192.168.100.123
The authenticity of host '192.168.100.123 (192.168.100.123)' can't be established.
ECDSA key fingerprint is SHA256:3fjgqAxLwRbNDXSOUh1+/JrI4KpzyrHgkzmymB4vUUM.
ECDSA key fingerprint is MD5:3e:f6:f6:dc:fa:ef:26:47:9e:70:2f:4d:39:38:0b:0c.
Are you sure you want to continue connecting (yes/no)?
有些程序难以避免的需要交互。你该如何解决脚本与程序的交互问题?
解决方案:使用 Expect 工具实现自动交互。
什么是 Expect
Expect 是一个免费的编程工具语言,用来实现自动和交互式任务进行通信,而无需人的干预。
| 项目 | 说明 |
|---|---|
| 作者 | Don Libes(1990 年开始编写) |
| 用途 | 自动实现交互功能的软件套件 |
| 原理 | 根据程序的提示,模拟标准输入提供给程序需要的输入 |
| 应用场景 | ssh 登录、ftp 上传、telnet 远程管理等需要交互的场景 |
简单理解:Expect 就是帮你在脚本中”自动打字”的工具。
安装
yum install -y expect tcl tclx tcl-devel
Expect 核心命令
| 命令 | 说明 |
|---|---|
spawn | Expect 内部命令,启动一个 shell 程序 |
expect | 期望程序输出哪些内容 |
send | 模拟用户输入,发送内容给程序 |
exp_continue | 跳过循环,继续下一条语句 |
interact | 允许用户交互(留在远程会话中) |
expect eof | 等待会话结束 |
示例 1:SSH 免交互登录
#!/usr/bin/expect
spawn ssh root@192.168.100.123
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "111111\r" }
}
interact
逐行解释:
| 代码 | 含义 |
|---|---|
#!/usr/bin/expect | 指定 Expect 解释器 |
spawn ssh root@192.168.100.123 | 启动 ssh 连接 |
"yes/no" { send "yes\r" } | 如果看到 yes/no 提示,发送 yes + 回车 |
exp_continue | 继续匹配下一条(匹配完 yes/no 后继续匹配 password) |
"password:" { send "111111\r" } | 如果看到 password 提示,发送密码 + 回车 |
interact | 登录成功后,允许用户交互 |
测试验证:
[root@localhost ~]# chmod +x ssh_login.exp
[root@localhost ~]# ./ssh_login.exp
spawn ssh root@192.168.100.123
The authenticity of host '192.168.100.123 (192.168.100.123)' can't be established.
ECDSA key fingerprint is SHA256:3fjgqAxLwRbNDXSOUh1+/JrI4KpzyrHgkzmymB4vUUM.
ECDSA key fingerprint is MD5:3e:f6:f6:dc:fa:ef:26:47:9e:70:2f:4d:39:38:0b:0c.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '192.168.100.123' (ECDSA) to the list of known hosts.
root@192.168.100.123's password:
Last login: Tue Jun 9 11:19:38 2026 from 192.168.100.1
[root@web2 ~]#
示例 2:公钥推送(expect 实战)
准备工作
- 安装 expect
- 准备 SSH 公钥(ssh-keygen)
步骤 1:通过 shell 循环判断在线主机
#!/bin/bash
# 创建一个 IP 地址文件
>ip.txt
# 使用 for 循环 ping 测试主机是否在线
for i in {2..254}
do
{
ip=192.168.100.$i
ping -c1 -W1 $ip &>/dev/null
if [ $? -eq 0 ]; then
echo "$ip" >> ip.txt
fi
}&
done
wait
cat ip.txt # 观察在线主机
步骤 2:通过 expect 进行交互推送
推送单个主机的 expect 脚本:
#!/usr/bin/expect
set timeout 10
spawn ssh-copy-id 192.168.100.123
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "111111\r" }
}
expect eof
步骤 3:完整优化脚本(安装 + 公钥准备 + 批量推送)
#!/bin/bash
# 创建一个 IP 地址文件
>ip.txt
# 检测 expect 是否安装
rpm -q expect &>/dev/null
if [ $? -ne 0 ]; then
yum install -y expect
fi
# 检测公钥是否创建
if [ ! -f ~/.ssh/id_rsa ]; then
ssh-keygen -P "" -f ~/.ssh/id_rsa
fi
# 使用 for 循环 ping 测试主机是否在线
for i in {2..254}
do
{
ip=192.168.100.$i
ping -c1 -W1 $ip &>/dev/null
if [ $? -eq 0 ]; then
echo "$ip" >> ip.txt
/usr/bin/expect <<-EOF
spawn ssh-copy-id $ip
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "111111\r" }
}
expect eof
EOF
fi
}&
done
wait
echo "finish..."
脚本结构说明:
┌─────────────────────────────────────────┐
│ 步骤 1:安装 expect │
│ 步骤 2:检测 SSH 公钥 │
│ 步骤 3:扫描在线主机 │
│ 步骤 4:逐个推送公钥(expect 交互) │
│ 步骤 5:等待全部完成 │
└─────────────────────────────────────────┘
常见问题
for 循环中 in 可以省略吗?
可以。for i 等同于 for i in "$@"(遍历所有位置参数)。但建议始终写上 in,代码更清晰。
{1..10} 和 $(seq 1 10) 有什么区别?
| 特性 | {1..10} | $(seq 1 10) |
|---|---|---|
| 支持变量 | 不支持({1..$n} 无效) | 支持($(seq 1 $n)) |
| 步长 | 不支持 | 支持 $(seq 1 2 10)(1,3,5,7,9) |
| 性能 | 更快(Shell 内置) | 稍慢(外部命令) |
建议:固定范围用
{1..10},变量范围用$(seq 1 $n)。
while : 是什么意思?
: 是 Shell 的内置命令,永远返回真(退出码 0),构成死循环。等价于 while true。
expect 脚本可以直接用 bash 执行吗?
不可以。expect 脚本的第一行必须是 #!/usr/bin/expect,使用 ./script.exp 或 expect script.exp 执行。
expect 中可以嵌套 shell 命令吗?
可以。使用 expect <<-EOF ... EOF heredoc 语法,可以在 bash 脚本中内嵌 expect 代码。
总结
通过本章节的学习,你应该掌握了:
- 理解 for 循环的语法结构和取值列表的多种写法
- 掌握 for 循环实战:批量创建用户、全网主机 ping 测试、用户列表文件创建用户、批量修改远程密码
- 理解 while 循环(条件为真时执行)
- 理解 until 循环(条件为假时执行)
- 掌握三种循环的区别和选择建议
- 理解 Expect 工具的作用和原理
- 掌握 Expect 核心命令(spawn、expect、send、exp_continue、interact)
- 掌握 SSH 免交互登录脚本编写
- 掌握批量公钥推送完整流程(安装 → 公钥准备 → 扫描 → 推送)
- 能够简单编写生产级的批量运维脚本
学习建议:循环和 Expect 是自动化运维的核心技能。建议在虚拟机上完整实践每一个案例:全网主机扫描、批量创建用户、批量修改密码、公钥推送。特别是 expect 的公钥推送,是实际工作中最常用的自动化场景之一。动手做一遍,远胜于阅读十遍文档。