MENU

自动化脚本进阶——Here Document in Shell

2019 年 06 月 16 日 • 阅读: 104 • 脚本

本文译自 TLDP Here Documents

享受当下,朋友们。

        —— 岛,[英] 阿道司·赫胥黎

Here Document 是一段特定用途代码。它通过 IO 重定向,为可交互程序或命令,如 ftpcatex 文本编辑器 等,提供命令列表。

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

特殊符号 << 位于 限制字符串 之前,限制字符串 框住命令列表。如此便产生了,将 命令列表 输出,重定向到 COMMAND 标准输入的效果。它与 可交互程序 < 命令文件 的形式相似,命令文件包含

command #1
command #2

确切的讲,Here Document 几近于以下形式

interactive-program <<LimitString
command #1
command #2
...
LimitString

选择一个充分特殊的 限制字符串,以确保它不会出现在 命令列表 中的任何地方,也不会与其他事项产生混淆。

请注意有时 Here Document 也可与非交互工具及命令产生良好效果,比如,wall

例 1. 广播: 向已登录终端的每个用户发送消息

#!/bin/bash

wall <<zzz23EndOfMessagezzz23
请将你的午餐披萨订单通过邮件发送给管理员。
    (凤尾鱼和蘑菇盖浇需另外付款。)
# 此处开始是额外说明。
# 注意:'wall' 会打印注释行。
zzz23EndOfMessagezzz23

# 上述功能本可更高效地通过以下形式实现
#       wall <message-file
# 然而,在脚本中嵌入消息模板是以脏换快
#+ 的一次性方案。

exit

哪怕是像 vi 文本编辑器 一样,极不可能的参与者,也能与 Here Document 配合良好。

例 2. 虚拟文件: 创建包含 2 行文本的虚拟文件

#!/bin/bash

# 以 'vi' 的非交互用法编辑文件。
# 模拟 'sed'。

E_BADARGS=85

if [ -z "$1" ]
then
    echo "用法: `basename $0` 文件名"
    exit $E_BADARGS
fi

TRAGETFILE=$1

# 向文件插入 2 行并保存

# ---------- Begin here document ----------#
vi $TRAGETFILE <<x23LimitStringx23
i
这是示例文件的第一行。
这是示例文件的第二行。
^[
ZZ
x23LimitStringx23
# ----------- End here document -----------#

# 注意上面的 '^[' 是 <Esc> 的转义
#+ 通过 Control-V 打出。

# Bram Moolenaar 指出,由于可能的终端交互问题
#+ 以上写法也许不适用于 'vim'。

exit

相较于 vi,上述脚本,本可使用 ex 以同等效率实现。Here Document 包含的一系列 ex 命令足够常用,以至于自成一类,以 ex 脚本 为人熟知。

#!/bin/bash

# 在后缀为 '.txt' 的文件中,将所有
#+ 'Smith' 实例替换为 'Jones'

ORIGINAL=Smith
REPLACEMENT=Jones

for word in $(fgrep -l $ORIGINAL *.txt)
do
    # ---------------------------------
    ex $word <<EOF
    :%s/$ORIGINAL/$REPLACEMENT/g
    :wq
EOF
    # :%s 是 'ex' 替换命令。
    # :wq 的含义是写入并退出。
    # ---------------------------------
done

ex 脚本 类似的是 cat 脚本

例 3. cat 多行消息

#!/bin/bash

# 'echo' 很适合单行消息打印,
#+ 但多行消息有点问题。
#  'cat' here document 克服了改限制。

cat <<End-of-message
---------------------------------
这是消息的第一行。
这是消息的第二行。
这是消息的第三行。
这是消息的第四行。
这是消息的最后一行。
---------------------------------
End-of-message

# 将上述代码第 7 行替换为
#+  cat > $Newfile <<End-of-message
#+      ~~~~~~~~~~
#+ 可将输出写入 $Newfile,而非标准输出。

exit 0

# ------------------------------------
# 因为前面有 'exit 0',以下代码不会执行。

# S.C. 指出,以下实现同样有效
echo "--------------------------------
这是消息的第一行。
这是消息的第二行。
这是消息的第三行。
这是消息的第四行。
这是消息的最后一行。
--------------------------------"
# 但是,文本中的双引号必须转义。

- 选项标记的限制字符串,输出时会忽略行首 tabs(不含空格)。该特性可能有助于生成强可读性脚本。

例 4. 忽略 tab 后的多行消息

#!/bin/bash

# 和前例相同,但 ...

# 为 here document 加上 - 选项,即 <<-
#+ 来忽略文档体中的行首 tabs,但 *不含* 空格。

cat <<-ENDOFMESSAGE
        这是消息的第一行。
        这是消息的第二行。
        这是消息的第三行。
        这是消息的第四行。
        这是消息的最后一行。
ENDOFMESSAGE

# 脚本将以左对齐输出。
# 每行开头的 tab 不会显示。

# 上述 5 行消息以 tab 开始,并非空格。
# 空格不被 <<- 影响。

# 注意改选项也不作用于 *内嵌* tabs。

exit 0

Here Document 支持参数和命令替换。 因此,可通过传入不同参数改变输出。

例 5. 可替换参数 Here Document

#!/bin/bash

# 另一个 cat here document,使用参数替换。

# 以无参形式调用, ./scriptname
# 传入 1 个参数, ./scriptname Mortimer
# 传入 1 个引号内的双词参数, ./scriptname "Mortimer Jones"

CMDLINEPARAM=1 # 默认参数个数

if [ $# -ge $CMDLINEPARAM ]
then
    NAME=$1 # 多于 1 个参数,仅取第一个。
else
    NAME="John Doe" # 默认参数值
fi

RESPONDENT="该脚本作者"

cat <<Endofmessage

你好, $NAME。
很高兴见到你,来自 $RESPONDENT 的问候。

# 该行注释会显示在输出中(为什么?)。

Endofmessage

# 注意空行会出现在输出中。
# 注释也一样。

exit

这是一个非常有用的含参 Here Document 脚本。

例 6. 将文件对上传到网站的传入目录

#!/bin/bash
# upload.sh

# 上传文件对(文件名.lsm, 文件名.tar.gz)
#+ 到 Sunsite/UNC(ibiblio.org) 的传入目录。
# 文件名.tar.gz 为压缩包自身。
# 文件名.lsm 是描述文件。
# Sunsite 要求上传 'lsm' 文件,否则拒绝贡献者。

E_ARGERROR=85

if [ -z "$1" ]
then
    echo "用法:`basename $0` 要上传的文件名"
    exit $E_ARGERROR
fi

Filename=`basename $1` # 从完整文件名中去掉路径。

Server="ibiblio.org"
Directory="/incoming/Linux"
# 上述变量无需写死在脚本中,
#+ 可用使用命令行参数替换。

Password="your.e-mail.address"

ftp -n $Server <<End-Of-Session
# -n 选项禁用自动登录

user anonymous "$Password" # 若改行无法工作,尝试:
                           # `user anonymous "$Password"`
binary
bell                       # 每个文件传输完成后提醒
cd $Directory
put "$Filename.lsm"
put "$Filename.tar.gz"
bye
End-Of-Session

exit 0

Here Document 开头,将 限制字符串 引住或转义,将禁用 参数替换 特性。原因是引用或转义 限制字符串 将有效地使 $,` 和 \特殊字符 被转义,从而使这些字符原样输出。(感谢 Allen Halsey 指出这点。)

例 7. 禁用参数替换

#!/bin/bash
# 禁用参数替换的 cat here-document。

NAME="Jone Doe"
RESPONDENT="该脚本作者"

cat <<Endofmessage

你好, $NAME。
很高兴见到你,来自 $RESPONDENT 的问候。

Endofmessage

# 当限制字符串被引号引住或转义时,不会发生参数替换。
# 下面两个 here document 开头中的任何一个,都会起
#+ 相同效果。
# cat <<"Endofmessage"
# cat <<\Endofmessage


# 并且,像这样:

cat <<"SpecialCharTest"

若限制字符串未被引住,则随后便会列出
本文件夹中的所有文件。
`ls -l`

若限制字符串未被引住,则数学表达式将
产生计算结果。
$((5 + 3))

若限制字符串未被引住,则单反斜杠将会
输出。
\\

SpecialCharTest

exit

禁用参数替换允许将文本原样输出。该特性的用法之一是生成脚本甚至程序代码。

例 8. 在脚本中生成脚本

#!/bin/bash
# generate-script.sh
# 源于 Albert Reiner 的创意。

OUTFILE=generated.sh # 最终生成的脚本名

# ------------------------------------
# 包含被生成脚本体的 Here document
(
cat <<'EOF'
#!/bin/bash

echo "这是一个由程序生成的 shell 脚本。"
# 由于处于子 shell 内部,
#+ 我们无法访问外层脚本变量。

echo "生成的脚本将被命名为:$OUTFILE"
# 上面一行不会如预期般工作,
#+ 因为参数表达式已经被禁用。
# 相反,脚本将原样输出。

a=7
b=3

let "c = $a * $b"
echo "c = $c"

exit 0
EOF
) > $OUTFILE
# ------------------------------------

# 引住限制字符串阻止了上述 here document
#+ 体中的变量扩展。
# 这就允许字符文本原样输出到文件中。

if [ -f "$OUTFILE" ]
then
    chmod 755 $OUTFILE
    # 赋予生成文件执行权限
else
    echo "未能成功创建文件:\"$OUTFILE\""
fi

# 这种方法同样可用来生成
#+ C,Perl,Python,Makefiles
#+ 和其他类似程序

exit 0

可将 Here Document 输出赋值给变量。事实上,这种一种曲折形式的命令替换。

#!/bin/bash

variable=$(cat <<SETVAR
这是一个
跨越多行执行的变量。
SETVAR
)

echo "$variable"

Here Document 可为同一脚本中的函数提供输入。

例 9. Here Document 和函数

# here-function.sh

GetPersonalData ()
{
    read firstname
    read lastname
    read address
    read city
    read state
    read zipcode
} # 这显然是交互式方法,但 ...

# 为上述方法提供输入
GetPersonalData <<RECORD001
Bozo
Bozeman
2726 Nondescript Dr.
Bozeman
MT
21226
RECORD001

echo
echo "$firstname $lastname"
echo "$address"
echo "$city, $state, $zipcode"
echo

exit 0

可使用 : 接受 Here Document 的输出。事实上,它定义了一个匿名 Here Document

例 10. 匿名 Here Document

#!/bin/bash

: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?} # 任一变量未定义都会输出错误信息。
TESTVARIABLES

exit $?

上述技术的变体可用来注释一段代码。

例 11. 注释代码块

#!/bin/bash
# commentblock.sh

: <<COMMENTBLOCK
echo "该行不会输出。"
这是一行缺少 '#' 前缀的注释。
这是另一行缺少 '#' 前缀的注释。

&*@!!++=
上面一行不会产生错误信息,
因为 Bash 解释器会忽略它。
COMMENTBLOCK

echo "上述 \"COMMENTBLOCK\" 的返回值是 $?。" # 0
# 不会有错误显示。
echo


# 上述技术同样可在调试时,为代码块添加注释。
# 这样可省去以前不得不在每行行首添加 '#',
#+ 调试完毕后必须回过头来去掉每一个 '#' 的麻烦。
# 实际上起始的 ':' 是可选的。

echo "开始注释代码块。"
# 位于双虚线间的代码不会执行。
# -----------------------------------------
# -----------------------------------------
: <<DEBUGXXX
for file in *
do
    cat "$file"
done
DEBUGXXX
# -----------------------------------------
# -----------------------------------------
echo "结束注释。"

exit 0

###########################################
# 需要注意的是,如果如果代码块中存在被括号括起
#+ 的变量,将导致问题。
# 比如:

#!/bin/bash

: <<COMMENTBLOCK
echo "该行不会输出。"
&*@!!++=
${foo_bar_bazz?}
$(rm -rf /tmp/foobar/)
$(touch my_build_directory/cups/Makefile)
COMMENTBLOCK

$ sh commented-bad.sh
commented-bad.sh: line 3: foo_bar_bazz: parameter null or not set

# 该问题的补救措施是将上面 49 行的 'COMMENTBLOCK' 加上引号。
: <<'COMMENTBLOCK'

# 此处由 Kurt Pfeifle 指出,非常感谢。

这一漂亮技巧的另一曲折用法是编写自文档化脚本。

例 12. 自文档化脚本

#!/bin/bash
# self-document.sh: 自文档化脚本
# 由 'colm.sh' 修改而来。

DOC_REQUEST=70

if [ "$1" = "-h" -o "$1" = "--help" ] # 请求帮助。
then
    echo; echo "用法:$0 [目录名]"; echo
    sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
    sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi

: <<DOCUMENTATIONXX
使用表格样式输出特定目录的统计信息。
--------------------------------
命令行参数提供被统计的目录名。
若未指定目录,亦或指定目录不可读,
则统计当前工作目录。

DOCUMENTATIONXX

if [ -z "$1" -o ! -r "$1" ]
then
    directory=
else
    directory="$1"
fi

echo "统计目录 "$directory":"; echo
(printf "权限 链接 用户 所在组 大小 月 日 时:分 参数名\n" \
; ls -l "$directory" | sed 1d) | column -t

exit 0

cat 脚本 是实现该功能的替代方案。

DOC_REQUEST=70

if [ "$1" = "-h" -o "$1" = "--help" ] # 请求帮助。
then
    cat <<DOCUMENTATIONXX
使用表格样式输出特定目录的统计信息。
--------------------------------
命令行参数提供被统计的目录名。
若未指定目录,亦或指定目录不可读,
则统计当前工作目录。

DOCUMENTATIONXX
exit $DOC_REQUEST
fi

有关自文档化脚本的更多内容,参见 例 A-28例 A-40例 A-41例 A-42

可通过 Here document 创建临时文件,但这些文件在打开后便被删除,无法被其他任何进程访问。

$ bash -c 'lsof -a -p $$ -d0' <<EOF
heredoc> EOF

一些工具在 Here Document 中无法工作。

位于最后一行,用于关闭 Here Document 的限制字符串,必须从首字符开始。在其之前不允许任何空格。跟随其后的空格同样会导致非预期行为。空格使限制字符串不被识别

#!/bin/bash

echo "-----------------------------------------"

cat <<LimitString
echo "这是 here document 内部的第一行消息。"
echo "这是 here document 内部的第二行消息。"
echo "这是 here document 内部的最后一行消息。"
     LimitString
#~~~~被缩进的限制字符串。错误!该脚本不会按预期执行。

echo "-----------------------------------------"

# 这些注释位于 here document 之外,
#+ 并且不会输出。

echo "here document 之外。"

exit 0

echo "该行本不该输出。" # 因其在 exit 之后。

有些朋友非常聪明地使用 ! 作为限制字符串。但,这样做并不是个好主意。

#!/bin/bash

# 这样可以工作。
cat <<!
你好!
! 三个其他的感叹号 !!!
!

# 但 ...
cat <<!
你好!
单个感叹号!
!
!
# 脚本将崩溃并显示错误信息。


# 当时,以下代码可正常工作。
cat <<EOF
你好!
单个感叹号!
!
EOF
# 使用多字符限制字符串更加安全。

对于那些使用 Here Document 后过于复杂的任务,可考虑使用 expect 脚本,该语言专门设计用来传递输入给可交互程序。

最后编辑于: 2019 年 08 月 02 日