Linux系统输入重定向:Bash Here-Document 详解:`<<TAG` 到底是什么 摘要Here-Document 是 Shell 脚本中非常常用的输入重定向方式。它可以把脚本中的多行内联文本直接作为标准输入传递给命令避免额外创建临时文件。本文围绕command file、command TAG和command (command)的区别展开系统说明 Here-Document 的基本语法、结束标识符、变量展开规则、缩进写法以及实际使用场景。一、概述为什么需要 Here-DocumentHere-Document是指在 Shell 脚本中把一段内联多行文本作为标准输入Standard Input, stdin传递给命令的语法。简单来说它解决的是“我不想单独准备一个文件但又想给命令传入多行内容”的问题。在 Linux 或 Unix Shell 中很多命令都可以从 stdin 读取内容例如cat、grep、wc、mysql、ssh、ftp等。传统做法通常是先准备一个文件再使用输入重定向commandinput.txt但如果输入内容很短或者这段内容只在当前脚本中使用一次单独创建文件就显得不够简洁。此时 Here-Document 就很适合commandTAG 多行文本内容 TAGHere-Document 的核心价值是把“外部文件输入”变成“脚本内联输入”。它让脚本更集中、更易读也更适合自动化场景。二、先理解三种常见输入方式在正式理解TAG之前需要先区分三种容易混淆的写法 file、TAG和 (...)。写法名称输入来源典型用途command file普通输入重定向外部文件把文件内容作为 stdincommand TAGHere-Document脚本内联文本把多行文本直接写在脚本中command (other-command)进程替换其他命令的输出把命令输出伪装成文件输入2.1 file从文件读取标准输入 file是最基础的输入重定向方式它会把指定文件内容交给命令作为 stdin。例如cat/etc/passwd这条命令的含义是让cat从/etc/passwd文件读取输入而不是从键盘读取。它适合输入内容已经存在于磁盘文件中的场景例如读取配置文件、日志文件、数据文件等。2.2TAG从脚本内联文本读取标准输入TAG不依赖外部文件而是把脚本中两行 TAG 之间的内容作为 stdin。例如catTAG Hello, world! This is a here-document example. TAG这段脚本会把中间两行文本传给cat最终输出Hello, world! This is a here-document example.这里的TAG只是一个结束标识符可以换成EOF、END、DATA等任意不容易和正文冲突的字符串。2.3 (...)把命令输出当成输入 (...)是进程替换Process Substitution它会把另一个命令的输出转换成当前命令可读取的输入流。例如wc-l(printfa\nb\nc\n)这条命令的含义是先执行printf a\nb\nc\n再把它的输出作为wc -l的 stdin。它和 Here-Document 的区别在于Here-Document 的输入内容写在脚本里而进程替换的输入内容来自另一个命令的运行结果。三、Here-Document 的基本语法Here-Document 的基本格式如下commandTAG 多行文本内容 TAG其中组成部分含义command接收 stdin 的命令例如cat、grep、wc启动 Here-DocumentTAG结束标识符由用户自定义中间文本实际传给命令的标准输入内容结束标识符不是命令也不是关键字它只是告诉 Shell多行输入到这里结束。例如catEOF 第一行内容 第二行内容 EOFShell 会把EOF之前的两行文本作为 stdin 传给cat而EOF本身不会出现在输出中。四、结束标识符 TAG 的定义规则Here-Document 看起来简单但很多错误都出在结束标识符上。尤其是空格、缩进和大小写问题容易导致脚本无法正常结束输入。4.1 TAG 可以是任意字符串TAG 本身没有特殊语义只要起始标识符和结束标识符完全一致即可。常见写法包括catEOF 内容 EOFcatEND 内容 ENDcatDATA 内容 DATA实际开发中建议使用不容易和正文冲突的标识符。例如正文中可能出现EOF就可以改用MY_CONFIG_END。4.2 结束标识符必须独占一行结束标识符所在行只能写标识符本身前后不能有空格或其他字符。正确写法catTAG 内容 TAG错误写法catTAG 内容 TAG上面错误的原因是结束行前面多了一个空格Shell 不会把它识别为真正的结束标识符。同样下面这种也不正确catTAG 内容 TAG因为TAG后面多了一个空格也会导致匹配失败。4.3 TAG 大小写敏感Here-Document 的结束标识符是大小写敏感的TAG和tag不是同一个标识符。错误示例catTAG 内容 tag这段脚本不会在tag处结束因为起始标识符是TAG结束标识符也必须是TAG。4.4-TAG支持 TAB 缩进普通TAG要求结束标识符顶格书写但脚本中有时为了排版会希望内容跟随代码块缩进。此时可以使用-TAG。-TAG会忽略行首的 TAB 字符但不会忽略普通空格。例如cat-TAG 这里前面是 TAB 缩进 TAG需要注意的是-TAG只能处理 TAB 缩进不能处理空格缩进。如果编辑器把 TAB 自动转换成空格就可能达不到预期效果。五、变量展开与原样输出Here-Document 最重要的细节之一是是否进行变量展开和命令替换。5.1 默认写法会展开变量和命令默认的TAG会在命令执行前进行变量展开、命令替换和部分转义处理。例如NAME孙总catTAG Hello,$NAME! 当前时间$(date%H:%M)TAG这段脚本中内容执行结果$NAME会被替换成变量值$(date %H:%M)会执行命令并替换成命令输出因此实际输出可能是Hello, 孙总! 当前时间21:30默认展开适合生成动态文本例如配置文件、SQL 语句、提示信息和自动化命令输入。5.2 单引号写法禁止展开如果希望 Here-Document 中的内容完全原样输出可以给 TAG 加单引号。NAME孙总catTAG Hello, $NAME! 当前时间$(date %H:%M) TAG输出结果是Hello, $NAME! 当前时间$(date %H:%M)这里$NAME不会被替换$(date %H:%M)也不会被执行。TAG适合输出模板、脚本片段、配置样例、Markdown 文本等不希望被 Shell 提前解释的内容。5.3 双引号写法通常不改变展开行为有时也会看到这种写法catTAG Hello, $USER! TAG在常见 Shell 中只要结束标识符被引号包裹Here-Document 正文通常会按照“引用标识符”的规则处理即不执行变量展开。为了避免误解实际写脚本时更推荐明确使用catTAG 原样文本 TAG想要原样输出就优先使用TAG想要变量替换就使用TAG。这样最清晰也最不容易出错。六、Here-Document 与重定向的关系6.1 本质上都是 stdin 重定向command file和command TAG的最终目标一样都是给 command 提供 stdin。区别只在于输入来源不同写法输入来源是否需要外部文件command file文件内容需要command TAG脚本内联文本不需要例如grepfooEND foo bar foobar END它等价于把下面三行内容交给grep foofoo bar foobar因此输出结果是foo foobar6.2 Here-Document 会先准备输入再执行命令Shell 执行 Here-Document 时大体可以理解为以下过程未引用被引用Shell 读取脚本发现 Here-Document 语法读取起始 TAG 与结束 TAG 之间的文本TAG 是否被引用执行变量展开和命令替换保留原始文本构造临时输入流把输入流作为 stdin 传给 command执行 command这个流程说明了一个关键点Here-Document 不是在命令运行过程中一行一行临时输入而是 Shell 先构造好输入流再交给命令执行。6.3 stdin 与 stdout 可以同时重定向Here-Document 负责 stdin普通输出重定向负责标准输出Standard Output, stdout。二者互不冲突。例如catTAGout.txtToday is$(date). TAG这条命令的执行逻辑是步骤动作第一步Shell 处理 Here-Document生成 stdin第二步cat读取 stdin第三步cat的 stdout 被写入out.txt如果还需要处理标准错误Standard Error, stderr也可以继续追加错误重定向some_commandTAGout.txt2error.loginput text TAG其中 out.txt处理 stdout2 error.log处理 stderrTAG处理 stdin。七、典型使用示例7.1 输出多行文本最简单的使用场景是直接输出多行文本。catTAG Hello, world! This is a here-document example. TAG输出结果Hello, world! This is a here-document example.这个例子中cat接收了两行 stdin并把它们原样输出到终端。7.2 给 grep 提供临时文本Here-Document 很适合给命令临时提供一小段测试数据。grepfooENDMARK foo bar foobar ENDMARK输出结果foo foobar这里grep foo会从 Here-Document 提供的三行文本中筛选包含foo的行。7.3 禁止变量展开当文本中包含$、$(...)、反引号等 Shell 特殊内容时应该使用TAG防止误展开。NAME孙总catTAG Hello, $NAME! TAG输出结果Hello, $NAME!这里的$NAME被当作普通字符串而不是变量。7.4 生成配置文件Here-Document 常用于自动生成配置文件尤其适合部署脚本和初始化脚本。APP_NAMEdemo-serviceAPP_PORT8080catEOFapp.confname$APP_NAMEport$APP_PORTmodeproduction EOF生成的app.conf内容是namedemo-service port8080 modeproduction这个场景中默认展开是有用的因为配置文件需要写入变量的实际值。7.5 原样生成模板文件如果要生成的是模板文件而不是最终配置文件就应该禁止展开。catEOFtemplate.confname$APP_NAME port$APP_PORT modeproduction EOF生成的template.conf内容是name$APP_NAME port$APP_PORT modeproduction生成模板时使用EOF生成最终文件时使用EOF这是非常实用的判断规则。八、常见误区与排查方法8.1 误区一结束标识符前后有空格很多 Here-Document 报错都不是语法本身复杂而是结束标识符没有精确匹配。错误示例catEOF hello EOF这里EOF前面有一个空格Shell 不会认为它是结束标识符。正确写法catEOF hello EOF8.2 误区二把空格缩进当成 TAB 缩进-EOF只会去掉 TAB不会去掉普通空格。cat-EOF 这是 TAB 缩进 EOF如果脚本中看起来缩进了但实际是空格-EOF不会按预期工作。遇到此类问题应检查编辑器是否开启了“将 TAB 转为空格”的设置。8.3 误区三忘记变量会被提前展开下面这段脚本看似是在写模板实际会提前展开变量catEOF 用户变量$USEREOF如果希望输出$USER字符串本身应该写成catEOF 用户变量$USER EOF判断是否加引号的关键不是 TAG 本身而是正文内容是否希望被 Shell 解释。8.4 误区四把 Here-Document 当成注释块有人会用这种方式“注释”多行内容:COMMENT 这里是一段不会执行的文本 COMMENT这种写法利用了:命令什么也不做的特性看起来像多行注释。但它并不是真正的 Shell 注释而是把多行文本作为 stdin 传给空命令。这种写法可以用但不建议滥用。真正重要的说明仍然应该使用#注释以免降低脚本可读性。九、实践建议什么时候该用 Here-DocumentHere-Document 适合“短文本、临时文本、模板文本、自动化输入”这几类场景。场景是否适合 Here-Document原因临时给命令传几行测试数据适合不必创建临时文件自动生成配置文件适合脚本和配置模板集中管理写 SQL 初始化语句适合多行 SQL 可读性更强写很长的大型文件不太适合会让脚本过长需要频繁复用的内容不太适合独立文件更易维护内容中包含大量复杂转义视情况而定推荐使用EOF原样输出在实际脚本中可以按下面规则选择需求推荐写法从已有文件读取输入command file在脚本中写死多行输入command EOF需要保留$VAR等原样内容command EOF输入来自另一个命令输出command (other-command)需要美观缩进且使用 TABcommand -EOF十、总结Here-Document 的本质是一种把脚本内联多行文本重定向为 stdin 的机制。它和 file一样属于输入重定向只是输入来源从外部文件变成了脚本中的文本块。理解 Here-Document需要抓住四个核心点第一TAG表示开始读取内联文本直到遇到独占一行的 TAG 为止。第二TAG 是用户自定义结束标识符没有特殊语义但起始和结束必须完全一致。第三默认写法TAG会展开变量和命令替换而TAG会原样保留文本。第四-TAG只会忽略 TAB 缩进不会忽略普通空格。在自动化脚本、配置生成、命令批量输入和临时测试数据构造中Here-Document 都非常实用。掌握它之后脚本不需要依赖大量临时文件也能清晰表达复杂的多行输入逻辑。写 Shell 脚本时能够正确区分 file、EOF和 (...)基本就掌握了输入重定向中最容易混淆也最实用的一组能力。