相信只要在Shell、PHP、Perl等语言中接触过『多行字符串』的人,对Heredoc以及其特征性的『EOF』标识符不会陌生。但关于Heredoc背后一些有意思的玩法,却很少有人接触。这篇文章为大家介绍一下Heredoc的基础操作,历史由来,及其进阶使用。

0x01

首先我们来介绍一下Heredoc。

Heredoc其实是一种描述输入流的字面量(file literal),字面量的概念可以粗略理解为『写死在代码里的常量』,通常用于在代码内包含文件(多行字符串其实也是一种文件),起源于Unix Shell,被Bash Shell、PHP、Perl、Ruby、Python等语言/Shell所吸收兼容。

尽管每个语言对Heredoc的实现各有不同,但其核心是不变的,即保留Heredoc文本段内所有字符(包括非显示字符),且以约定的标识符作为Heredoc的终止。

0x02

在这里,我们介绍几门常用语言的Heredoc使用:

Bash Shell

cat << EOF
Hello, World
Hello, 123
EOF

以上代码段,将EOF包裹的文字原封不动以文件流的方式输出到cat,cat再将『文件』输入流输出到标准输出,即终端。

为什么说cat接收到的是文件呢?,上面的Heredoc可以理解为如下文件串:

Hello, World\nHello, 123\nEOF

注意:实际上EOF并不是一个实际的字符,而是操作系统所提供表示文件到达结束的一个标识,在Linux Shell中,该标识符可使用Ctrl(Control) + D输入,例如在Python交互式命令行中,可以通过按下该组合键实现退出命令行的功能,其原理与读取一个Python脚本然后执行到操作系统返回EOF的位置相同。

如果希望在Heredoc中插入变量,可以进行如下操作:

$ cat << EOF
> \$ Working dir "$PWD" `pwd`
> EOF
$ Working dir "/root" /root

使用\来转义可能会被解析为变量或命令的标识符。

如果不希望Heredoc解析其中所有变量,希望原样输出,可以进行如下操作:

$ cat << 'EOF'                              # 也可使用双引号代替
> \$ Working dir "$PWD" `pwd`
> EOF
\$ Working dir "$PWD" `pwd`

注意,Heredoc会保留开头的EOF到结尾的EOF之间的所有字符,包括缩进符号与空白符。

但是如果我们在撰写Shell脚本时,需要使用到缩进以保证其高可读性,这个时候我们可以在<<符号后面加上一个-符号,以表示忽略Heredoc中每行开始的的缩进符号(包括空格和Tab):

$ cat <<- EOF
>           Hello, World!
>           Hello, 123!
> HELLO
> EOF
Hello, World!
Hello, 123!
HELLO

上面所有的例子都使用了EOF这一字串作为Heredoc开始与结尾的标识符,实际上我们可以使用任意保证在文本中不会重复出现的字串来作为该标识符:

$ cat << END_DOC
> Hello, World!
> END_DOC
Hello, World!

修改标识符的方法,在类似的其他语言中同样有效。

我们同样可以使用Heredoc来实现无编辑器写入多行文件,这在一些未提供编辑器(或只提供不顺手的编辑器)环境中(例如Busybox)极为实用:

$ cat << EOF > ~/test.txt
> Hello, World!
> Hello, 123!
> EOF

这样就可以将文件覆盖写入test.txt,如果将>修改为>>就可以实现追加写入,这与常规的Shell命令一致。

与Heredoc相类似的,还有Herestring,使用方法同样很简单:

$ cat <<< hello
hello
$ cat <<< 'hello
world'
hello
world

Herestring的主要用途主要还是重定向单行文本,例如在bc(一个*nix计算器程序)中,我们可以使用以下方法解决bc需要交互式Shell的问题,以实现脚本化:

$ bc <<< 2^10
1024

单行文本的输出同样可以使用echo完成,此处不再赘述,可参考man echo

Perl

在Perl中,同样有Heredoc的实现,主要用于存储多行文本:

my $sender = "Buffy the Vampire Slayer";
my $recipient = "Spike";

print << "END";

Dear $recipient,

I wish you to leave Sunnydale and never return.

Not Quite Love,
$sender

END

输出:

Dear Spike,

I wish you to leave Sunnydale and never return.

Not Quite Love,
Buffy the Vampire Slayer

这里EOF两边的双引号表示Heredoc内部的变量允许被解析。如果不希望被解析可使用单引号代替:

print <<'END';
Dear $recipient,

I wish you to leave Sunnydale and never return.

Not Quite Love,
$sender
END

输出:

Dear $recipient,

I wish you to leave Sunnydale and never return.

Not Quite Love,
$sender

与Bash Shell类似,在Perl中可以使用在<<符号后加入~符号的形式来解决缩进问题:

该功能只在Perl 5.26之后版本提供

if (1) {
    print <<~EOF;
    Hello there
    EOF
}

注意,Perl中不推荐在开头的EOF处不带引号,在较新的版本中,该操作会造成一个Deprecated警告。

Perl也支持修改EOF定界符为其他符号。

PHP

PHP从Perl中吸收了很多语法,Perl又从Bash Shell中吸收了很多语法,故PHP的Heredoc与以上两门语言类似:

<?php

$name       = "Joe Smith";
$occupation = "Programmer";
echo <<<EOF

    This is a heredoc section.
    For more information talk to $name, your local $occupation.

    Thanks!

EOF;

$toprint = <<<EOF

    Hey $name! You can actually assign the heredoc section to a variable!

EOF;
echo $toprint;

?>

输出:

This is a heredoc section.
For more information talk to Joe Smith, your local Programmer.

Thanks!

Hey Joe Smith! You can actually assign the heredoc section to a variable!

需要注意的是,PHP中Heredoc所使用的符号为<<<而非<<,这是因为PHP同时收纳C语言的一部分语法,即<<>>被使用为位运算符,故使用<<<作为Heredoc标识符。

在PHP5.3版本后,也可以像Perl一样(并且推荐像Perl一样)使用单引号、双引号包裹,称为Nowdoc:

$x = <<<'END'
Dear $recipient,

I wish you to leave Sunnydale and never return.

Not Quite Love,
$sender
END;

输出:

Dear $recipient,

I wish you to leave Sunnydale and never return.

Not Quite Love,
$sender

PHP同样支持修改EOF定界符为其他符号

Ruby

同样作为一门与*nix关系密切的语言,Ruby也从以上各种语言中吸收了Heredoc的语法:

puts <<GROCERY_LIST
Grocery list
----
1. Salad mix.
2. Strawberries.*
3. Cereal.
4. Milk.*

* Organic
GROCERY_LIST

输出:

Grocery list
------------
1. Salad mix.
2. Strawberries.*
3. Cereal.
4. Milk.*

* Organic

Ruby将<<符号作为输入重定向符,也作为Heredoc标识符,这一点与Bash Shell不一样,因此如果需要将多行字符串在Ruby中输出到文件,需要使用两个输入重定向符号:

File::open("grocery-list", "w") do |f|
  f << <<GROCERY_LIST
Grocery list
----
1. Salad mix.
2. Strawberries.*
3. Cereal.
4. Milk.*

* Organic
GROCERY_LIST
end

注意其中的缩进

Ruby同样支持在Heredoc中嵌入变量:

now = Time.now
puts <<-EOF
  It's #{now.hour} o'clock John, where are your kids?
  EOF

输出:

It's 11 o'clock John, where are your kids?

与Perl一致,我们可以使用~符号实现缩进忽略,但由于Perl语言本身依赖缩进,故只会忽略首行缩进长度,并应用在后面的行中:

puts <<~EOF
  This line is indented two spaces.
    This line is indented four spaces.
      This line is indented six spaces.
  EOF

输出:

This line is indented two spaces.
  This line is indented four spaces.
    This line is indented six spaces.

Python

Python由于其独特的语法,表示Heredoc的方法与其他语言有所差异,即使用三个引号包裹多行字符串:

print("""
Customer: Not much of a cheese shop is it?
Shopkeeper: Finest in the district , sir.
""")

输出:

Customer: Not much of a cheese shop is it?
Shopkeeper: Finest in the district , sir.

Python3.6之后,增加了对模板语言的支持,更方便了在多行字符串中插入变量、执行简单的格式化操作:

shop_type = "CHEESE"
accolade = "finest"
print(f"""
Customer: Not much of a {shop_type.lower()} shop is it?
Shopkeeper: {accolade.capitalize()} in the district , sir.
""")

输出:

Customer: Not much of a cheese shop is it?
Shopkeeper: FINEST in the district , sir.

参考:https://en.wikipedia.org/wiki/Here_document