前些天看 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
这是因为命令等效为 "
$ tee > myapp.conf <<EOF somestring
port=8080
host=localhost
EOF
此时的命令等效于 "
以上示例可以更清晰的感受到 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