# 19 嵌入文檔
<blockquote class="blockquote-center">Here and now, boys.
    --Aldous Huxley, Island</blockquote>
嵌入文檔是一段有特殊作用的代碼塊,它用 [I/O 重定向](http://tldp.org/LDP/abs/html/io-redirection.html#IOREDIRREF) 在交互程序和交互命令中傳遞和反饋一個命令列表,例如 [ftp](http://tldp.org/LDP/abs/html/communications.html#FTPREF),[cat](http://tldp.org/LDP/abs/html/basic.html#CATREF) 或者是 ex 文本編輯器
```
COMMAND <<InputComesFromHERE
...
...
...
InputComesFromHERE
```
嵌入文檔用限定符作為命令列表的邊界,在限定符前需要一個指定的標識符 `<<`,這會將一個程序或命令的標準輸入(stdin)進行重定向,它類似 `交互程序 < 命令文件` 的方式,其中命令文件內容如下
```
command #1
command #2
...
```
嵌入文檔的格式大致如下
```
interactive-program <<LimitString
command #1
command #2
...
LimitString
```
限定符的選擇必須保證特殊以確保不會和命令列表里的內容發生混淆。
注意嵌入文檔有時候用作非交互的工具和命令有著非常好的效果,例如 [wall](http://tldp.org/LDP/abs/html/system.html#WALLREF)
樣例 19-1. broadcast: 給每個登陸者發送信息
```
#!/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 < 信息文本
# 然而, 在腳本里嵌入信息模板不乏是一種迅速而又隨性的解決方式.
exit
```
樣例: 19-2. dummyfile:創建一個有兩行內容的虛擬文件
```
#!/bin/bash
# 非交互的使用 `vi` 編輯文件.
# 仿照 'sed'.
E_BADARGS=85
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-----------#
# 注意 "^" 對 "[" 進行了轉義
#+ 這段起到了和鍵盤上按下 Control-V <Esc> 相同的效果.
# Bram Moolenaar 指出這種情況下 'vim' 可能無法正常工作
#+ 因為在與終端交互的過程中可能會出現問題.
exit
```
上述腳本實現了 `ex` 的功能, 而不是 `vi`. 嵌入文檔包含了 `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 is the "ex" substitution command.
# :wq is write-and-quit.
# -------------------------------------
done
```
類似的 `ex 腳本` 是 `cat 腳本`.
樣例 19-3. 使用 `cat` 的多行信息
```
#!/bin/bash
# 'echo' 可以輸出單行信息,
#+ 但是如果是輸出消息塊就有點問題了.
# 'cat' 嵌入文檔卻能解決這個局限.
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, 而不是標準輸出.
exit 0
#--------------------------------------------
# 由于上面的 "exit 0",下面的代碼將不會生效.
# S.C. points out that the following also works.
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.
-------------------------------------"
# 然而, 文本可能不包括雙引號除非出現了字符串逃逸.
```
`-` 的作用是標記了一個嵌入文檔限制符 (<<-LimitString) ,它能抑制輸出的行首的 `tab` (非空格). 這在腳本可讀性方面可能非常有用.
樣例 19-4. 抑制 tab 的多行信息
```
#!/bin/bash
# 和之前的樣例一樣, 但...
# 嵌入文檔內的 '-' ,也就是 <<-
#+ 抑制了文檔行首的 'tab',
#+ 但 *不是* 空格.
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
```
嵌入文檔支持參數和命令替換. 因此可以向嵌入文檔傳遞不同的參數,變向的改其輸出.
樣例 19-5. 可替換參數的嵌入文檔
```
#!/bin/bash
# 另一個使用參數替換的 'cat' 嵌入文檔.
# 試一試沒有命令行參數, ./scriptname
# 試一試一個命令行參數, ./scriptname Mortimer
# 試試用一兩個單詞引用命令行參數,
# ./scriptname "Mortimer Jones"
CMDLINEPARAM=1 # Expect at least command-line parameter.
if [ $# -ge $CMDLINEPARAM ]
then
NAME=$1 # If more than one command-line param,
#+ then just take the first.
else
NAME="John Doe" # Default, if no command-line parameter.
fi
RESPONDENT="the author of this fine script"
cat <<Endofmessage
Hello, there, $NAME.
Greetings to you, $NAME, from $RESPONDENT.
# 這個注釋在輸出時顯示 (為什么?).
Endofmessage
# 注意輸出了空行.
# 所以可以這樣注釋.
exit
```
這個包含參數替換的嵌入文檔是相當有用的
樣例 19-6. 上傳文件對到 `Sunsite` 入口目錄
```
#!/bin/bash
# upload.sh
# 上傳文件對 (Filename.lsm, Filename.tar.gz)
#+ 到 Sunsite/UNC (ibiblio.org) 的入口目錄.
# Filename.tar.gz 是個 tarball.
# Filename.lsm is 是個描述文件.
# Sunsite 需要 "lsm" 文件, 否則將會退回給發送者
E_ARGERROR=85
if [ -z "$1" ]
then
echo "Usage: `basename $0` Filename-to-upload"
exit $E_ARGERROR
fi
Filename=`basename $1` # Strips pathname out of file name.
Server="ibiblio.org"
Directory="/incoming/Linux"
# 腳本里不需要硬編碼,
#+ 但最好可以替換命令行參數.
Password="your.e-mail.address" # Change above to suit.
ftp -n $Server <<End-Of-Session
# -n 禁用自動登錄
user anonymous "$Password" # If this doesn't work, then try:
# quote user anonymous "$Password"
binary
bell # Ring 'bell' after each file transfer.
cd $Directory
put "$Filename.lsm"
put "$Filename.tar.gz"
bye
End-Of-Session
exit 0
```
在嵌入文檔頭部引用或轉義"限制符"來禁用參數替換.原因是 `引用/轉義` 限定符能有效的[轉義](http://tldp.org/LDP/abs/html/escapingsection.html#ESCP) "$", "`", 和 "\" 這些[特殊符號](http://tldp.org/LDP/abs/html/special-chars.html#SCHARLIST), 使他們維持字面上的意思. (感謝 Allen Halsey 指出這點.)
樣例 19-7. 禁用參數替換
```
#!/bin/bash
# A 'cat' here-document, but with parameter substitution disabled.
NAME="John Doe"
RESPONDENT="the author of this fine script"
cat <<'Endofmessage'
Hello, there, $NAME.
Greetings to you, $NAME, from $RESPONDENT.
Endofmessage
# 當'限制符'引用或轉義時不會有參數替換.
# 下面的嵌入文檔也有同樣的效果
# cat <<"Endofmessage"
# cat <<\Endofmessage
# 同樣的:
cat <<"SpecialCharTest"
Directory listing would follow
if limit string were not quoted.
`ls -l`
Arithmetic expansion would take place
if limit string were not quoted.
$((5 + 3))
A a single backslash would echo
if limit string were not quoted.
\\
SpecialCharTest
exit
```
生成腳本或者程序代碼時可以用禁用參數的方式來輸出文本.
樣例 19-8. 生成其他腳本的腳本
```
#!/bin/bash
# generate-script.sh
# Based on an idea by Albert Reiner.
OUTFILE=generated.sh # Name of the file to generate.
# -----------------------------------------------------------
# '嵌入文檔涵蓋了生成腳本的主體部分.
(
cat <<'EOF'
#!/bin/bash
echo "This is a generated shell script."
# 注意我們現在在一個子 shell 內,
#+ 我們不能訪問 "外部" 腳本變量.
echo "Generated file will be named: $OUTFILE"
# 上面這行并不能按照預期的正常工作
#+ 因為參數擴展已被禁用.
# 相反的, 結果是文字輸出.
a=7
b=3
let "c = $a * $b"
echo "c = $c"
exit 0
EOF
) > $OUTFILE
# -----------------------------------------------------------
# 在上述的嵌入文檔內引用'限制符'防止變量擴展
if [ -f "$OUTFILE" ]
then
chmod 755 $OUTFILE
# 生成可執行文件.
else
echo "Problem in creating file: \"$OUTFILE\""
fi
# 這個方法適用于生成 C, Perl, Python, Makefiles 等等
exit 0
```
可以從嵌入文檔的輸出設置一個變量的值. 這實際上是種靈活的 [命令替換](http://tldp.org/LDP/abs/html/commandsub.html#COMMANDSUBREF).
```
variable=$(cat <<SETVAR
This variable
runs over multiple lines.
SETVAR
)
echo "$variable"
```
同樣的腳本里嵌入文檔可以作為函數的輸入.
樣例 19-9. 嵌入文檔和函數
```
#!/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.
Bozeman
MT
21226
RECORD001
echo
echo "$firstname $lastname"
echo "$address"
echo "$city, $state $zipcode"
echo
exit 0
```
可以這樣使用: 作為一個虛構的命令接受嵌入文檔的輸出. 這樣實際上就創建了一個 "匿名" 嵌入文檔.
樣例 19-10. "匿名" 嵌入文檔
```
#!/bin/bash
: <<TESTVARIABLES
${HOSTNAME?}${USER?}${MAIL?} # Print error message if one of the variables not set.
TESTVARIABLES
exit $?
```
- 上面技巧的一種變體允許 "可添加注釋" 的代碼塊.
樣例 19-11. 可添加注釋的代碼塊
```
#!/bin/bash
# commentblock.sh
: <<COMMENTBLOCK
echo "This line will not echo."
這些注釋沒有 "#" 前綴.
則是另一種沒有 "#" 前綴的注釋方法.
&*@!!++=
上面這行不會產生報錯信息,
因為 bash 解釋器會忽略它.
COMMENTBLOCK
echo "Exit value of above \"COMMENTBLOCK\" is $?." # 0
# 沒有錯誤輸出.
echo
# 上面的技巧經常用于工作代碼的注釋用作排錯目的
# 這省去了在每一行開頭加上 "#" 前綴,
#+ 然后調試完不得不刪除每行的前綴的重復工作.
# 注意我們用了 ":", 在這之上,是可選的.
echo "Just before commented-out code block."
# 下面這個在雙破折號之間的代碼不會被執行.
# ===================================================================
: <<DEBUGXXX
for file in *
do
cat "$file"
done
DEBUGXXX
# ===================================================================
echo "Just after commented-out code block."
exit 0
######################################################################
# 注意, 然而, 如果將變量中包含一個注釋的代碼塊將會引發問題
# 例如:
#/!/bin/bash
: <<COMMENTBLOCK
echo "This line will not 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 指出這一點.
```
- 另一個漂亮的方法使得"自文檔化"的腳本成為可能
樣例 19-12. 自文檔化的腳本
```
#!/bin/bash
# self-document.sh: self-documenting script
# 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 script](http://tldp.org/LDP/abs/html/here-docs.html#CATSCRIPTREF) 是另一種可行的方法.
```
DOC_REQUEST=70
if [ "$1" = "-h" -o "$1" = "--help" ] # Request help.
then # Use a "cat script" . . .
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
```
> 另請參閱 [樣例 A-28](http://tldp.org/LDP/abs/html/contributed-scripts.html#ISSPAMMER2), [樣例 A-40](http://tldp.org/LDP/abs/html/contributed-scripts.html#PETALS), [樣例 A-41](http://tldp.org/LDP/abs/html/contributed-scripts.html#QKY), and [樣例 A-42](http://tldp.org/LDP/abs/html/contributed-scripts.html#NIM) 更多樣例請閱讀腳本附帶的注釋文檔.
- 嵌入文檔創建了臨時文件, 但這些文件在打開且不可被其他程序訪問后刪除.
```
bash$ bash -c 'lsof -a -p $$ -d0' << EOF
> EOF
lsof 1213 bozo 0r REG 3,5 0 30386 /tmp/t1213-0-sh (deleted)
```
- 某些工具在嵌入文檔內部并不能正常運行.
- 在嵌入文檔的最后關閉限定符必須在起始的第一個字符的位置開始.行首不能是空格. 限制符后尾隨空格同樣會導致意想不到的行為.空格可以防止限制符被當做其他用途. [[1]](http://tldp.org/LDP/abs/html/here-docs.html#FTN.AEN17822)
```
#!/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
#^^^^限制符的縮進. 出錯! 這個腳本將不會如期運行.
echo "----------------------------------------------------------------------"
# 這些評論在嵌入文檔范圍外并不能輸出
echo "Outside the here document."
exit 0
echo "This line had better not echo." # 緊跟著個 'exit' 命令.
```
- 有些人非常聰明的使用了一個單引號(!)做為限制符. 但這并不是個好主意
```
# 這個可以運行.
cat <<!
Hello!
! Three more exclamations !!!
!
# 但是 . . .
cat <<!
Hello!
Single exclamation point follows!
!
!
# Crashes with an error message.
# 然而, 下面這樣也能運行.
cat <<EOF
Hello!
Single exclamation point follows!
!
EOF
# 使用多字符限制符更為安全.
```
為嵌入文檔設置這些任務有些復雜, 可以考慮使用 `expect`, 一種專門用來和程序進行交互的腳本語言。
**Notes:**
  除此之外, Dennis Benzinger 指出, [使用 <<- 抑制 tab.](http://tldp.org/LDP/abs/html/here-docs.html#LIMITSTRDASH)
- 第一部分 初見shell
- 1. 為什么使用shell編程
- 2. 和Sha-Bang(#!)一起出發
- 2.1 調用一個腳本
- 2.2 牛刀小試
- 第二部分 shell基礎
- 3. 特殊字符
- 4. 變量與參數
- 4.1 變量替換
- 4.2 變量賦值
- 4.3 Bash弱類型變量
- 4.4 特殊變量類型
- 5. 引用
- 5.1 引用變量
- 5.2 轉義
- 6. 退出與退出狀態
- 7. 測試
- 7.1 測試結構
- 7.2 文件測試操作
- 7.3 其他比較操作
- 7.4 嵌套 if/then 條件測試
- 7.5 牛刀小試
- 8. 運算符相關話題
- 8.1 運算符
- 8.2 數字常量
- 8.3 雙圓括號結構
- 8.4 運算符優先級
- 第三部分 shell進階
- 10. 變量處理
- 10.1 字符串處理
- 10.1.1 使用 awk 處理字符串
- 10.1.2 參考資料
- 10.2 參數替換
- 11. 循環與分支
- 11.1 循環
- 11.2 嵌套循環
- 11.3 循環控制
- 11.4 測試與分支
- 12. 命令替換
- 13. 算術擴展
- 14. 休息時間
- 第五部分 進階話題
- 19. 嵌入文檔
- 20. I/O 重定向
- 20.1 使用 exec
- 20.2 重定向代碼塊
- 20.3 應用程序
- 22. 限制模式的Shell
- 23. 進程替換
- 26. 列表結構
- 25. 別名