用 expect 驱动必须有 TTY 的命令行工具:以 hdspace 交互隧道为例
背景
在开发者空间里操作云开发环境时,hdspace CLI 的 devenv start-tunnel --shell 会建立端口转发并直接给出一个交互式 root shell。问题是它要求真实的 TTY:在脚本、CI 或者 AI 助手的非交互终端里直接调用,要么立刻退出,要么卡在等待输入。本文记录用 expect 驱动这类“必须有 TTY”的命令的做法,以及踩过的几个坑。
为什么管道喂命令不行
很多人第一反应是 echo "ls" | hdspace ... --shell。这样 stdin 是管道而不是终端,程序检测到 isatty(0) 为假后会拒绝进入交互模式,或者读到 EOF 直接断开。nohup、重定向到文件也是同理。需要的是一个伪终端(pty),让程序以为自己连着真人的键盘。
expect 的最小脚本
macOS 自带 expect(基于 Tcl)。核心思路是:spawn 启动命令时 expect 自动分配 pty;用 expect 等待提示符出现,再用 send 发送命令。示例:
#!/usr/bin/expect -f
set timeout 60
spawn ./hdspace devenv start-tunnel --name=myenv --ports=10022:22 --shell
expect {
-re {[#$] $} {}
timeout { puts "no prompt"; exit 1 }
}
send "cd /workspace && ls\r"
expect -re {[#$] $}
send "exit\r"
expect eof
要点:换行要发 \r 而不是 \n,这是终端里回车键的真实字符;每条 send 之后都要再 expect 一次提示符,否则命令会叠在一起发出去,输出顺序混乱。
踩坑记录
1. 提示符匹配太宽。用 -re {#} 会匹配到命令输出里的井号(比如 shell 注释、Markdown 标题),导致过早发送下一条命令。建议匹配行尾的 # 或 $ ,必要时先 export PS1='READY> ' 换成独特的提示符再匹配。
2. 长时间命令被 timeout 打断。默认 timeout 10 秒,安装依赖、拉镜像远超这个时间。可以对单条 expect 设置 -timeout 600,或者在后台启动长任务(nohup ... &)后立即返回,再轮询日志。
3. 后台进程随会话退出被杀。在交互 shell 里用 & 启动的 http.server 或隧道进程,会在 exit 时收到 SIGHUP。要常驻就用 nohup cmd > log 2>&1 & 加 disown,或者 setsid,有条件的直接写成 systemd 服务。
4. 输出里夹杂控制字符。pty 输出带颜色码和 \r,直接 grep 会匹配失败。可以在远端命令前加 TERM=dumb,或者在本地用 sed 's/\x1b\[[0-9;]*m//g' 清理后再解析。
5. 敏感信息别写进脚本。AK/SK、API Key 不要硬编码在 .exp 文件里,改用环境变量(Tcl 中 $env(NAME) 读取),并在 expect 中用 log_user 0 临时关闭回显,避免密钥进日志。
替代方案对比
如果不想写 Tcl:script -q /dev/null cmd 可以给命令套一层 pty,适合只需要“有 TTY”而不需要交互应答的场景;Python 的 pexpect 与 expect 语义一致,适合已有 Python 工具链的项目;能用 SSH 的话,ssh -tt 强制分配 pty,配合 heredoc 一次发完多条命令最简单。
小结
“必须有 TTY”的 CLI 在自动化里很常见,判断标准是看它在管道下是否行为异常。expect 用 spawn 提供 pty、用 expect/send 做应答,几行就能把交互流程脚本化。注意提示符匹配要精确、长任务调大超时、常驻进程要脱离会话、密钥走环境变量,基本就不会再被卡住。
- 点赞
- 收藏
- 关注作者
评论(0)