Expect脚本exec执行mailx命令失败,为何需用sh -c?
编写了一段Expect脚本,执行exec命令时出现异常——用puts输出命令内容显示正常,但exec执行时报错。脚本内容如下:
#!/usr/bin/expect -f set recipients { "abc@abc.com" "xyz@abc.com" } set body "Success!" set subject "File Upload Status" set email_sender "EmailSender <Email.Sender@abc.com>" set smtp_server "smtp://webmail.abc.com" set recipient_list [join $recipients ","] set cmd "echo \"$body\" | mailx -s \"$subject\" -r \"$email_sender\" -S smtp=$smtp_server \"$recipient_list\"" puts $cmd # Execute the mailx command exec $cmd
执行时的错误信息:
> ./test.expect echo "Success!" | mailx -s "File Upload Status" -r "EmailSender <Email.Sender@abc.com>" -S smtp=smtp://webmail.abc.com "abc@abc.com,xyz@abc.com" couldn't execute "echo "Success!" | mailx -s "File Upload Status" -r "EmailSender <Email.Sender@abc.com>" -S smtp=smtp:/webmail.abc.com "abc@abc.com,xyz@abc.com"": no such file or directory while executing "exec $cmd" (file "./test.expect" line 16)
尝试给exec后的$cmd加单双引号都没用,而且错误信息里smtp地址少了一个/(但puts输出正常)。改成exec sh -c $cmd后脚本能正常运行,想问:哪里操作错了?为什么必须显式调用sh -c?有没有办法避免?
1. 核心错误原因
Expect的exec和shell的命令执行逻辑完全不一样:
- shell会把整条命令拆解成主命令+参数+管道/重定向这些元语法来处理;
- 但Expect的
exec是直接把传入的字符串当成单个可执行文件的路径去查找,它根本不认识管道|、引号这些shell语法,也不会自动拆分参数。
你把带管道的整串命令传给exec,它就会把整个字符串当作一个文件名去找,自然找不到,所以报“no such file or directory”。至于错误信息里smtp地址少一个/,是Expect解析字符串时的转义问题:$cmd里smtp=$smtp_server展开后是smtp=smtp://webmail.abc.com,exec处理时会把//当成转义,最终变成smtp:/webmail.abc.com,但puts是直接输出原始字符串,所以显示正常。
2. 为什么sh -c $cmd能正常运行
sh -c是让系统shell来接管命令解析:
sh是系统里存在的可执行文件,-c是它的参数,后面的$cmd是shell要执行的命令字符串;- 此时shell会处理管道、引号、参数拆分这些逻辑,所以命令能正常执行。
3. 不用sh -c的替代方法
如果不想依赖shell,可以用Expect原生语法拆分命令,避开shell的元语法:
方法一:用管道直接传内容给mailx
# 把recipients列表直接展开成mailx的参数,不用逗号拼接 set mail_args [list -s $subject -r $email_sender -S smtp=$smtp_server {*}$recipients] # 创建管道连接mailx,直接写入body内容 set pipe [open "|mailx {*}$mail_args" w] puts $pipe $body close $pipe
这里用{*}展开recipients列表,让每个邮箱作为单独的参数传给mailx,比用逗号拼接更规范;同时用open "|命令"创建管道,直接把body内容写入管道,不需要用echo。
方法二:用Expect的原生管道语法
# 直接在exec里写管道,两边命令分开写,不用拼成字符串 exec echo $body | mailx -s $subject -r $email_sender -S smtp=$smtp_server {*}$recipients
Expect的exec本身支持管道,但必须把管道两边的命令直接写在exec里,而不是拼成一个字符串。{*}用来展开recipients列表,确保每个邮箱都是独立参数。
内容的提问来源于stack exchange,提问作者user22157547

