基礎
HereDoc 全名叫做 Here Document
,中文可以稱之為 嵌入文件
。對它的叫法實際上很多,here文件,hereis,here-string 等等都是它。
嵌入文件是 Shell I/O 重定向功能的一種替代。我們已經知道 Shell 的 I/O 重定向是一種檔案控制程式碼的傳輸。例如:
COMMAND 1>/tmp/1.lst 2>&1
複製程式碼
將命令的標準輸出為一個檔案,同時也將錯誤輸出到同一個檔案中。
而下例:
cat /etc/passwd | grep '^admin:'
複製程式碼
則通過管道將前一命令的輸出當做後一命令的標準輸入。
基本語法
Here Document 就是標準輸入的一種替代品。它使得指令碼開發人員可以不必使用臨時檔案來構建輸入資訊,而是直接就地生產出一個檔案並用作命令的標準輸入。一般來說其格式是這樣的:
COMMAND <<IDENT
...
IDENT
複製程式碼
在這裡,<<
是引導標記,IDENT
是一個限定符,由開發人員自行選定,兩個 IDENT
限定符之間的內容將被當做是一個檔案並用作 COMMAND 的標準輸入。例如echo大段文字時,我們可以使用 cat file
的語法:
cat <<EOF
SOME TEXT
HERE
!
EOF
複製程式碼
此例中,我們使用 EOF
短語作為限定符。
Here Document 是可以巢狀的,只要雙層分別使用不同的
IDENT
限定符且保證正確的巢狀關係即可:
ssh user@host <<EOT ls -la --color cat <<EOF from a remote host EOF [ -f /tmp/1.tmp ] && rm -f /tmp/1.tmp EOT 複製程式碼
看起來有點怪?其實還好啦。
實際上,限定符可以取得非常長,只要是字母開頭且只包含字母和數字(通常,下劃線和短橫線也是有效的,不過根據 bash 的版本不同、宿主實現的不同,可能會有一定的出入)即可。
abs
中有一個例子,節選如下:
wall <<zzz23EndOfMessagezzz23
fdjsldj
fdsjlfdsjfdls
zzz23EndOfMessagezzz23
複製程式碼
這是正確有效的,不過這個其實更怪一些。
Here String
在 bash, ksh 和 zsh 中,還可以使用 Here String:
$ tr a-z A-Z <<<"Yes it is a string"
YES IT IS A STRING
複製程式碼
此時也可以使用變數:
$ tr a-z A-Z <<<"$var"
複製程式碼
不常見的用法
同時重定向標準輸出
有沒有可能將HEREDOC儲存為一個檔案?顯然是可以:
cat << EOF > /tmp/yourfilehere
These contents will be written to the file.
This line is indented.
EOF
複製程式碼
你可以注意到這種寫法不同於經常性的寫法:
cat >/tmp/1<<EOF
s
EOF
複製程式碼
但兩者都是對的。
root
但當需要 root 許可權時,'>' 並不能很好地工作,此時需要 sudo tee
上場:
cat <<EOF | sudo tee /opt/1.log
s
EOF
複製程式碼
子shell
標準輸出的重定向,還可以通過子 shell 的方式來構造:
(echo '# BEGIN OF FILE | FROM'
cat <<- _EOF_
LogFile /var/log/clamd.log
LogTime yes
DatabaseDirectory /var/lib/clamav
LocalSocket /tmp/clamd.socket
TCPAddr 127.0.0.1
SelfCheck 1020
ScanPDF yes
_EOF_
echo '# END OF FILE'
) > /etc/clamd.conf
複製程式碼
這個例子只是一個示意,因為實際上該例子用不著那麼麻煩,單個 cat HEREDOC
足夠達到目的了,也不需要開子 shell 那麼重。
cat <<EOF
的少見的變形
let() {
res=$(cat)
}
let <<'EOF'
...
EOF
複製程式碼
元芳,你怎麼看?
還可以寫作這樣:
let() {
eval "$1"'=$(cat)'
}
let res<<'EOF'
...
EOF
複製程式碼
當然,其實它和單行指令是等效的:
{ res=$(cat); } <<'EOF'
...
EOF
複製程式碼
{}
是語句塊,而不是子shell,因而更省力。根據具體情況來使用它,有時候你希望子 shell 的變數無汙染的效果,或者別的期待,那你就使用 ()
。
在引數展開語法中使用 HEREDOC
variable=$(cat <<SETVAR
This variable
runs over multiple lines.
SETVAR
)
echo "$variable"
複製程式碼
示例展示了在 $()
語法中可以隨意地嵌入 HEREDOC。
如果你只是需要為變數用 HEREDOC 賦值,read var
通常是更好的主意:
read i <<!
Hi
!
echo $i # Hi
複製程式碼
對函式使用 HEREDOC
GetPersonalData () {
read firstname
read lastname
read address
read city
read state
read zipcode
} # This certainly appears to be an interactive function, but . . .
# Supply input to the above function.
GetPersonalData <<RECORD001
Bozo
Bozeman
2726 Nondescript Dr.
Bozeman
MT
21226
RECORD001
echo
echo "$firstname $lastname"
echo "$address"
echo "$city, $state $zipcode"
echo
複製程式碼
可以看到,只要函式能夠接收標準輸入,那就可以將 HEREDOC 套用上去。
匿名的 HEREDOC
#!/bin/bash
# filename: aa.sh
: <<TESTVARIABLES
${UX?}, ${HOSTNAME?} | ${USER?} | ${MAIL?} # Print error message if one of the variables not set.
TESTVARIABLES
exit $?
複製程式碼
這個示例中,如果變數沒有被設定,則會產生一條錯誤訊息,而該 HEREDOC 的用處實際上是用來展開要確認的變數,HEREDOC產生的結果作為 :
的標準輸入,實際上被忽略了,最後只有 HEREDOC 展開的狀態碼被返回,用以確認是不是有某個變數尚未被設定:
$ ./aa; echo $?
./aa: line 3: UX: parameter null or not set
1
複製程式碼
由於 UX 變數缺失,因此呼叫的結果是一行錯誤輸出,以及呼叫的退出碼為 1,也就是 false 的意思。
:
是true
命令的同義詞。就好像.
是source
命令的同義詞一樣。
進一步
除了用來一次性檢測一大批變數有否被賦值的效果之外,匿名的 HEREDOC 也常常被用作大段的註釋。
cat >/dev/null<<COMMENT
...
COMMENT
: <<COMMENT
...
COMMENT
複製程式碼
這些寫法都可以,看你的個人喜好。Bash 程式設計師的一般風格是能省鍵盤就省鍵盤。但有時候他們也喜歡能炫就炫:
:<<-!
____ _ ____ _
/ ___| ___ ___ __| | / ___| ___ ___ __| |
| | _ / _ \ / _ \ / _` | | | _ / _ \ / _ \ / _` |
| |_| | (_) | (_) | (_| | | |_| | (_) | (_) | (_| |
\____|\___/ \___/ \__,_| \____|\___/ \___/ \__,_|
____ _ _
/ ___|| |_ _ _ __| |_ _
\___ \| __| | | |/ _` | | | |
___) | |_| |_| | (_| | |_| |
|____/ \__|\__,_|\__,_|\__, |
|___/
!
複製程式碼
while read
當我們需要讀一個csv檔案時,我們會用到 while read 結構。
將 csv 檔案改為 HEREDOC:
while read pass port user ip files directs; do
sshpass -p$pass scp -o 'StrictHostKeyChecking no' -P $port $files $user@$ip:$directs
done <<____HERE
PASS PORT USER IP FILES DIRECTS
. . . . . .
. . . . . .
. . . . . .
PASS PORT USER IP FILES DIRECTS
____HERE
複製程式碼
由於不同格式的 CSV 的處理並非本文的主題,因此這裡不再展開討論具體情況了。
補充:迴圈的重定向
對於 while … done
來說,標準輸入的重定向應該寫在 done
之後。同樣的,for … do … done
也是如此,until … done
也是如此。
while
while [ "$name" != Smith ] # Why is variable $name in quotes?
do
read name # Reads from $Filename, rather than stdin.
echo $name
let "count += 1"
done <"$Filename" # Redirects stdin to file $Filename.
複製程式碼
until
until [ "$name" = Smith ] # Change != to =.
do
read name # Reads from $Filename, rather than stdin.
echo $name
done <"$Filename" # Redirects stdin to file $Filename.
複製程式碼
for
for name in `seq $line_count` # Recall that "seq" prints sequence of numbers.
# while [ "$name" != Smith ] -- more complicated than a "while" loop --
do
read name # Reads from $Filename, rather than stdin.
echo $name
if [ "$name" = Smith ] # Need all this extra baggage here.
then
break
fi
done <"$Filename" # Redirects stdin to file $Filename.
複製程式碼
新的縮排和對齊語法
刪除 TAB 縮排字元
<<-IDENT
是新的語法,市面上的 Bash 均已支援這種寫法。它的特殊之處就在於 HEREDOC 正文內容中的所有字首 TAB 字元都會被刪除。
這種語法往往被用在指令碼的 if 分支,case 分支或者其他的程式碼有縮排的場所,這樣 HEREDOC 的結束標記不必非要在新的一行的開始之處不可。一方面視覺效果上 HEREDOC 跟隨了所在程式碼塊的縮排層次,可讀性被提升,另一方面對於許多懶惰的編輯器來說,不會發生面對 HEREDOC 時語法分析出錯、程式碼摺疊的區塊判斷不正確的情況。
function a () {
if ((DEBUG)); then
cat <<-EOF
French
American
- Uses UTF-8
Helvetica
- Uses RTL
EOF
fi
}
複製程式碼
如上的指令碼段落中,結束標記EOF可以不必處於行首第一個字母,只要EOF以及其上的HEREDOC正文都以TAB字元進行縮排就可以了。
注意如果TAB字元縮排在這裡沒有被嚴格遵守的話,Bash直譯器可能會報出錯誤。
像在正文中的 - Uses UTF-8
除開行首的 TAB字元縮排之外,還包含兩個空格字元,這不會受到 <<-
的影響而被刪除。
禁止變數展開
一般情況下,HEREDOC 中的 ${VAR}
,$(pwd)
,$((1+1))
等語句會被展開,當你想要編寫 ssh 指令時,可能你希望的是不要展開 $
標記。
這可以用 <<"EOF"
來實現。
只需要在 IDENT
標記上加上引號包圍就可以達到效果,結束標記則無需引號。
cat <<"EOF"
Command is:
$ lookup fantasy
EOF
# 如果不想展開,則你需要對 $ 字元進行轉義
cat <<EOF
\$ lookup fantasy
EOF
複製程式碼
這個例子中,請注意單個的 $
字元其實是不會展開也不會報錯的,所以我們只是為了編寫一個示例而已。
引號包圍呢,單引號、雙引號都可以,都會同樣地生效。
甚至,你可以使用轉義語法,也就是說:
cat <<\EOF
Command is:
$ lookup fantasy
EOF
複製程式碼
也能禁止引數展開。
同時應用上兩者
上面兩個新的語法特性,是可以被同時組合和運用的:
cat <<-"EOF"
Command is:
$ lookup fantasy
EOF
複製程式碼
雖然你可能根本不需要遇到這樣的情形。