編寫安全的shell腳本

來源:公衆號『很酷的程序員』
ID:RealCoolEngineer

正確安全地編寫shell腳本,避免腳本導致出乎意料的結果,並且在出現的問題的時候及時報錯退出。

本文基於筆者以往的shell腳本編程經驗,給出一些基礎的編程技巧,以便提高shell腳本的安全性和健壯性。

一 一定要使用shell嗎?

shell腳本其實很多地方可能都會導致問題,尤其在執行刪除(rm -rf xxx)操作的時候,更是要仔細。因此如果使用shell腳本不是必要的,那就儘量避免使用,畢竟只要不用就不會犯錯(^_^)。

在實踐的過程中,往往也會有額外的字符串處理、目錄遍歷等等一些操作,所以筆者實際工作中更多會採用Python + Shell的方式開發,使用Python的subprocess模塊執行一些需要在shell環境中執行的命令。但是在使用的時候要進行避免指定shell=True參數,否則還是可能遇到純shell編程中的一些問題。比如:

import subprocess as sp
# 推薦使用
sp.run(('ls', '-l'))
# 而不是
sp.run('ls -l', shell=True)

二 shell腳本推薦設置

shell本身是有一些配置選項的,通過內置命令set進行設置和查看,細節可以查看官方文檔:The Set Builtin

通過命令set -o可以查看shell的當前選項開關情況如:

➜ ~ # set -o | grep pipe
pipefail              off

通過set -o <option-name>可以開啓某個選項:

➜ ~ # set -o pipefail
➜ ~ # set -o | grep pipe
pipefail              on

使用set +o <option-name>則是關閉指定選項:

➜ ~ # set +o pipefail
➜ ~ # set -o | grep pipe
pipefail              off

下面重點來了,對於安全的shell腳本,建議在腳本開頭添加以下設置

set -euf -o pipefial

1 -e參數

在執行到某個命令返回非0時立即結束腳本

這是很有用的,在已經發生錯誤的時候及時結束腳本的執行,否則腳本即使有錯誤信息也還是會繼續執行下去,只要最後一個命令正確執行,那麼整個腳本的返回值將會是正常。如下腳本:

#!/bin/bash
set -e
echo "Begin to run"
some-command-not-exists  # Will quit script here
echo "Done"

上面的腳本將會在第4行處出現錯誤,並直接退出腳本。

如果某些命令可能執行錯誤是在預期內的,那麼可以通過在命令後面添加|| true來避免腳本直接退出:

some-comman-may-fail || true

如果是多行命令,則建議使用set +e暫時關閉發生錯誤則立即退出的功能:

set +e
some-comman-may-fail-1
some-comman-may-fail-2
...
set -e

2 -u參數

將未設置的參數或者變量視爲錯誤並退出腳本

比如以下腳本:

#!/bin/bash
#set -u
echo "Begin to run"
some-command "p1" $var "p3"  # Will quit script here
echo "Done"

因爲var變量沒有定義,那麼在第4行就根本不會執行命令,而是直接報錯退出。

這裏還有另外一個問題,如果沒有使用-uvar沒有定義,傳遞給命令的第二個參數就會變成"p3",顯然這不是編程者的本意。該問題的解決方法後面會提及。

3 -f參數

禁用文件名通配符

比如腳本:

#!/bin/bash
set -f
echo "Begin to run"
rm -rf *.md
echo "Done"

此時*.md並不會匹配所有文件名後綴爲.md的文件,而是文件名只能是"*.md"。這樣的好處在於避免錯誤操作一些後綴名相同的文件,此時可以通過執行需要操作的文件列表來替代。

此功能也可以通過內置命令shopt設置shopt -s failglob來實現,同樣的是不對通配符*做任何處理。
當然如果必須要使用通配符,那麼就不能使用-f參數了。

4 -o pipefail選項

針對管道,在管道中某個命令出錯時立即報錯返回。結合-e就可以實現在管道報錯時也直接退出腳本。

比如以下log分析命令:

set -e -o pipefail
find . -name "logcat*" | xargs grep -a -h "" | grep -a --color=auto -v "\-* switch to"

如果當前目錄沒有任何log文件,那就沒有必要繼續往下執行了。

類似地,如果多個命令之間存在順序依賴關係,那麼可以使用&&將多個命令連接起來,這樣在某個命令執行失敗時也可以提前返回。比如:

make && make test && make install

5 -x參數

顯示腳本的執行過程

此參數並不是安全編寫shell腳本的推薦參數,但是對於新手而言,使用此參數可以看到腳本的執行過程。而且不一定需要寫在腳本內部,也可以通過bash -x xxx.sh查看執行過程。
比如腳本:

#!/bin/bash
echo "Begin to run"
some-command "p1" $var "p3"
echo "Done"

執行時可以看到其流程:

+ echo 'Begin to run'
Begin to run
+ some-command p1 p3
./demo.sh: line 3: some-command: command not found
+ echo Done
Done

每個+後面就是執行的命令,緊接着的就是執行命令的輸出。

二 安全使用變量

1 任何時候都應該使用引號

在使用變量時,總應該使用雙引號。因爲shell腳本會執行Word Splitting,可能導致一些意想不到的情況。

和前面示範demo的一樣,對於some-command "p1" $var "p3",因爲var沒有定義,所以在執行時實際執行的是some-command p1 p3,因此就會發生意料之外的問題;而如果使用了雙引號,第二個參數就會是一個空字符串,這纔是更加符合預期的結果。

2 設置參數默認值

可以給變量設置默認值以避免一些異常的發生。

針對未定義字符串:

  1. ${var-defaultValue}:如果var未定義,那麼表達式爲默認值;
  2. ${var=defaultValue}:如果var沒有定義,那麼表達式爲默認值,並且設置var爲默認值。

針對未定義或者空字符串

  1. ${var:-defaultValue}:如果未定義或者爲空,那麼表達式爲默認值;
  2. ${var:=defaultValue}:如果var沒有定義或者爲空,那麼表達式爲默認值,並且設置var爲默認值。

三 藉助工具進行檢查

寫完腳本可以使用外部工具做一次檢查,比如開源的shellcheck工具,可以在本地安裝,也可以使用網頁版:https://www.shellcheck.net/

其實很多IDE(Clion、vscode)都有相關的插件支持shell腳本的檢查,安裝shellcheck插件即可。

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