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