shell 中的 set -e 導致的退出問題

legend050709ComeON發表於2020-09-25

目錄

set命令

set -e

set -o pipefail

set -u

set -x

grep 

退出碼

-c引數

grep範例

範例

範例1

範例2

解決方法



set命令

參見:http://www.ruanyifeng.com/blog/2017/11/bash-set.html

Bash 執行指令碼的時候,會建立一個新的 Shell。

# bash script.sh

上面程式碼中,script.sh是在一個新的 Shell 裡面執行。這個 Shell 就是指令碼的執行環境,Bash 預設給定了這個環境的各種引數。set命令用來修改 Shell 環境的執行引數,也就是可以定製環境。一共有十幾個引數可以定製,官方手冊有完整清單,本文介紹其中最常用的四個。順便提一下,如果命令列下不帶任何引數,直接執行set,會顯示所有的環境變數和 Shell 函式。

 

set -e

如果指令碼里面有執行失敗的命令(返回值非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從根本上解決了這個問題,它使得指令碼只要發生錯誤,就終止執行。

#!/usr/bin/env bash
set -e

foo
echo bar

另外:

set +e表示關閉-e選項,

set -e表示重新開啟-e選項。

還有一種方法是使用command || true,使得該命令即使執行失敗,指令碼也不會終止執行。

#!/bin/bash
set -e

foo || true
echo bar
上面程式碼中,true使得這一行語句總是會執行成功,後面的echo bar會執行。

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: 未找到命令

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

-u還有另一種寫法-o nounset,兩者是等價的。

set -o nounset

set -x

預設情況下,指令碼執行後,螢幕只顯示執行結果,沒有其他內容。如果多個命令連續執行,它們的執行結果就會連續輸出。有時會分不清,某一段內容是什麼命令產生的。set -x用來在執行結果之前,先輸出執行的那一行命令。

set -x 開始除錯;

set +x 結束除錯;

-x還有另一種寫法-o xtrace。

set -o xtrace

 

grep 

退出碼

退出狀態如果找到選定的行,則退出狀態為0,如果找不到,則為1。如果發生錯誤,則退出狀態為2。(注:POSIX錯誤處理程式碼應檢查'2'或更大。)

如果您想將退出程式碼強制設定為0,則只需將|| true附加到命令中即可:

echo 'Total' | grep -c No || true

 

-c引數

-c,--count禁止正常輸出;而是為每個輸入檔案列印匹配行數。

因此,您所看到的是匹配項的計數,不要與grep匹配項的退出程式碼混淆

grep -c "xx" 檔案  <=====> grep "xxx" 檔案 | wc -l

 

grep範例

echo 'Total' | grep -c No
0
因此,“總計”中不存在“否”。但是然後查詢其返回碼,
echo $?
1

echo 'No' | grep -c No
1

echo $?
0

範例

範例1

#! /bin/bash

set -x

set -o pipefail

local mlnx_enabled=$(lspci -s "0000:5e:00.0" | grep -ci "Mellanox")

if [[ ${mlnx_enabled} -ne 0 ]]; then

      echo "Mellanox nic enabled"

else 

      echo "Mellanox nic not enable"

fi

 

對於在非Mellanox nic 的機器上,上訴程式碼會直接退出,而不會輸出任何 echo 資訊。

因為 grep -ci "Mellanox" 的 輸出值為 0,退出碼為 1;

由於退出碼為1,即異常退出,此時 set -e 被設定,則會執行到 mlnx_enabled=$(lspci -s "0000:5e:00.0" | grep -ci "Mellanox")

時,程式直接退出;

範例2

#! /bin/bash

set -x

set -o pipefail

local mlnx_enabled=$(lspci -s "0000:5e:00.0" | grep -i "Mellanox" | wc -l)

if [[ ${mlnx_enabled} -ne 0 ]]; then

      echo "Mellanox nic enabled"

else 

      echo "Mellanox nic not enable"

fi

grep -i "Mellanox"  的退出碼為1,但是  grep -i "Mellanox" | wc -l 整體的退出碼其實是 wc -l 的退出碼,不為1,所以不會由於 set -e  而退出。

但是由於 set -o pipefail 的存在,管道的任何一個子命令失敗,都會退出。由於 grep -i "Mellanox" 失敗,從而導致程式退出。

解決方法

失敗cmd &&  或者 失敗cmd || 就可以讓 set -e 失效;

# cat test.sh
#!/usr/bin/env bash

set -e
set -o pipefail

echo "test1"
MELLANOX="FALSE"
lspci -s "0000:5e:00.0" | grep -ci "222Mellanox" && MELLANOX="true"
echo "$?"
echo "MELLANOX: ${MELLANOX}"

echo "test2"
MELLANOX="FALSE"
lspci -s "0000:5e:00.0" | grep -ci "Mellanox" && MELLANOX="true"
echo "$?"
echo "MELLANOX: ${MELLANOX}"

echo "test3"
MELLANOX="FALSE"
lspci -s "0000:5e:00.0" | grep -ci "2222Mellanox"
if [[ $? -eq 0 ]]; then
  MELLANOX="true"
fi
echo "MELLANOX: ${MELLANOX}"

 

# sh -x test.sh
+ set -e
+ set -o pipefail
+ echo test1
test1
+ MELLANOX=FALSE
+ lspci -s 0000:5e:00.0
+ grep -ci 222Mellanox
0
+ echo 1
1
+ echo 'MELLANOX: FALSE'
MELLANOX: FALSE
+ echo test2
test2
+ MELLANOX=FALSE
+ lspci -s 0000:5e:00.0
+ grep -ci Mellanox
1
+ MELLANOX=true
+ echo 0
0
+ echo 'MELLANOX: true'
MELLANOX: true
+ echo test3
test3
+ MELLANOX=FALSE
+ lspci -s 0000:5e:00.0
+ grep -ci 2222Mellanox
0

相關文章