尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Here Document与Expect:Shell自动化免交互原理与实战
写脚本自动化部署的时候我最头疼的不是权限问题不是变量嵌套而是那一堆“交互式提问”。装软件时它问你y还是n连服务器时它让你输密码改配置时它要确认覆盖。新手很容易把Here Document和Expect混为一谈觉得都是“提前准备好输入内容”实际上这两者的工作机制和使用场景差别非常大。这篇文章就把这两个东西掰开揉碎讲清楚前半部分讲Here Document免交互的完整用法和常见坑后半部分讲Expect自动化交互的四个核心命令和实战套路。适合正在学shell脚本、写自动化部署脚本、或者被交互式命令卡住的运维和开发朋友。1. 两种免交互机制的定位差异heredoc与expect各自该什么时候用1.1 先从一次部署卡壳说起我之前写过一批批量初始化服务器的脚本原本计划得挺好脚本里探测一下系统版本装几个包改几行配置最后把服务拉起来。结果卡在了一个交互式安装程序上——那玩意装到一半会停下来问“是否需要保留旧的配置文件”等十秒钟没人回答就按默认选项继续。一开始我用Here Document去处理把它需要回答的内容提前塞进去install_some_tool EOF n n Y EOF结果程序根本不买账照样卡在那里等输入。后来才发现这类程序读取输入的时候用的是/dev/tty根本不读标准输入流。你heredoc写得再周全人家压根不看你递过去的东西。这就是Here Document和Expect最本质的区别heredoc是把内容预先放到标准输入流里但凡程序是从终端设备读取输入的heredoc就失效了。而Expect的核心思路是模拟真实用户去“对话”——它看着程序输出什么就回什么完全模拟一个真人坐在终端前面敲键盘。1.2 一个核心判断标准输入是“预定剧本”还是“即兴对话”我后来给自己总结了一条判断标准用到现在基本没出过错如果这次交互的内容是固定不变的、程序不会根据输入改变提问顺序就用heredoc如果程序会根据你的回答产生新的问题、需要你根据回显内容临时决定下一步就用Expect。打个比方heredoc是提前写好一整段台词的提词板把自己要说的话一次性念完Expect是和一个活人聊天对方说什么你接什么有时候还需要等对方先开口。比如往文件里写入一段固定配置、给数据库执行一批固定的SQL语句、给FTP传一个文件清单这类用heredoc就非常合适。但如果是SSH首次登录时的“是否确认主机指纹”确认、执行sudo时提示输密码、或者连上一台网络设备后要根据对方的命令提示符动态下发命令就得上Expect。有时候两者的边界会重叠比如你用heredoc把expect的脚本内容传给expect解释器执行——这也是后文会提到的实用技巧。理解了定位差异后面学语法就不容易绕晕。2. Here Document基础语法与常用变形2.1 最正统的写法cat配合EOF往文件里写内容Here Document的语法结构不复杂核心就是用加定界符把一段内容包起来遇到单独的定界符就结束。最常见的用法是用cat把内容重定向到文件里cat EOF /etc/motd 欢迎访问这台服务器 当前登录用户$USER 系统时间$(date %Y-%m-%d %H:%M:%S) EOF执行完以后去/etc/motd看一眼$USER和$(date)已经被替换成实际值了。这是因为 heredoc 的内容在交给cat之前先被当前的bash主进程做了变量展开和命令替换。这一点非常关键——你感觉是在写一份静态文件但bash早把里面的动态部分处理完了。于是衍生出一个实用分支如果你希望把这批内容原封不动地写到目标文件里包括那些$符号就要给定界符加引号最常用的是单引号cat EOF /tmp/script.sh #!/bin/bash echo 当前时间$(date) echo 用户目录$HOME EOF这里$和反引号都会被保留不会产生意外展开。写配置模板、生成脚本、保留正则表达式时这个习惯能省掉很多麻烦。2.2 按需关闭扩展单引号定界符的隐藏价值前面已经提到单引号定界符EOF会关闭整段内容的扩展。但很多人在实际用的时候只关注“变量会不会被替换”却忽略了另一个场景内容里同时包含需要展开的部分和不需要展开的部分。这种情况下单独用EOF或者单独用EOF都不完美。处理办法其实不复杂。如果你只需要拦住其中某一个变量不被展开可以在它前面加反斜杠比如\$USER这样bash只对这个变量跳过展开其余地方照常处理cat EOF 当前路径$PWD 字面变量演示\$HOME 不会展开 系统信息$(uname -r) EOF还有一种比较少见但很实用的场景heredoc内容里要写一个EOF字符串原文。此时如果外层定界符也用EOF就会提前截断。解决方法是换一个不冲突的定界符比如EOS、_END_、MARK定界符本身可以任意取值只要前后一致且不与正文冲突即可。2.3 逃不掉的缩进问题-的实用场景写脚本时为了保持代码整洁很多人喜欢把heredoc块放进if分支或for循环里并加上缩进。但bash解析heredoc时要求在结束定界符那一行不能有任何前置空格或Tab否则就判定为内容还没结束。于是经常出现一个经典的报错脚本跑了半天最后提示warning: here-document delimited by end-of-file。解决方案是使用-if [ $1 init ]; then cat -EOF /tmp/init.txt 初始化配置 第2行内容 EOF fi注意这里的细节-只在内容行和结束行都使用Tab缩进时才有效连续多个空格它是不会处理的。很多编辑器默认把Tab转成4个空格这时候-照样报错需要在编辑器里关闭“缩进转空格”的选项或者干脆写脚本时不缩进heredoc块。2.4 Here String比heredoc更迷你的输入手段除了heredocbash还提供一种更简洁的免交互输入方式Here String语法是。它能把一个字符串作为标准输入传给命令grep error 2024-01-01 10:00:00 error: disk full wc -l a b c这里wc -l统计的其实是三行文本。Here String常被用在比较简单的单行输入场景比如配合read给变量赋值、给passwd以外的命令投喂一行交互内容。它的字符串内容同样遵循变量扩展规则想要关闭扩展同样可以用引号包住字符串不过这种方式本质上是字符串不是文本块论灵活性远不如heredoc。3. Here Document在真实运维场景里的几种玩法3.1 批量创建系统配置heredoc配合tee追加重定向cat EOF file是最常见的组合它的缺陷是每次都会把文件整个覆盖。如果你需要在文件末尾追加内容写法改成cat EOF file就行。如果既要保留原有内容又希望在文件里写入多段内容更稳妥的组合是配合teetee -a /etc/security/limits.conf EOF * soft nofile 65535 * hard nofile 65535 EOF使用tee -a的好处之一是内容写入的同时还会输出到终端方便在脚本里实时查看写入结果。当然也可以把tee的输出重定向到/dev/null只在脚本日志里留下痕迹。我第一次批量改几十台服务器配置时就踩了坑——只用了覆盖模式结果把原来文件里的默认配置给清了服务重启后直接出问题。从那以后凡是修改已有配置文件我默认都用追加或先备份再覆盖。3.2 把SQL喂给数据库MySQL导入不再手动粘贴运维或者开发环境里经常要执行一批SQL手一条条粘贴既慢又容易出错。heredoc特别适合这个场景因为SQL里常常包含单引号和特殊字符用EOF可以避免不必要展开mysql -uroot -p密码 -h127.0.0.1 EOF CREATE DATABASE IF NOT EXISTS monitor DEFAULT CHARSET utf8mb4; USE monitor; CREATE TABLE IF NOT EXISTS metrics ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, value DOUBLE NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO metrics (name, value) VALUES (cpu_usage, 12.5); EOF注意这里用了单引号定界符避免SQL里的内容被shell展开。如果你不小心用了不带引号的EOF哪怕SQL里只有一个$符号bash就会试图把它当成变量替换结果要么报错要么查出来的数据不对。同样的思路还可以用于给Redis执行批量命令、给MongoDB执行JS脚本、给Hive执行HQL等。核心都是同一个逻辑把一段固定文本作为标准输入灌给交互程序。3.3 heredoc的边界为什么它搞不定动态交互heredoc处理静态输入很顺手但遇到“需要等待程序回显再决定下一步”的交互就无能为力。最典型的例子是SSH首次连接某个陌生主机时的确认Are you sure you want to continue connecting (yes/no)?如果写成ssh roothost EOF ... EOF这个yes/no的问题会直接显示在终端上等待手动输入heredoc里的内容并不会自动去回答它。因为ssh这个程序在做二次确认的时候读的是当前终端的输入而不是标准输入管道。类似的还有passwd修改密码、sudo提示密码、各类交互式的安装向导。它们有些是因为安全原因刻意不从标准输入读取有些是因为程序内部会在打开终端设备后重新读取。这时候就需要Expect出场——它的设计目标就是把这种“动态交互”变成自动化。4. Expect自动化交互四个命令吃透整个逻辑4.1 安装与基本运行方式Expect是一个基于Tcl语言编写的自动化工具大多数Linux发行版的软件源里都有。安装很简单# Debian/Ubuntu apt install expect -y # CentOS/RHEL yum install expect -y装完之后不需要额外配置。Expect脚本有两种运行方式一是给脚本加上#!/usr/bin/expect解释器直接执行二是在bash脚本里用expect -c 脚本内容或expect EOF方式嵌入。下文会分别演示。先解释一下Expect的工作原理它启动一个子进程比如ssh然后程序通过伪终端和这个子进程通信监听子进程的输出。当输出匹配到我们设定的模式时就向子进程发送指定的输入内容。本质上就是“读出屏幕上的文字判断后自动按键”。4.2 spawn、expect、send、interact的分工与配合Expect有四个核心命令理解了它们的分工脚本基本就能写出来。spawn启动一个交互程序可以理解成“打开一个虚拟终端并运行命令”。expect等待子进程输出的关键字匹配到之后执行预设动作。send向子进程发送字符串模拟用户敲键盘。interact把控制权交还给用户之后由真人手动操作。看一个最基础的登录示例#!/usr/bin/expect set timeout 5 spawn ssh root192.168.1.10 expect password: send mypassword\r expect # send uptime\r expect # send exit\r expect eof解析一下spawn启动SSH进程然后脚本在expect password:处停下来等待SSH输出包含password:字样。一旦匹配到send把密码发送过去。注意发的是\r回车而不是\n换行因为终端交互程序认回车键。之后同理发送命令后等待命令提示符#出现再发下一条命令。interact命令用于把长期的控制权交还给人。比如你要登录一台机器做点临时操作可以写一个半自动脚本自动输密码登进去以后交给人操作#!/usr/bin/expect set host [lindex $argv 0] set pass [lindex $argv 1] spawn ssh root$host expect password: send $pass\r interact脚本支持从命令行接收参数[lindex $argv 0]是第一个参数[lindex $argv 1]是第二个参数。执行方式./auto_login.exp 192.168.1.10 mypassword4.3 超时与分支判断exp_continue的真正价值expect脚本里第一个容易忽略的就是超时。set timeout 5意思是如果5秒内没匹配到关键字就继续往下执行。如果不设置默认超时时间是10秒。等你匹配一个可能根本不存在的关键字脚本会在10秒后直接跳过。如果希望永不超时可以设置set timeout -1。第二个容易忽略的是多分支匹配。实际交互中程序往往不是只给出一种提示。比如SSH首次连接可能会出现yes/no确认而连接过的机器直接就是password:。这种情况下单用expect password:就可能出错。正确的写法是用多分支结构#!/usr/bin/expect set timeout 10 spawn ssh root192.168.1.10 expect { (yes/no) { send yes\r exp_continue } password: { send mypassword\r } timeout { puts 连接超时 exit 1 } } expect # send hostname\r expect # send exit\r expect eof这里exp_continue是核心关键字作用是“匹配成功并发送内容后继续等待下一个模式”。第一次遇到yes/no时脚本回复yes然后执行exp_continue回到expect继续等待。下一次匹配到password:就输入密码。如果10秒内什么都不匹配就走进timeout分支打印提示并退出。exp_continue还有一个变体用法你可以把它写进所有匹配分支的末尾实现循环匹配同一个输出。比如程序连续输出多屏信息每屏出现--More--时按下空格继续翻页可以写成expect { password: { send 123456\r } --More-- { send ; exp_continue } # { send exit\r } }这段脚本就能应对一边输密码、一边刷出翻页提示、最终出现命令提示符的复杂情况。5. Expect实战从免密登录到批量巡检5.1 最小可用脚本自动登录远端服务器并执行命令先搭一个最实用的模板。假设有多台服务器需要定期巡检手动登录然后挨个敲uptime、df -h、free -m太费时间。写一个expect脚本自动完成这些事#!/usr/bin/expect set timeout 10 set host [lindex $argv 0] set pass [lindex $argv 1] spawn ssh -o StrictHostKeyCheckingno root$host expect { password: { send $pass\r } timeout { puts 密码提示超时; exit 1 } } expect # send echo 系统负载 ; uptime\r expect # send echo 磁盘使用 ; df -h\r expect # send echo 内存使用 ; free -m\r expect # send exit\r expect eof注意这里给ssh加了一个参数-o StrictHostKeyCheckingno作用是跳过主机指纹确认。平时自己连接机器时不建议关掉这个检查但脚本里如果不想处理首次登录的yes/no问题这个参数可以让流程简化不少。如果出于某种原因必须保留指纹确认就把前面那个多分支expect的写法用起来。这个脚本的不足之处在于每台机器都要单独执行一次。要批量处理可以写一个外层bash循环#!/bin/bash for ip in 192.168.1.10 192.168.1.11 192.168.1.12; do echo 正在巡检 $ip ./auto_check.exp $ip 密码 done5.2 处理yes/no确认与密码提示的完整流程如果不想关掉主机指纹确认又希望脚本能处理首次连接时的yes/no关键就是5.2节提到的多分支exp_continue写法。完整流程应该是#!/usr/bin/expect set timeout 8 set host [lindex $argv 0] set pass [lindex $argv 1] spawn ssh root$host expect { yes/no { send yes\r exp_continue } password: { send $pass\r } timeout { puts 连接超时 exit 1 } } expect # send pwd\r expect # send exit\r expect eof这里有个很隐晦的坑有些系统SSH的提示文本是Are you sure you want to continue connecting (yes/no)?有些则直接是yes/no短句。正则匹配在expect里默认是子串匹配写成yes/no就能匹配到。但如果你用了-re正则标志就得处理其中的括号和问号反而不如直接用精确匹配。5.3 把expect嵌进bash脚本两种语言的协作套路很多人不习惯单独写.exp文件更希望所有逻辑都放在一个bash脚本里。expect提供了两种嵌入方式分别适合不同场景。第一种是用expect -c直接传入脚本内容。单行命令适合极简场景expect -c spawn ssh root192.168.1.10 expect password: send 123456\r expect # send hostname\r expect # send exit\r expect eof 注意这里外层用了单引号保证内部的$不会被bash展开。如果你的密码里含有$字符这种方式就有些危险——单引号内的内容虽然不会被bash展开但expect/Tcl在处理$pass这类变量时又会做一层解析。稳妥的做法是把密码存成环境变量在expect内用$env(PASSWORD)读取export TARGET_PASSpss$w0rd expect -c set timeout 5 spawn ssh root192.168.1.10 expect password: send $env(TARGET_PASS)\r interact 第二种嵌入方式是用heredoc把expect脚本作为内容传给expect解释器这正好把前文的两个技术点串起来了#!/bin/bash expect EOF set timeout 5 spawn ssh root192.168.1.10 expect { yes/no { send yes\r; exp_continue } password: { send 123456\r } } expect # send uptime\r expect # send exit\r expect eof EOF这里的EOF用了单引号定界符目的是把内部的$、\r等字符原样交给expect解释器。如果你忘了加单引号bash会先尝试展开$env之类的变量遇到不存在的变量会被替换成空字符串脚本就会出错。我倾向于推荐heredoc嵌入方式原因是多行、多分支的expect代码在-c参数里写起来很费劲转义也会变得一团糟而heredoc可以保持脚本整体的可读性。6. 排错经验与收尾建议6.1 最常见的几个坑把频繁踩到的坑整理成一个表方便排查症状原因解决方案heredoc报here-document delimited by end-of-file结束定界符前有空格/Tab或结束定界符没单独成行顶格写结束符或改用-配合Tab缩进heredoc里变量被意外展开定界符没加引号内容含$、反引号时用EOFexpect发送了字符串但程序没反应用了\n而不是\rsend内容用\r结尾模拟回车expect脚本卡住不动直到超时匹配的模式写错了程序输出和预期不一致先手动执行一次命令查看实际输出文本再改脚本密码含$、[等特殊字符发送错误传给expect时被bash或Tcl做了解释脚本内用$env(变量)读取环境变量或对特殊字符转义执行expect时报command not foundexpect未安装或解释器路径不对先执行which expect确认路径直接用绝对路径写shebang这里重点说一个经常被忽略的细节expect匹配模式里的括号是有意义的。写expect (yes/no)时括号是普通字符bash不会管它Tcl默认也按字面量处理。但如果你在这个位置加了-re正则标志圆括号就变成分组符号字符串里的括号会被当成正则语法匹配可能莫名其妙失败。我的经验是能不用正则就不用用子串匹配反而更稳。6.2 一条务实的使用准则用熟了之后会发现Expect也不是万能钥匙。它的问题在于太吃匹配文本程序输出变了就得跟着调整脚本而且密码明写在脚本里本身就有安全风险。所以我自己的使用准则是这样的能用heredoc解决的绝不上expect。比如写文件、导SQL、批量执行固定命令这些都是“预定剧本”heredoc一句话就搞定。只有面对那种真正动态交互、且没法通过修改程序参数来绕过的场景才动用expect。而在expect里尽量把密码用环境变量传进来脚本本身不落地保存敏感信息执行完就销毁。如果交互流程很简单还可以考虑更轻量的方案比如用sshpass传密码。但sshpass有个明显的短板它只能处理密码输入没法应对yes/no和后续的动态交互灵活度远不如expect。最终选型还是回到第一个问题的答案脚本要处理的是定式还是变式。最后分享一个小技巧expect脚本在调试时可以在脚本开头临时加一行exp_internal 1它会输出详细的交互过程日志把程序每次显示的内容和expect匹配的进度都打印出来。我第一次写多分支匹配时就是靠这个命令看清了SSH到底输出的是password:还是Password:一下就定位到了问题。
RELATED

相关推荐

NAS上的全屋智能AI中枢:Miloco自动化部署实战

NAS上的全屋智能AI中枢:Miloco自动化部署实战

先聊个现象:很多朋友的智能家居玩了两三年,网关、音箱、传感器买了一堆,但日常体验还是“喊一句开灯、再喊一句关空调”,所谓“全屋智能”更像是一堆遥控器换了个叫法。真正的问题不是设备不够多,而是没有一个统一调度…

📅 2026/10/18 20:29:27
Unity 2D炮台追踪与子弹发射:旋转、射击与对象池优化

Unity 2D炮台追踪与子弹发射:旋转、射击与对象池优化

做2D游戏遇到“炮台追踪主角并发射子弹”这个需求,基本是塔防、射击、Boss战里绕不开的一环。很多刚接触Unity的朋友第一反应就是“让炮台look at主角就行”,结果一做就发现要么炮台转起来像抽搐,要么子弹满天乱飞,要么管子口和枪…

📅 2026/10/18 20:24:26
进制转换器开发详解:从除基取余到浮点精度与补码边界

进制转换器开发详解:从除基取余到浮点精度与补码边界

简介:数字进制转换器是一款面向编程初学者与计算机基础学习者的实用小工具,用于在十进制、二进制、八进制和十六进制之间快速完成数值转换。资源附带详细的进制原理讲解,涵盖位权、除基取余法等核心算法,并给出C#语言的具体实现代…

📅 2026/10/18 20:24:26
MORE NEWS

更多资讯

📰

Claude Desktop 配置 MCP server 连接本地 SQLite:Windows 11 网络驱动器路径踩坑与 TaoToken 通道实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Flink多并行度下Watermark不触发窗口的机制与排查

1. 从一个真实场景说起:多并行度下水位线为什么不触发窗口如果你写过 Flink 的窗口聚合,大概率遇到过这种诡异情况:单并行度跑得好好的,一调到多并行度,窗口就像被冻住了一样,死活不输出结果。日志里水位线…

📰

vLLM 推测解码实战:NLD 让大模型推理提速

大模型在线推理,部署上去以后最让人头疼的问题是什么?显存不够、吞吐上不去、首字延迟压不下来——这些我都遇到过,但最扎心的其实是 decode 阶段一个 token 一个 token 往外蹦,明明显存带宽也没用满,算力也没跑满&…

📰

注意力机制+CNN一维信号分类:MATLAB实现与避坑指南

简介:基于注意力机制的CNN数据分类Matlab实现资源,面向计算机、电子信息、数学等专业学生及需要完成课程设计、期末大作业或毕业设计的开发者。资源包含6个文件,其中主程序main.m为完整可运行的模型入口,4个png为网络结构或实验结…

📰

vLLM推测解码实操:从原理到调优,为大模型推理加速

1. 推测解码到底是什么:从“一个字一个字蹦”到“批量猜测再验证”1.1 为什么大模型推理这么慢:自回归的枷锁用过vLLM做线上推理服务的朋友应该都有体感,大模型生成token的速度,跟它“咬文嚼字”的方式是强绑定的。自回归解码的本…

📰

AI重塑编程行业:从LLM代码生成到可视化数据,附可复用代码案例与TaoToken统一API接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬