编写 expect 脚本并非简单的命令拼接,不少初学者按格式填写后执行,结果要么启动报错,要么卡住不动,要么发送的命令毫无反应。实际上,expect 只是一个忠实的执行者,它会严格按照你设定的“剧本”等待和发送内容。问题往往出在,你编写的“剧本”与终端实际输出的“剧情”不一致。

执行 expect 脚本前必须确认的三项基础配置
脚本无法正常运行,首先不要怀疑逻辑,而是检查以下三个基础条件,缺一不可:
第一,系统中必须安装 expect。执行 which expect 命令,应返回类似 /usr/bin/expect 的路径。如果没有,则需要安装:CentOS 系统使用 yum install expect,Debian/Ubuntu 系统使用 apt-get install expect。
第二,底层依赖 Tcl 也必须存在。通常安装 expect 时会自动附带,但如果运行时出现错误 can't find package Tcl,则需要手动补充:yum install tcl。
第三,脚本文件本身要符合规范。首行必须写 #!/usr/bin/expect,建议使用绝对路径,因为部分系统对 #!/usr/bin/env expect 这种写法不兼容。编写完成后,通过 chmod +x your_script.exp 赋予执行权限。
spawn 启动命令后 expect 匹配不到提示符的常见原因
脚本启动了,却卡在第一个 expect 语句上,这是最常见的故障。原因大多不是正则表达式写错,而是你预想的输出与实际程序输出的内容根本不一致。
举个典型场景:使用 spawn ssh user@host,你期望直接出现 password: 提示。但实际上,系统可能先显示一行 The authenticity of host 'xxx' can't be verified. 询问 yes/no,然后才出现密码提示。如果你的脚本仅写了 expect "password:",它就会永远卡在“yes/no”上,无法等到“password:”。
如何解决?首先,不要猜测,要用事实说话。运行脚本时加上 -d 调试参数:expect -d ./script.exp。此时你会看到 expect 实际捕获到的每一个字符,包括隐藏的控制符,真相一目了然。
其次,匹配策略要灵活。不要只使用字符串完全匹配,建议用 -re 启用正则表达式,一次覆盖多种可能。例如:expect -re "(password:|yes/no|.*\$)",这样无论是密码提示还是主机认证提示,都能被匹配。
另外,注意细节。提示符末尾可能包含空格或制表符,使用 .* 兜底更稳妥:expect "password:.*"。远程登录后的 shell 提示符形式多样,可能是 [root@host ~]# 或 user@host:~$,不要硬写 expect "$ ",改用宽泛的正则如 expect -re {[.*]|.*$} 会更可靠。
send 发送密码后脚本立刻退出,而不是继续执行命令
另一个令人困扰的情况是:密码发出后脚本立即结束,你期望后续执行的命令(如 ls)根本没有运行。问题出在哪里?
核心在于对 send 和 expect 协作关系的误解。send 只负责“发送”,它不关心对方是否“收到”或“准备好”。发完密码后紧跟着 send "ls",但此时远程 shell 可能仍在处理登录流程,尚未进入可接收命令的状态,这条 ls 命令就会无声无息地被丢弃。
因此,黄金法则是:每次 send 之后,必须紧跟一个 expect,等待下一个明确的提示信号。 发送密码后,要等待出现 shell 提示符(如 expect "# " 或 expect -re {\$|#}),然后再发送下一条命令。
还有两个细节不可忽略:一是 send 命令末尾必须加 \r(回车),send "ls\r" 才是真正执行,仅写 send "ls" 只是将字符敲入终端,未按回车键。二是脚本的收尾:如果想在命令执行后将控制权交还给用户手动操作,就在末尾加 interact;如果只想安静执行完然后退出,则使用 expect eof。
最后提一条安全建议:不要把密码明文写在脚本里。可以使用 set password $env(PASSWD) 从环境变量读取,运行时这样调用:PASSWD=your_password ./login.exp。
timeout 设为 -1 并不安全,真实场景应分层设置
很多人图省事,在脚本开头写一句 set timeout -1,表示永不超时。这看似一劳永逸,实则埋下隐患。
试想,如果远程主机宕机或网络突然中断,脚本将永远挂起,等待一个永远不会到来的响应。在自动化流水线(CI/CD)中,这意味着任务卡死、资源占用,需要人工介入才能终止。
正确的做法是根据不同操作阶段的合理耗时,分层设置超时时间:
- 连接阶段(如
spawn ssh):网络连接和初始握手,10 到 15 秒通常足够。set timeout 15 - 认证阶段(等待密码提示符):输入密码后等待 shell 准备就绪,5 到 8 秒足矣。
- 命令执行阶段:这完全取决于发送的具体命令。一个
ls可能只需 1 秒,但一个rsync大文件传输可能需要 120 秒,而apt update可能需要 300 秒。需要根据实际情况预估。
更重要的是,每个 expect 语句块都应包含对 timeout 情况的处理。例如:
expect {
timeout { send_user "SSH连接失败,超时!\n"; exit 1 }
"password:" { send "$passwd\r" }
}
这样,一旦在预定时间内未等到预期响应,脚本就能优雅地报错退出,而不是无限期挂起。
总之,掌握 expect 的关键,不在于死记硬背语法,而在于真正理解你要自动化的交互过程:目标程序究竟会输出什么(包括隐藏的控制字符)、输出的顺序是怎样的、中间是否可能插入无关的系统日志、终端环境是否会影响输出(如颜色代码)。只有摸清这些细节,你编写的 expect 脚本才能精准可靠,而不是靠运气才能运行成功的“玄学”脚本。
