shell教程
OutOfMemory.CN技术专栏-> shell-> shell教程-> Chapter 17. Here Documents

Chapter 17. Here Documents

Hereandnow,boys.AldousHuxley,"Island"heredocument就是一段特殊目的的代码块.他使用I/O重定向的形式来将一个命令序列传递到一个交互程序或者命令中,比如ftp,cat,或者ex文本编辑器.COMMAND<<InputComesFromHERE...InputComesFromHERElimitstring用来划定命令序列的范围(译者注:
 

Here and now, boys.

 Aldous Huxley, "Island"

here document 就是一段特殊目的的代码块. 他使用I/O 重定向的形式来将一个命令序列传递到一个交互程序或者命令中, 比如ftp, cat, 或者ex文本编辑器.

COMMAND <<InputComesFromHERE
...
InputComesFromHERE

limit string 用来划定命令序列的范围(译者注: 两个相同的limit string之间就是命令序列). 特殊符号 << 用来表识limit string. 这个符号具有重定向文件的输出到程序或命令的输入的作用. 与 interactive-program < command-file 很相象, command-file包含:

command #1
command #2
...

here document 的形式看上去是如下的样子:

#!/bin/bash
interactive-program <<LimitString
command #1
command #2
...
LimitString

选择一个名字非常诡异的limit string将会避免命令列表和limit string重名的问题.

注意,某些时候here document 用在非交互工具和命令上的时候也会有好的效果, 比如, wall.


Example 17-1. 广播: 发送消息给每个登录上的用户

#!/bin/bash

wall <<zzz23EndOfMessagezzz23
E-mail your noontime orders for pizza to the system administrator.
    (Add an extra dollar for anchovy or mushroom topping.)
# 额外的消息文本写在这里.
# 注意: 'wall' 会打印注释行.
zzz23EndOfMessagezzz23

# 可以使用更有效率的做法
#         wall <message-file
#  然而将消息模版嵌入到脚本中
#+ 是一种"小吃店"(快速但是比较脏)的只能使用一次的解决办法.

exit 0

即使是某些不大可能的工具, 如vi也可以使用here document.


Example 17-2. 仿造文件: 创建一个两行的仿造文件

#!/bin/bash

# 用非交互的方式来使用'vi'编辑一个文件.
# 模仿'sed'.

E_BADARGS=65

if [ -z "$1" ]
then
  echo "Usage: `basename $0` filename"
  exit $E_BADARGS
fi

TARGETFILE=$1

# 在文件中插入两行, 然后保存.
#--------Begin here document-----------#
vi $TARGETFILE <<x23LimitStringx23
i
This is line 1 of the example file.
This is line 2 of the example file.
^[
ZZ
x23LimitStringx23
#----------End here document-----------#

#  注意上边^[是一个转义符,键入Ctrl+v <Esc>就行,
#+ 事实上它是<Esc>键.

#  Bram Moolenaar指出这种方法不能正常地用在'vim'上, (译者注: Bram Moolenaar是vim作者)
#+ 因为可能会有终端的相互影响问题.

exit 0

上边的脚本也可以不用vi而用ex来实现. Here document 包含ex命令列表的做法足够形成自己的类别了, 叫ex scripts.

#!/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 scripts"相似的是cat scripts.


Example 17-3. 使用cat的多行消息

#!/bin/bash

#  'echo' 对于打印单行消息是非常好的,
#+  但是在打印消息块时可能就有点问题了.
#   'cat' here document可以解决这个限制.

cat <<End-of-message
-------------------------------------
This is line 1 of the message.
This is line 2 of the message.
This is line 3 of the message.
This is line 4 of the message.
This is the last line of the message.
-------------------------------------
End-of-message

#  用下边这行代替上边的第7行
#+   cat > $Newfile <<End-of-message
#+       ^^^^^^^^^^
#+ 那么就会把输出写到文件$Newfile中, 而不是stdout.

exit 0


#--------------------------------------------
# 下边的代码不会运行, 因为上边的"exit 0".

# S.C. 指出下边代码也可以运行.
echo "-------------------------------------
This is line 1 of the message.
This is line 2 of the message.
This is line 3 of the message.
This is line 4 of the message.
This is the last line of the message.
-------------------------------------"
# 然而, 文本可能不包含双引号, 除非它们被转义.

- 选项用来标记here document的limit string (<<-LimitString), 可以抑制输出时前边的tab(不是空格). 这可以增加一个脚本的可读性.


Example 17-4. 带有抑制tab功能的多行消息

#!/bin/bash
# 与之前的例子相同, 但是...

#  - 选项对于here docutment来说,<<-
#+ 可以抑制文档体前边的tab,
#+ 而*不*是空格 *not* spaces.

cat <<-ENDOFMESSAGE
	This is line 1 of the message.
	This is line 2 of the message.
	This is line 3 of the message.
	This is line 4 of the message.
	This is the last line of the message.
ENDOFMESSAGE
# 脚本在输出的时候左边将被刷掉.
# 就是说每行前边的tab将不会显示.

# 上边5行"消息"的前边都是tab, 不是空格.
# 空格是不受<<-影响的.

# 注意, 这个选项对于*嵌在*中间的tab没作用.

exit 0

here document 支持参数和命令替换. 所以也可以给here document的消息体传递不同的参数, 这样相应的也会修改输出.


Example 17-5. 使用参数替换的here document

#!/bin/bash
# 一个使用'cat'命令的here document, 使用了参数替换

# 不传命令行参数给它,   ./scriptname
# 传一个命令行参数给它,   ./scriptname Mortimer
# 传一个2个单词(用引号括起来)的命令行参数给它,
#                           ./scriptname "Mortimer Jones"

CMDLINEPARAM=1     #  所期望的最少的命令行参数的个数.

if [ $# -ge $CMDLINEPARAM ]
then
  NAME=$1          #  如果命令行参数超过1个,
                   #+ 那么就只取第一个参数.
else
  NAME="John Doe"  #  默认情况下, 如果没有命令行参数的话.
fi

RESPONDENT="the author of this fine script"


cat <<Endofmessage

Hello, there, $NAME.
Greetings to you, $NAME, from $RESPONDENT.

# This comment shows up in the output (why?).

Endofmessage

# 注意上边的空行也打印到输出,
# 而上边那行"注释"当然也会打印到输出.
# (译者注: 这就是为什么不翻译那行注释的原因, 尽量保持原代码的原样)
exit 0

这是一个包含参数替换的here document的有用的脚本.


Example 17-6. 上传一个文件对到"Sunsite"的incoming目录

#!/bin/bash
# upload.sh

#  上传文件对(Filename.lsm, Filename.tar.gz)
#+ 到Sunsite/UNC (ibiblio.org)的incoming目录.
#  Filename.tar.gz是自身的tar包.
#  Filename.lsm是描述文件.
#  Sunsite需要"lsm"文件, 否则就拒绝贡献.


E_ARGERROR=65

if [ -z "$1" ]
then
  echo "Usage: `basename $0` Filename-to-upload"
  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"
binary
bell                             # 在每个文件传输后, 响铃.
cd $Directory
put "$Filename.lsm"
put "$Filename.tar.gz"
bye
End-Of-Session

exit 0

在here document的开头引用或转义"limit string"会使得here document的消息体中的参数替换被禁用.


Example 17-7. 关闭参数替换

#!/bin/bash
#  一个使用'cat'的here document, 但是禁用了参数替换.

NAME="John Doe"
RESPONDENT="the author of this fine script"

cat <<'Endofmessage'

Hello, there, $NAME.
Greetings to you, $NAME, from $RESPONDENT.

Endofmessage

#  当"limit string"被引用或转义那么就禁用了参数替换.
#  下边的两种方式具有相同的效果.
#  cat <<"Endofmessage"
#  cat <<\Endofmessage

exit 0

禁用了参数替换后, 将允许输出文本本身(译者注: 就是未转义的原文). 产生脚本甚至是程序代码就是这种用法的用途之一.


Example 17-8. 一个产生另外一个脚本的脚本

#!/bin/bash
# generate-script.sh
# 基于Albert Reiner的一个主意.

OUTFILE=generated.sh         # 所产生文件的名字.


# -----------------------------------------------------------
# 'Here document包含了需要产生的脚本的代码.
(
cat <<'EOF'
#!/bin/bash

echo "This is a generated shell script."
#  Note that since we are inside a subshell,
#+ we can't access variables in the "outside" script.

echo "Generated file will be named: $OUTFILE"
#  Above line will not work as normally expected
#+ because parameter expansion has been disabled.
#  Instead, the result is literal output.

a=7
b=3

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

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

#  将'limit string'引用起来将会阻止上边
#+ here document的消息体中的变量扩展.
#  这会使得输出文件中的内容保持here document消息体中的原文.

if [ -f "$OUTFILE" ]
then
  chmod 755 $OUTFILE
  # 让所产生的文件具有可执行权限.
else
  echo "Problem in creating file: \"$OUTFILE\""
fi

#  这个方法也用来产生
#+ C程序代码, Perl程序代码, Python程序代码, makefile,
#+ 和其他的一些类似的代码.
#  (译者注: 中间一段没译的注释将会被here document打印出来)
exit 0
 

也可以将here document的输出保存到变量中.

variable=$(cat <<SETVAR
This variable
runs over multiple lines.
SETVAR)

echo "$variable"

同一脚本中的函数也可以接受here document的输出作为自身的参数.


Example 17-9. Here documents与函数

#!/bin/bash
# here-function.sh

GetPersonalData ()
{
  read firstname
  read lastname
  read address
  read city
  read state
  read zipcode
} # 这个函数无疑的看起来就一个交互函数, 但是...


# 给上边的函数提供输入.
GetPersonalData <<RECORD001
Bozo
Bozeman
2726 Nondescript Dr.
Baltimore
MD
21226
RECORD001


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

exit 0

也可以这么使用: 做一个假命令来从一个here document中接收输出. 这么做事实上就是创建了一个"匿名"的here document.


Example 17-10. "匿名" here Document

#!/bin/bash

: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?}  # 如果其中一个变量没被设置, 那么就打印错误信息.
TESTVARIABLES

exit 0

上边所示技术的一种变化可以用来"注释"掉代码块.


Example 17-11. 注释掉一段代码块

#!/bin/bash
# commentblock.sh

: <<COMMENTBLOCK
echo "This line will not echo."
This is a comment line missing the "#" prefix.
This is another comment line missing the "#" prefix.

&*@!!++=
The above line will cause no error message,
because the Bash interpreter will ignore it.
COMMENTBLOCK

echo "Exit value of above \"COMMENTBLOCK\" is $?."   # 0
# 这里将不会显示任何错误.


#  上边的这种技术当然也可以用来注释掉
#+ 一段正在使用的代码, 如果你有某些特定调试要求的话.
#  这将比对每行都敲入"#"来得方便的多,
#+ 而且如果你想恢复的话, 还得将添加上的"#"删除掉.

: <<DEBUGXXX
for file in *
do
 cat "$file"
done
DEBUGXXX

exit 0

关于这种小技巧的另一个应用就是能够产生自文档化(self-documenting)的脚本.


Example 17-12. 一个自文档化(self-documenting)的脚本

#!/bin/bash
# self-document.sh: 自文档化(self-documenting)的脚本
# Modification of "colm.sh".

DOC_REQUEST=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # 请求帮助.
then
  echo; echo "Usage: $0 [directory-name]"; echo
  sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
  sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi


: <<DOCUMENTATIONXX
List the statistics of a specified directory in tabular format.
---------------------------------------------------------------
The command line parameter gives the directory to be listed.
If no directory specified or directory specified cannot be read,
then list the current working directory.

DOCUMENTATIONXX

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

echo "Listing of "$directory":"; echo
(printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
; ls -l "$directory" | sed 1d) | column -t

exit 0

使用cat 脚本 也能够完成相同的目的.

DOC_REQUEST=70

if [ "$1" = "-h"  -o "$1" = "--help" ]     # 请求帮助.
then                                       # 使用"cat 脚本" . . .
  cat <<DOCUMENTATIONXX
List the statistics of a specified directory in tabular format.
---------------------------------------------------------------
The command line parameter gives the directory to be listed.
If no directory specified or directory specified cannot be read,
then list the current working directory.

DOCUMENTATIONXX
exit $DOC_REQUEST
fi

参见 Example A-27 可以了解更多关于自文档化脚本的好例子.

Here document创建临时文件, 但是这些文件将在打开后被删除, 并且不能够被任何其他进程所存取.

 bash$ bash -c 'lsof -a -p $$ -d0' << EOF
 > EOF
 lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
 	      

某些工具是不能工作在here document中的.

结束的limit string, 就是here document最后一行的limit string, 必须开始于第一个字符位置. 它的前面不能够有任何前置的空白. 而在这个limit string后边的空白也会引起异常问题. 空白将会阻止limit string的识别.(译者注: 下边这个脚本由于结束limit string的问题, 造成脚本无法结束, 所有内容全部被打印出来, 所以注释就不译了, 保持例子脚本的原样.)

#!/bin/bash

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

cat <<LimitString
echo "This is line 1 of the message inside the here document."
echo "This is line 2 of the message inside the here document."
echo "This is the final line of the message inside the here document."
     LimitString
#^^^^Indented limit string. Error! This script will not behave as expected.

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

#  These comments are outside the 'here document',
#+ and should not echo.

echo "Outside the here document."

exit 0

echo "This line had better not echo."  # Follows an 'exit' command.

对于那些使用"here document"得非常复杂的任务, 最好考虑使用expect脚本语言, 这种语言就是为了达到向交互程序添加输入的目的而量身定做的.

here string 可以被认为是here document的一种定制形式. 除了COMMAND <<<$WORD 就什么都没有了, $WORD将被扩展并且被送入COMMAND的stdin中.


Example 17-13. 在一个文件的开头添加文本

#!/bin/bash
# prepend.sh: 在文件的开头添加文本.
#
#  Kenny Stauffer所捐助的脚本例子,
#+ 被本文作者作了少量的修改.


E_NOSUCHFILE=65

read -p "File: " file   # 'read'命令的 -p 参数显示提示符.
if [ ! -e "$file" ]
then   # 如果没有这个文件那就进来.
  echo "File $file not found."
  exit $E_NOSUCHFILE
fi

read -p "Title: " title
cat - $file <<<$title > $file.new

echo "Modified file is $file.new"

exit 0

# 下边是'man bash'中的一段:
# Here Strings
# 	here document的一种变形,形式如下:
#
# 		<<<word
#
# 	 word被扩展并且提供到command的标准输入中.

练习: 找出here string的其他用法.

© 内存溢出 OutOfMemory.CN