本文译自 TLDP Here Documents
享受当下,朋友们。
—— 《岛》,[英] 阿道司·赫胥黎
简要介绍
Here Document
是一段特定用途代码。它通过 IO 重定向,为可交互程序或命令,如 ftp、cat 及 ex 文本编辑器
等,提供 多行
命令列表。
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
脚本,该语言专门设计用来传递输入给可交互程序。
如有问题请在下方留言,文章转载请注明出处,详细交流请加下方群组!请大佬不要屏蔽文中广告,因为它将帮我分担服务器开支,如果能帮忙点击我将万分感谢。