纯Shell执行正常的Docker命令在expect脚本中运行报错是什么原因?
问题底层原理
你遇到的报错是两层解析规则叠加导致的:
- 第一层:Bash Here-Doc 提前展开
Bash 中如果 Here-Doc 的界定符(比如你脚本里的END)没有被单引号包裹,Bash 会在将内容传给 Expect 进程之前,先解析所有$开头的变量、命令替换($())、反引号内容,把本地执行的结果直接替换到字符串里。你原脚本里的$(docker ps -a | grep Exited | wc -l)会先在本地服务器执行,把返回的数字(比如本地没有 Exited 容器就返回 0)直接替换进 send 的字符串里,根本不会传到远端执行。 - 第二层:Expect 自身语法解析
Expect 是基于 Tcl 语言开发的,Tcl 中双引号包裹的字符串会默认做变量/命令替换,而[]在 Tcl 中是内联命令执行的语法,和 Shell 里的$()作用完全一致。所以就算 Bash 没提前展开,你把[ $(xxx) -ne 0 ]放在 send 的双引号里,Expect 也会尝试把方括号里的内容当作 Tcl 命令执行,自然会报invalid command name "0"这类错误。
规范写法
根据是否需要保留外层 Bash 变量的展开能力,有两种常用解决方案:
方案1:完全关闭提前展开,原样发送命令(无外层变量依赖时推荐)
如果不需要在 Expect 段中使用外层 Bash 定义的变量,直接给 Here-Doc 的界定符加单引号,同时 send 命令的参数用 Expect 的大括号{}包裹即可:大括号在 Tcl 中是纯字符串标识,不会做任何解析替换,确保命令原样发送到远端 Shell 执行。
#!/bin/bash while read ip pass do { # 给END加单引号,告诉Bash不要解析Here-Doc内的任何特殊字符 /usr/bin/expect <<-'END' spawn ssh root@$ip expect { "yes/no" { send "yes\r";exp_continue } "password:" { send "$pass\r" } } expect "#" # 大括号包裹要发送的命令,最后加\r触发远端执行 send { [ $(docker ps -qf status=exited | wc -l) -ne 0 ] && docker start $(docker ps -qf status=exited) } send "\r" expect eof END }& done<apps_ip.txt
注:这里的Docker命令做了优化,用原生过滤参数-qf status=exited替代grep+cut,避免Docker输出格式变动导致的解析错误。
方案2:保留外层Bash变量展开,手动转义特殊字符(适配你的场景)
你当前的脚本需要用到外层 Bash 读取的$ip、$pass变量,不能给界定符加单引号,此时只要给所有需要传到远端执行的$、[、]前加反斜杠转义即可:
#!/bin/bash while read ip pass do { /usr/bin/expect <<-END spawn ssh root@$ip expect { "yes/no" { send "yes\r";exp_continue } "password:" { send "$pass\r" } } expect "#" # 给远端需要保留的$、[、]加反斜杠转义,末尾加\r触发执行 send "\[ \$(docker ps -qf status=exited | wc -l) -ne 0 \] && docker start \$(docker ps -qf status=exited)\r" expect eof END }& done<apps_ip.txt
补充简化方案
如果不需要在SSH会话中做复杂交互,你可以直接用sshpass传密码执行命令,不需要嵌套Expect,完全避免转义问题:
#!/bin/bash while read ip pass do { sshpass -p "$pass" ssh -o StrictHostKeyChecking=no root@$ip \ '[ $(docker ps -qf status=exited | wc -l) -ne 0 ] && docker start $(docker ps -qf status=exited)' }& done<apps_ip.txt
内容的提问来源于stack exchange,提问作者zjhleo
相关产品推荐
相关产品推荐

