Shell Here Document 用法详解:语法、变量展开与远程执行 📅 发布时间:2026/9/15 8:32:09 👁 浏览次数: 我刚入行那会儿第一次在项目脚本里看到EOF时整个人是懵的。这玩意长得像输入重定向用法又像临时文件放在cat后面能生成一段文本塞给ssh又能远程执行一整套命令。后来搞清楚了才发现Here Document中文一般叫“此处文档”或“嵌入式文档”是 Shell 里被严重低估的语法之一尤其是在写部署脚本、批量生成配置、远程执行多行命令时几乎绕不开它。这篇内容我打算从实际使用出发把 Here Document 的语法细节、常见坑位、真实场景一次性讲透争取让新手看完能直接上手也让写过一段时间脚本的人查漏补缺。文章涉及到的例子都以 Bash 为主但大部分语法在 sh、zsh 里同样适用。1. 什么是 Here Document从常见困惑说起1.1 它解决的是什么问题先想想一个很常见的需求你想在 Shell 里把一段多行文本写入文件。如果是两三行用echo拼接还能忍比如echo line1 line2 line3 demo.txt但如果你要生成的是一段 Nginx 配置、一份 SQL 初始化脚本、或者一个带变量替换的服务配置文件用echo写会特别痛苦既要转义双引号又要处理换行和缩进读起来也费劲。Here Document 就是用来解决这个痛点的它允许你在命令行或脚本里直接嵌入一段多行文本然后把这整段内容作为标准输入交给某个命令处理。这里可以先记一个最简单的形式cat EOF demo.txt Hello, Shell! This is a here document. Line count: 3 EOF有个核心逻辑要搞清楚 EOF并不是cat的特殊参数而是 Shell 的输入重定向语法。Shell 会读取后面直到“定界符”为止的所有内容把它们作为标准输入传给cat。cat拿到这段输入后再用重定向到文件。理解这一步后面很多问题比如为什么EOF前后不能乱加空格就都迎刃而解了。1.2 基本语法结构拆解标准的 Here Document 写法是命令 定界符 ... 多行内容 ... 定界符这里“定界符”delimiter可以随便取名字常见的有EOF、EOL、END你用FOO、MYMARKER也完全没问题关键是首尾必须一模一样。Shell 不会把定界符当关键字看待它只是用来标记“内容到这里就结束了”的边界。补充一个容易踩坑的细节和定界符之间的空格是可选的但你写内容时千万别在定界符前后乱加空格。比如cat EOF hello EOF这样没问题。但如果你写成cat EOF hello EOF # 这里多了一个空格Shell 就找不到结束标记命令会一直等待输入直到你手动按下 CtrlC 或输入一个不带空格的EOF才结束。这个问题我在新手阶段遇到过不止一次后面会专门展开讲。2. 核心语法细节变量展开、引号与定界符这条语法链路上最容易出问题的就是“到底哪些内容会被 Shell 解释”。我把它拆成几个关键规则一个个说清楚。2.1 默认情况下变量和命令替换都会执行Here Document 默认会做变量展开variable expansion和命令替换command substitution。也就是说你写在文档里的$变量名会被替换成对应值$(命令) 里的命令也会被先执行一遍。看个例子name张三 cat EOF 你好$name 今天是 $(date %F) EOF这段输出会是你好张三 今天是 2025-01-09这个特性在生成动态配置文件时非常有用。比如写一个脚本模板目标是根据环境变量生成不同环境的配置heredoc 可以直接把变量塞进内容里省去繁琐的转义server_nameapi.example.com port8080 cat EOF nginx.conf server { listen $port; server_name $server_name; location / { proxy_pass http://127.0.0.1:3000; } } EOF这种写法比用sed去替换占位符直观得多也比echo拼接字符串靠谱得多。2.2 单引号定界符关闭所有“魔法”如果你不希望变量和命令被展开想要“原样输出”那就把定界符用单引号包起来cat EOF 你好$name 今天是 $(date %F) EOF输出会原封不动地变成你好$name 今天是 $(date %F)单引号把定界符包住之后Shell 就关闭了文档内部的变量展开和命令替换。这背后的原因是 Shell 在解析 heredoc 时发现定界符带单引号就会以“字面量”模式处理文档内容。实际中这个特性哪里用得上呢最常见的场景是写脚本的脚本。比如你在一个自动化部署脚本里需要生成另一个脚本文件而生成出来的脚本里又包含一些$符号和命令替换这时就必须用单引号定界符否则内容会被外层 Shell 提前“吃掉”cat EOF /tmp/cleanup.sh #!/bin/bash for file in /var/log/*.log; do echo 处理日志文件: $file gzip $file done EOF如果你在这里用了不带引号的EOF$file会在生成脚本时被展开结果就是空值生成出来的脚本根本没法用。这个坑非常典型几乎是每次分享 heredoc 时我都会重点强调的一条。2.3 用 - 忽略前导制表符有时候你在脚本里嵌套 heredoc为了让代码格式好看希望文档内的内容也相对缩进。此时可以在后面加一个减号-Shell 会忽略文档内每一行开头所有制表符注意是 tab不是空格#!/bin/bash if true; then cat - EOF 第一行 第二行 EOF fi这样输出内容里不会带上那部分 tab 缩进。不过有个细节得提醒一下-只忽略制表符不忽略空格缩进。如果你用空格来缩进内容里该有的空格还是会保留。说实话这个特性因为受限于 tab 和空格的区别实际使用率不算太高但知道了总比碰到时一脸茫然强。2.4 把 heredoc 内容同时输出到屏幕和文件配 teecat配合重定向是把内容写到文件但如果你又想在终端看到内容又想把内容保存下来直接用cat就得写两遍。这时候可以用teecat EOF | tee config.txt [settings] themedark langzh EOFtee会把标准输入原样写到文件同时再输出一份到标准输出。这个命令在调试阶段特别有用你既能确认文档内容有没有变量替换错误又能把结果落到文件里。类似的思路还可以用在需要同时传给后续命令的场景比方说通过管道把 heredoc 内容交给grep、sed继续处理。2.5 与 sudo 配合让特权命令也能读入多行内容还有一个比较冷门但实用的点sudo默认情况下读取的是终端直接对 heredoc 用管道可能会遇到权限问题。比如cat EOF /etc/profile.d/myenv.sh export MY_ENVhello EOF如果当前用户没有/etc/profile.d的写权限这个命令会报 permission denied。正确做法有两种。第一种把整个 cat 放到 sudo 下执行注意重定向是放在 sudo 外面的需要写成sudo bash -c cat /etc/profile.d/myenv.sh EOF export MY_ENVhello EOF第二种用sudo tee这种更简洁也更推荐cat EOF | sudo tee /etc/profile.d/myenv.sh export MY_ENVhello EOFsudo tee的写法相当于把“读入 heredoc 内容”和“用 root 权限写文件”分开了既安全又直观。建议在需要写系统级配置文件时优先用这个写法。3. 实际使用场景从文件生成到远程执行知道语法的“原理”之后最重要的是能顺畅地用在真实场景里。我按平时写脚本的频率从高到低列几个典型用法给大家参考。3.1 生成配置文件与多行文本文件这是 heredoc 最基础也最高频的用途。我前两年维护过一批内部工具每个工具部署时都要往服务器上写一份 YAML 配置。用 heredoc 配合变量展开一份模板就能覆盖开发、测试、生产三套环境envproduction app_port9000 log_levelinfo cat EOF /opt/myapp/config.yaml app: name: demo env: $env port: $app_port logging: level: $log_level output: json EOF脚本跑一遍配置瞬间生成。而且因为 heredoc 里的内容就是最终文件的完整结构可读性比一行行echo好太多了。写文件时有一个小技巧如果你想在写入文件的同时保留文件原有内容追加而不是覆盖把改成就行cat EOF /etc/hosts 10.10.10.10 demo.internal 10.10.10.11 api.internal EOF这个在维护 hosts、追加任务计划crontab时非常顺手。3.2 在脚本里嵌入 SQL、Python 等子程序写自动化脚本时经常需要在 Shell 里临时执行一段 Python 或 SQL。如果你把这段代码写在单独文件里发布和部署就多一个文件依赖。用 heredoc 直接内嵌既保证了脚本的完整性又能让逻辑集中在一起。比如一个数据导入脚本可以先在 Shell 里处理变量再交给 Python 做逻辑处理table_nameuser_snapshot date_str$(date %Y%m%d) python3 EOF import os table ${table_name} date_val ${date_str} print(f开始导出 {table}_{date_val}) # 这里继续写你的 Python 逻辑 EOF注意这里 Python 代码里的$变量其实是被 Shell 先展开了的因为定界符没加引号所以传参本质上是“文本替换”而非“环境变量传递”。如果你的 Python 代码里大量用到$符号或者你不希望内容被 Shell 动过那就用 PYEOF。究竟是让外层 Shell 展开还是保持原样取决于你最终想要的执行效果。数据库操作也一样。我以前写过一个临时表清理脚本就是用 heredoc 把 SQL 直接喂给 mysql 客户端mysql -u root -p${db_pass} EOF USE mydatabase; DELETE FROM temp_records WHERE created_at DATE_SUB(NOW(), INTERVAL 30 DAY); OPTIMIZE TABLE temp_records; EOF这样写比mysql -e ...拼接多条 SQL 爽得多不用担心引号嵌套地狱。3.3 SSH 远程执行多行命令这是我觉得 heredoc 最体现价值的地方。你需要在远程机器上一次性执行多行命令如果用ssh userhost cmd1 cmd2会遇到两个麻烦一是转义问题本地 Shell 和远端 Shell 两层解析容易出错二是可读性极差复杂指令没法看。用 heredoc 喂给 ssh则自然得多ssh user192.168.1.20 EOF cd /opt/app git pull origin master systemctl reload app echo 部署完成 EOF这里 Shell 会把 heredoc 内容作为标准输入传给 sshssh 再把它交给远端 Shell 执行。整个过程没有多余的引号嵌套非常干净。但这里有一个非常容易踩的坑如果 heredoc 里用了本地变量你希望变量在本地展开还是远端展开默认情况下定界符没加引号Shell 会在本地完成变量展开然后把展开后的结果发送给远端。比如tagv1.2.0 ssh user192.168.1.20 EOF cd /opt/app git checkout $tag EOF实际执行的是git checkout v1.2.0。这通常是我们要的效果但如果不小心让某个$符号在本地被展开成空值命令就会变成git checkout很容易出问题。解决办法还是那套想保留远端展开就加单引号想本地展开就保持默认关键是要知道自己在做什么。3.4 与 for 循环结合生成批量文件heredoc 和循环结合可以批量生成相似文件。比如我想一次性生成 10 个不同端口的 systemd service 文件for port in 8081 8082 8083 8084 8085; do cat EOF /etc/systemd/system/worker-${port}.service [Unit] DescriptionWorker on port ${port} Afternetwork.target [Service] ExecStart/usr/bin/python3 /opt/worker.py --port ${port} Restartalways [Install] WantedBymulti-user.target EOF done循环每执行一次就生成一个包含对应端口的 service 文件。这种做法在执行批量部署、批量测试时非常省力。不过我实际使用时一般会先写成$port如果遇到变量紧跟字符的情况再用${port}包裹避免 Shell 解析混乱。4. 常见问题与排查技巧实录Here Document 本身不算复杂但越简单的语法越容易在日常细节上“翻车”。我把这些年遇到过的高频问题整理成了一个速查表下面展开讲讲每个问题的现象和解决办法。4.1 定界符末尾有空格或不可见字符这是使用 heredoc 最经典的大坑。你写完文档内容最后一行打上EOF命令却一直卡住不结束直到你手动按 CtrlC。原因就是定界符前后多了空格或者EOF后面有个看不见的\rWindows 换行符。排查方法很简单用cat -A查看文件或直接在终端输入od -c检查结尾字符。比如说cat EOF hello EOF如果EOF后面多了一个空格命令就不会结束。我在 Windows 上用 VS Code 编辑脚本时特别容易踩这个坑因为编辑器默认换行符可能是 CRLF导致EOF\r而不是EOF。解决办法是统一使用 LF 换行或者在脚本里用sed -i s/\r$//清理。4.2 变量展开时机搞错有很多朋友在写 heredoc 时纠结“我的变量为什么没被替换”。别急先确认两件事定界符加了单引号没有如果加了整个文档就是字面量不展开任何变量。变量是否在 heredoc 之前就已经赋值heredoc 是在重定向那一瞬间读取内容的如果变量是在 heredoc 之后的某行才赋值那展开时它自然是空值。我自己习惯用一个小规则判断能不能接受“Shell 先替换再输出”能接受就不加引号不能接受就加引号。比如写 Dockerfile 时内容里的$变量是给容器内部用的定界符必须加单引号否则宿主机 Shell 会抢先把变量展开掉那就乱了。4.3 文档里包含特殊字符导致解析异常Here Document 里的内容并不是“绝对安全”的。默认情况下以下这些内容会被 Shell 特殊处理$变量展开为变量的值$(命令)和反引号命令执行命令并替换为输出\转义符会影响部分字符的解释如果文档内容里恰好包含了这些符号但你又希望它们保持原样最简单的办法就是给定界符加单引号。比如你要生成一段 Markdown 文档里面写了$(date)这种文本不加引号就会被替换成实际日期。我写技术文档时经常被这个“惊喜”坑到现在凡是含金钱符号或反引号的内容默认都使用 EOF。再补充一个反斜杠的细节即使在默认模式下heredoc 内部的\也会被 Shell 当作转义符处理比如\\会被解析成单个\\$会被解析成字面意义的$。这跟双引号字符串的处理风格很像但很多人没意识到。4.4 heredoc 与 here string 的区别搜索时经常有人把 heredoc 和 here string放一起比较。简单说heredoc命令 EOF适合多行内容here string命令 内容适合单行字符串比如grep error this line has an error会在字符串末尾自动加一个换行符然后把整串内容作为标准输入传递给命令。它比echo xxx | grep省一个管道进程但本质上只适合单行或少量文本。真有大批量内容还是用 heredoc 更清晰。4.5 报错 “syntax error near unexpected token”这种报错多半出现在你把 heredoc 写在同一行却没有换行的时候。比如cat EOF file.txt hello EOF这种写法不符合语法规则Shell 会直接报错。heredoc 的内容必须另起一行结束符也必须单独占一行。就算你想在同一行内用;分隔命令也要注意结束符必须独立成行。我的习惯是先写完整个 heredoc再在前后补充其他命令不要追求“一行流”。5. 一些经验心得什么时候该用什么时候别用写了这么多年 Shell我用 heredoc 的频率非常高但也逐渐总结出一些使用边界。5.1 喜欢用 heredoc 的四个场景第一需要生成多行配置文件尤其带变量替换的场景heredoc 完胜。第二脚本中需要内嵌一段其他语言Python、SQL、Perl用 heredoc 保持脚本独立性方便分发。第三SSH 远程执行复杂命令heredoc 让远端脚本逻辑一目了然并且可以同时利用本地变量。第四需要在文档中构造大量测试数据时heredoc 可以直接填充省去了临时文件管理。5.2 不建议用 heredoc 的场景如果内容只有一行用echo xxx或 here string 就够了没必要上 heredoc。另外如果你生成的文本里既有大量特殊字符需要转义又有变量要展开此时 heredoc 反而容易让逻辑混乱不如用模板文件加sed替换或者改用envsubst。还有一点如果你写的是团队共享脚本建议在 heredoc 前加一行注释说明定界符为什么加引号、变量是本地展开还是远端展开。这样别人维护代码时不用靠猜。像我们团队内部约定俗成是所有 heredoc 的定界符必须使用大写字母加数字如EOF1禁止使用end、END_TEXT这种容易和正文内容混在一起的词凡是内容中含$的一律加单引号定界符。5.3 我对“语法糖”的态度There are people who call heredoc a “syntax sugar.” 我倒觉得它更像一种组织代码的方式。它让“输入数据”和“处理命令”的边界变得很清楚一旦理解它只是“标准输入的来源”你就能灵活地和管道、重定向、ssh、docker exec 等组合使用。比如配合docker exec -i执行容器内命令docker exec -i mysql_container mysql -uroot -p$MYSQL_ROOT_PASSWORD EOF CREATE DATABASE IF NOT EXISTS test; USE test; CREATE TABLE demo (id INT); EOF这里必须加-i否则不会把标准输入传入容器。类似的组合还有很多一旦你掌握 heredoc 的底层原理就能自己推理出哪些场景能用、哪些需要调整。最后再分享一个小技巧调试 heredoc 时我喜欢先用cat EOF把内容直接打到屏幕上确认输出无误后再把cat换成实际的写入命令或远程命令。这样可以把“内容拼写问题”和“命令执行问题”分开排查定位速度会快非常非常多。毕竟 heredoc 本身不难难的是把它放到复杂场景里时我们能保持思路清晰。