伺服器的開發和管理離不開 Bash 指令碼,掌握它需要學習大量的細節。
set
命令是 Bash 指令碼的重要環節,卻常常被忽視,導致指令碼的安全性和可維護性出問題。本文介紹它的基本用法,讓你可以更安心地使用 Bash 指令碼。
一、簡介
我們知道,Bash 執行指令碼的時候,會建立一個新的 Shell。
$ bash script.sh
上面程式碼中,script.sh
是在一個新的 Shell 裡面執行。這個 Shell 就是指令碼的執行環境,Bash 預設給定了這個環境的各種引數。
set
命令用來修改 Shell 環境的執行引數,也就是可以定製環境。一共有十幾個引數可以定製,官方手冊有完整清單,本文介紹其中最常用的四個。
順便提一下,如果命令列下不帶任何引數,直接執行set
,會顯示所有的環境變數和 Shell 函式。
$ set
二、set -u
執行指令碼的時候,如果遇到不存在的變數,Bash 預設忽略它。
#!/usr/bin/env bash echo $a echo bar
上面程式碼中,$a
是一個不存在的變數。執行結果如下。
$ bash script.sh bar
可以看到,echo $a
輸出了一個空行,Bash 忽略了不存在的$a
,然後繼續執行echo bar
。大多數情況下,這不是開發者想要的行為,遇到變數不存在,指令碼應該報錯,而不是一聲不響地往下執行。
set -u
就用來改變這種行為。指令碼在頭部加上它,遇到不存在的變數就會報錯,並停止執行。
#!/usr/bin/env bash set -u echo $a echo bar
執行結果如下。
$ bash script.sh bash: script.sh:行4: a: 未繫結的變數
可以看到,指令碼報錯了,並且不再執行後面的語句。
-u
還有另一種寫法-o nounset
,兩者是等價的。
set -o nounset
三、set -x
預設情況下,指令碼執行後,螢幕只顯示執行結果,沒有其他內容。如果多個命令連續執行,它們的執行結果就會連續輸出。有時會分不清,某一段內容是什麼命令產生的。
set -x
用來在執行結果之前,先輸出執行的那一行命令。
#!/usr/bin/env bash set -x echo bar
執行上面的指令碼,結果如下。
$ bash script.sh + echo bar bar
可以看到,執行echo bar
之前,該命令會先列印出來,行首以+
表示。這對於除錯複雜的指令碼是很有用的。
-x
還有另一種寫法-o xtrace
。
set -o xtrace
四、Bash 的錯誤處理
如果指令碼里面有執行失敗的命令(返回值非0),Bash 預設會繼續執行後面的命令。
#!/usr/bin/env bash foo echo bar
上面指令碼中,foo
是一個不存在的命令,執行時會報錯。但是,Bash 會忽略這個錯誤,繼續往下執行。
$ bash script.sh script.sh:行3: foo: 未找到命令 bar
可以看到,Bash 只是顯示有錯誤,並沒有終止執行。
這種行為很不利於指令碼安全和除錯。實際開發中,如果某個命令失敗,往往需要指令碼停止執行,防止錯誤累積。這時,一般採用下面的寫法。
command || exit 1
上面的寫法表示只要command
有非零返回值,指令碼就會停止執行。
如果停止執行之前需要完成多個操作,就要採用下面三種寫法。
# 寫法一 command || { echo "command failed"; exit 1; } # 寫法二 if ! command; then echo "command failed"; exit 1; fi # 寫法三 command if [ "$?" -ne 0 ]; then echo "command failed"; exit 1; fi
另外,除了停止執行,還有一種情況。如果兩個命令有繼承關係,只有第一個命令成功了,才能繼續執行第二個命令,那麼就要採用下面的寫法。
command1 && command2
五、 set -e
上面這些寫法多少有些麻煩,容易疏忽。set -e
從根本上解決了這個問題,它使得指令碼只要發生錯誤,就終止執行。
#!/usr/bin/env bash set -e foo echo bar
執行結果如下。
$ bash script.sh script.sh:行4: foo: 未找到命令
可以看到,第4行執行失敗以後,指令碼就終止執行了。
set -e
根據返回值來判斷,一個命令是否執行失敗。但是,某些命令的非零返回值可能不表示失敗,或者開發者希望在命令失敗的情況下,指令碼繼續執行下去。這時可以暫時關閉set -e
,該命令執行結束後,再重新開啟set -e
。
set +e command1 command2 set -e
上面程式碼中,set +e
表示關閉-e
選項,set -e
表示重新開啟-e
選項。
還有一種方法是使用command || true
,使得該命令即使執行失敗,指令碼也不會終止執行。
#!/bin/bash set -e foo || true echo bar
上面程式碼中,true
使得這一行語句總是會執行成功,後面的echo bar
會執行。
-e
還有另一種寫法-o errexit
。
set -o errexit
六、set -o pipefail
set -e
有一個例外情況,就是不適用於管道命令。
所謂管道命令,就是多個子命令透過管道運算子(|
)組合成為一個大的命令。Bash 會把最後一個子命令的返回值,作為整個命令的返回值。也就是說,只要最後一個子命令不失敗,管道命令總是會執行成功,因此它後面命令依然會執行,set -e
就失效了。
請看下面這個例子。
#!/usr/bin/env bash set -e foo | echo a echo bar
執行結果如下。
$ bash script.sh a script.sh:行4: foo: 未找到命令 bar
上面程式碼中,foo
是一個不存在的命令,但是foo | echo a
這個管道命令會執行成功,導致後面的echo bar
會繼續執行。
set -o pipefail
用來解決這種情況,只要一個子命令失敗,整個管道命令就失敗,指令碼就會終止執行。
#!/usr/bin/env bash set -eo pipefail foo | echo a echo bar
執行後,結果如下。
$ bash script.sh a script.sh:行4: foo: 未找到命令
可以看到,echo bar
沒有執行。
七、總結
set
命令的上面這四個引數,一般都放在一起使用。
# 寫法一 set -euxo pipefail # 寫法二 set -eux set -o pipefail
這兩種寫法建議放在所有 Bash 指令碼的頭部。
另一種辦法是在執行 Bash 指令碼的時候,從命令列傳入這些引數。
$ bash -euxo pipefail script.sh
八、參考連結
(完)