DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

while read 中的 SSH 只执行第一台主机

主机清单可以完整打印,加入 SSH 后却只执行第一台。检查循环与 SSH 共享标准输入的情况,按远端是否需要输入选择 -n 或独立文件描述符。

主机清单有多行,单独用 read 和 printf 能全部打印;往循环里加一条 SSH 命令后,却只处理第一台主机,随后脚本结束。第一台已经返回结果,后面的主机连开始提示都没有,也没有明显的连接报错。

下面这种写法就可能遇到这个问题:

while IFS= read -r target; do
    ssh "$target" 'uname -a'
done < servers.txt

done < servers.txt 把整个循环的标准输入接到了清单文件。read 从中取一行,循环里的 ssh 也继承了同一份标准输入。SSH 可以把后续内容读走并传给远端,下一次 read 就可能直接遇到文件末尾。即使远端命令是通常不需要输入的 uname -a,也不能据此认为 SSH 客户端不会读取输入。

这里讨论的是 OpenSSH 客户端与 shell 输入重定向的组合,不是某个 Linux 内核版本特有的故障。示例使用 POSIX 风格的 shell 写法,可用于 Bash 等兼容 shell;先在自己的测试主机清单上验证。若脚本一直卡在第一台没有返回,应先处理连接或远端命令阻塞,不能直接归入这一问题。

确认清单确实读得完整

servers.txt 每行只放一个可信的 SSH 目标,例如 ops@node-a.example.com。这些是占位地址,需替换。清单使用 Unix 换行,并在最后一行末尾保留换行;不要混入命令参数、行尾注释或空格分隔的多个目标。

先保留循环,只打印读取结果,不连接任何服务器:

while IFS= read -r target; do
    [ -z "$target" ] && continue
    printf 'target=<%s>\n' "$target"
done < servers.txt

这一步若也漏行,应先检查文件格式、实际读取路径和循环条件。IFS= 保留行内的空白,-r 避免把反斜杠当作转义;它们不会阻止 SSH 读取输入。本文的脚本只跳过空行,不会替你清理首尾空格或 Windows 回车。

如果清单能完整打印,再看原脚本中每次连接前后的日志。第一台返回后循环自然结束,与第一台卡住、显式 break、连接失败触发退出,是不同的现象。加入 -n 后所有目标都能依次开始,才支持“SSH 读走清单”这个判断。

远端命令不需要输入时,给 SSH 加 -n

OpenSSH 的 -n 阻止客户端读取标准输入。对于查询系统信息这类不需要输入数据的远端命令,可以把循环改成下面的形式。保存为脚本,在已经确认主机密钥、并配置好非交互认证的测试环境运行:

#!/bin/sh
failed=0
attempted=0

while IFS= read -r target; do
    [ -z "$target" ] && continue
    attempted=$((attempted + 1))
    printf 'BEGIN %s\n' "$target"

    if ssh -n -o BatchMode=yes "$target" 'uname -a'; then
        printf 'OK %s\n' "$target"
    else
        status=$?
        printf 'FAIL %s status=%s\n' "$target" "$status" >&2
        failed=1
    fi
done < servers.txt

printf 'attempted=%s\n' "$attempted"
exit "$failed"

这个例子顺序处理主机,某台失败后仍继续下一台,并在最后以非零状态表示存在失败。这样不会把“循环继续了”误当成“所有远端命令都成功了”。SSH 成功执行远端命令时会返回该命令的退出状态,连接等 SSH 错误通常返回 255;仍需结合错误信息判断原因。

BatchMode=yes 禁用密码询问、主机密钥确认等交互,适合无人值守脚本。它并不解决标准输入争用,也不替你配置认证。如果某台需要首次确认密钥或输入密码,先按正常流程核对和配置,不要为了让批处理通过而关闭主机密钥检查。

也可以只对 SSH 做输入重定向:

ssh -o BatchMode=yes "$target" 'uname -a' < /dev/null

这条写在循环体里的命令把 SSH 的输入换成空设备,清单仍供下一次 read 使用。不要把循环末尾的 done < servers.txt 改成读取空设备,否则循环本身也读不到主机了。

需要传数据给远端时,分开清单和数据输入

如果原本要通过 SSH 向远端传文件内容或脚本,直接加 -n 会切断这份输入。此时可以用一个独立的文件描述符读取主机清单,让 SSH 从另一个文件读取数据。

下面假设本地已有一个不含敏感信息的 payload.txt。远端只用 wc -c 统计收到的字节数,不写远端文件:

#!/bin/sh
failed=0

while IFS= read -r target <&3; do
    [ -z "$target" ] && continue
    printf 'BEGIN %s\n' "$target"

    if ssh -o BatchMode=yes "$target" 'wc -c' < payload.txt 3<&-; then
        printf 'OK %s\n' "$target"
    else
        status=$?
        printf 'FAIL %s status=%s\n' "$target" "$status" >&2
        failed=1
    fi
done 3< servers.txt

exit "$failed"

3< servers.txt 为循环打开清单,read ... <&3 只在读取这一行时使用它。SSH 的标准输入来自 payload.txt;3<&- 在该命令中关闭继承的清单描述符,不影响父 shell 下一轮继续读取。这里没有使用 read -u,避免把 Bash 的选项直接套到所有 /bin/sh 环境。

文件描述符 3 只是示例。在更大的脚本中,先确认它没有被其他逻辑占用。每次调用 SSH 都重新打开 payload.txt,因此每台主机收到的是同一份完整文件。若需要每台使用不同数据,应明确对应的数据文件,不能再让 SSH 随意读取主机清单。

检查每台结果,再换回业务命令

使用两三台已授权的测试主机,逐一核对 BEGIN、OK 或 FAIL。第一种脚本的 attempted 应等于清单中非空行数,每个目标都应有结果;第二种脚本则应逐台得到与本地文件大小相符的字节数,并检查失败状态。不要只看脚本最后一行输出。

确认每台主机都收到预期输入后,再换成所需的业务命令。批量操作前保留原脚本,尤其要核对业务命令是否依赖标准输入。若修改后远端收不到应有数据,撤回该命令的 -n,改用独立描述符和显式数据输入;恢复脚本不会撤销已经在远端执行的操作,因此不要用写配置、删除文件等命令验证循环是否完整。