如何整合Bash与Expect脚本实现批量SSH公钥自动分发?
如何整合Bash与Expect脚本实现批量SSH公钥自动分发?
我明白你的困扰啦——想用Bash批量遍历主机列表,同时借助Expect自动输入密码完成ssh-copy-id的交互,对吧?你的单独Expect脚本能搞定单主机,但整合进Bash循环就失效,核心原因是Bash本身无法解析Expect的专属语法,直接用here-doc把Expect代码丢给Bash执行肯定行不通。下面给你两个实用的整合方案,都是我实际用过的:
方案一:在Bash脚本中内嵌Expect逻辑(单脚本搞定)
这种方式不需要单独写Expect脚本,直接在Bash循环里用expect -c命令执行Expect代码,把Bash的变量传递进去就行,非常紧凑:
#!/bin/bash invdir=/home/john/inventory # 先检查参数是否齐全,比原脚本更健壮 if [ -z "$1" ] || [ -z "$2" ]; then echo "你必须输入两个参数:环境名称(prod, dev, dr, test)和对应环境的密码" exit 1 fi # 检查指定环境的主机列表文件是否存在 host_file="${invdir}/${1}" if [ ! -f "$host_file" ]; then echo "错误:环境 $1 的主机列表文件不存在!" exit 1 fi # 循环读取主机列表,处理每个主机 while IFS= read -r host; do # 跳过空行和注释行(如果你的列表里有注释的话) [[ "$host" =~ ^#.*$ || -z "$host" ]] && continue echo "正在处理主机:$host" # 用expect -c执行交互逻辑,双引号让Bash变量能传递到Expect里 expect -c " spawn ssh-copy-id -i ~/.ssh/id_rsa.pub $host expect { # 处理密码输入提示 \"password:\" { send \"$2\n\"; exp_continue } # 处理第一次连接时的主机指纹确认提示 \"Are you sure you want to continue connecting (yes/no)?\" { send \"yes\n\"; exp_continue } # 等待命令执行完成 eof } " done < "$host_file"
关键细节说明:
- 用
while IFS= read -r host读取文件,避免了原脚本中for host in \cat $file``遇到主机名带空格就拆分的问题 - 增加了主机指纹确认的处理,第一次连接新主机时不会卡住
- 提前做了参数和文件存在性检查,出错时能给出明确提示
方案二:Bash+独立Expect脚本(逻辑分离更清晰)
如果觉得内嵌Expect代码太乱,可以把交互逻辑单独写成Expect脚本,然后在Bash循环里调用它:
第一步:写独立的Expect脚本(比如命名为ssh-copy-id-auto.exp)
#!/usr/bin/expect -f # 检查参数数量是否正确 if { $argc != 2 } { puts "用法:$argv0 <主机地址> <密码>" exit 1 } # 接收Bash传递过来的参数 set host [lindex $argv 0] set password [lindex $argv 1] # 执行ssh-copy-id并处理交互 spawn ssh-copy-id -i ~/.ssh/id_rsa.pub $host expect { "password:" { send "$password\n"; exp_continue } "Are you sure you want to continue connecting (yes/no)?" { send "yes\n"; exp_continue } eof }
给脚本加上执行权限:
chmod +x ssh-copy-id-auto.exp
第二步:修改你的Bash脚本,循环调用这个Expect脚本
#!/bin/bash invdir=/home/john/inventory if [ -z "$1" ] || [ -z "$2" ]; then echo "你必须输入两个参数:环境名称(prod, dev, dr, test)和对应环境的密码" exit 1 fi host_file="${invdir}/${1}" if [ ! -f "$host_file" ]; then echo "错误:环境 $1 的主机列表文件不存在!" exit 1 fi # 循环遍历主机,调用Expect脚本 while IFS= read -r host; do [[ "$host" =~ ^#.*$ || -z "$host" ]] && continue echo "正在处理主机:$host" ./ssh-copy-id-auto.exp "$host" "$2" done < "$host_file"
这个方案的优点:
- 逻辑分离,Expect脚本专注处理交互细节,Bash脚本专注批量遍历,可读性更强
- 以后如果要修改交互逻辑,直接改Expect脚本就行,不用动Bash部分
一些注意事项
- 确保你的系统已经安装了Expect:Debian/Ubuntu用
sudo apt install expect,CentOS/RHEL用sudo yum install expect - 密码作为参数传递时,要注意安全性——虽然脚本里不会明文存储,但执行时
ps命令可能会看到密码,敏感环境可以考虑用环境变量传递 - 主机列表文件里每行一个主机,格式可以是
user@host(指定远程用户)或者直接host(默认用当前用户名)
备注:内容来源于stack exchange,提问作者JKC
相关产品推荐
相关产品推荐

