学习了解 << EOF 语法规则(Here Document)

Published: 2024-01-26

Tags: Shell

本文总阅读量

前些天看 Kubernetes 文档的时候,看到有命令如下:

$ tee files/kuard-pod.yaml << EOF
apiVersion: v1
kind: Pod
metadata:
  name: kuard
spec:
  containers:
    - image: gcr.io/kuar-demo/kuard-amd64:1
      name: kuard
      ports:
        - containerPort: 8080
          name: http
          protocol: TCP
EOF

在之前也见过类似的用法,比如使用的 cat 命令写入配置

$ cat > myapp.conf <<EOF 
port=8080
host=localhost
EOF

它们的作用都是将多行内容写入到文件,可以在终端运行,无需借助编辑器。不局限于写文件,也可以让 redis-cli 执行多行命令,只要是支持标准输入的命令都可以使用

$ redis-cli <<EOF
SET key1 "value1"
GET key1
DEL key1
EOF

这种用法是 here document 提供的功能,此处暂不直接抛出定义,先进行两个简单的测试

tee 和 cat 命令简介

虽然命令很简单,但这里也对 tee 进行简要的说明,tee 命令会从标准输入设备读取数据,将其内容输出到标准输出,同时将内容保存到文件。

$ echo 'Hello World' | tee hello.txt
Hello World

Tee 命令可以同时输出到多个文件 tee hello.txt hello2.txt,正如它名字中的 “T”,一个输入,多个输出

相较于 tee,cat 命令就太常见了,它可以将文件内容读出,打印到标准输出

$ echo "Hello World" | cat  > hello.txt

# 使用 cat 多此一举,完全可以使用 echo 搭配重定向操作符写入文件
$ echo "Hello World"  > hello.txt

从示例可以看到,它们写入的都是单行内容,不容易多行写入,或是需要在文本内添加 “\n” 换行符才可以

$ echo 'Hello World\nnew line' | tee hello.txt
Hello World
new line

如果需要写入带有格式的配置文件或是代码,手动添加换行符是很繁琐的

大块文本作为标准输入,这就要借助今天介绍的 “<<EOF” 语法

“<<EOF” 语法介绍

经常部署的的朋友会看到很多示例的格式都如下所示

$ tee filename.txt << EOF
Content Line 1
Content Line 2
Content Line 2
EOF

这里的 EOF 怎么看都是一个特殊符号,其实不然,它只是人们的约定俗成,易于理解的字符串。

官方文档中命令描述如下

COMMAND <<InputComesFromHERE
...
...
...
InputComesFromHERE

它是一个自定义的字符串,以下是可执行示例,可执行测试

$ cat > test.sh <<END_OF_SCRIPT
cat <<EOF
hello
EOF
END_OF_SCRIPT

知道它是一个自定义字符串,保持首尾相同即可,接下来的示例,我们还是使用 EOF 字符串

使用引号避免变量替换

前几天想将一段 Shell 写入到脚本文件,后续执行脚本文件能够输出环境变量内容

$ NAME=david
$ cat > echo-hello.sh <<EOF
echo hello, $NAME
EOF

执行后查看脚本文件内容为 echo hello, david,而我希望写入的是 echo hello, $NAME,不符合预期,只需要用引号(单引号、双引号均可)包裹起始 EOF 就能解决,命令如下:

$ cat > echo-hello.sh <<'EOF'
echo hello, $NAME
EOF

除了单引号和双引号,也可以使用反斜线 << \EOF,效果是一样的

注意结尾 EOF 前后空格

在起始字符串前后添加空格对命令无影响

$ cat > echo-hello.sh <<      'EOF'     
echo hello, $NAME
EOF

但是结尾的 EOF 前后都不能有空格和其它字符。

Here Document 内容范围

之前提及过写配置的例子如下,cat 后紧跟重定向符号

$ cat > myapp.conf <<EOF 
port=8080
host=localhost
EOF

如果调整管道符到起始 EOF 的后方,也是可以工作的

$ cat <<EOF > myapp.conf
port=8080
host=localhost
EOF

这是因为当 Shell 遇到起始标记 <<EOF 时,它会将标记之后的行视为 Here Document 的内容,直到遇到单独一行的 EOF 才停止

如果我们在当前行添加一个 “somestring”,以下命令会报错

$ cat > myapp.conf <<EOF somestring
port=8080
host=localhost
EOF

这是因为命令等效为 " | cat > myapp.conf somestring",自然 cat 不知道 something 是什么,如果把 cat 换成 tee 命令进行测试,执行不会报错

$ tee > myapp.conf <<EOF somestring
port=8080
host=localhost
EOF

此时的命令等效于 " | tee > myapp.conf somestring",tee 把 somestring 当成文件名,把 也写到了名为 somestring 的文件中

以上示例可以更清晰的感受到 here document 的内容范围

连接符可以消除 但不能除掉空格

在 Shell 脚本中,如果我们需要通过判断语句决定写文件,就会遇到这个问题。

# 错误的示例,执行会报错 line 6: syntax error: unexpected end of file
if true; then
    cat <<EOF
    a
    EOF
fi

好办,可以使用 <<- EOF,即增加 “-” 连接符删除掉 ,但是 “-” 不支持去掉空格,当前较好的解决办法就是手动删除空格,即便看起来脚本不是美观

if true; then
    cat <<EOF
a
EOF
fi

改写一个 Docker Install 命令

了解了 <<EOF 牌锤子,接下来找一个钉子实践,Ubuntu 安装 Docker 的文档中有一个命令使用反斜线换行

$ echo \
    "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
    $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
    sudo tee docker.list > /dev/null

生成的 docker.list 内容如下

deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu   focal stable

如果使用 <<EOF 改写,改写后的命令:

$ sudo tee docker2.list > /dev/null <<EOF
deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable
EOF

看着还是是挺清晰的,隐约记得 Docker 安装时之前好像是用的 <<EOF,如果没记错的话,现在使用 echo 可能的原因就是 <<EOF 不是所有人都了解,而 echo + 反斜杠了解的人更多,再者 $(. /etc/os-release... 起始的内容,容易误导人以为这是两个命令。

果然手里拿着锤子不能看哪里都是钉子,<<EOF 虽好,也不能贪杯,需要结合场景选择适合的写法。

更多 here document 使用示例,可以参考官方文档:Here Documents

参考