在日常运维中,我们经常需要批量对一组主机执行命令,例如通过 ssh 查询时间、检查负载或部署服务。一个常见的做法是使用 while read 循环读取 hosts.txt 文件中的 IP 地址,并逐个执行远程命令。
然而,看似简单的脚本却可能“只执行第一台主机”,后续主机被跳过。 本文将深入剖析这一问题的根本原因,并提供多种安全可靠的解决方案,包括使用文件描述符重定向等高级技巧。
假设我们有如下主机列表文件:
$ cat hosts.txt
192.168.0.18
192.168.0.19
192.168.0.15编写一个简单的遍历脚本:
#!/bin/bash
while read LINE; do
echo "$LINE"
ssh "$LINE" "date -Ins"
done <hosts.txt运行结果却是:
192.168.0.18
2025-09-25T05:37:39,884205999-07:00只有第一台主机被执行,循环提前终止!
虽然 while read 是从 hosts.txt 读取每一行,但 ssh 命令在执行时,默认会从 标准输入(stdin) 读取数据,用于交互式会话或命令传输。
当脚本执行到 ssh "$LINE" "date -Ins" 时,ssh 会“吞噬”后续来自 hosts.txt 的输入流,导致 read 命令无法继续获取下一行,循环被提前中断。
🔍 关键点:
ssh和while read共享了同一个 stdin 流,造成资源竞争
-n 选项禁止 SSH 读取 stdin(推荐)最简单、最标准的解决方式是使用 ssh 的 -n 选项:
while read LINE; do
echo "$LINE"
ssh -n "$LINE" "date -Ins"
done <hosts.txt
-n的作用是:Redirects stdin from /dev/null,防止ssh读取任何输入。
✅ 优点:简洁、标准、无需修改循环结构
✅ 推荐指数:⭐⭐⭐⭐⭐
通过为 while read 分配一个独立的文件描述符(如 3),可以将循环输入与 ssh 的 stdin 完全隔离:
while read -u 3 LINE; do
echo "$LINE"
ssh "$LINE" "date -Ins"
done 3<hosts.txt🔍 原理说明:
•3<hosts.txt:将 hosts.txt 绑定到文件描述符 3•read -u 3 LINE:从文件描述符 3 读取一行,不使用 stdin•ssh 即使读取 stdin,也不会影响循环读取
✅ 优点:完全隔离输入流,适用于复杂脚本或需要保留 stdin 的场景
✅ 适用场景:脚本中同时有其他输入操作,或需要精细控制 I/O
✅ 推荐指数:⭐⭐⭐⭐☆
for 循环 + 命令替换对于小规模主机列表,可以使用 for 循环:
for host in $(cat hosts.txt); do
echo "$host"
ssh "$host" "date -Ins"
done⚠️ 注意事项:
•$(cat hosts.txt) 会一次性加载所有内容到内存•如果 IP 地址中包含空格或特殊字符,可能导致解析错误•不适合超大列表或包含空格的主机名
✅ 优点:语法简单,易理解
❌ 缺点:不够健壮,存在词法分割风险
✅ 推荐指数:⭐⭐⭐☆☆
推荐方案

•while read 未能遍历所有行,根本原因是 ssh 抢占了 stdin•解决方案的核心是:隔离 ssh 与循环的输入流•推荐使用 ssh -n,简单高效•高级场景可使用 read -u 3 + 3<file 实现完全隔离
掌握这些技巧,不仅能解决主机遍历问题,还能避免在其他涉及 ssh、sudo、mysql 等命令时出现类似的 stdin 冲突问题。
附:推荐脚本模板
#!/bin/bash
# 安全遍历 hosts 文件并执行远程命令
while read -u 3 host; do
[[ -z "$host" || "$host" =~ ^# ]] && continue # 跳过空行和注释
echo "==> $host"
ssh -n "$host" "date -Ins" || echo "Failed to connect to $host"
done 3<hosts.txt✅ 支持注释、空行跳过,使用
-n和独立文件描述符,健壮性强。