認識 Here Document

hedzr發表於2019-05-26

基礎

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
複製程式碼

雖然你可能根本不需要遇到這樣的情形。

參考