認識 Here Document

基礎

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

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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章