1. 程式人生 > >編寫健壯的Bash shell指令碼

編寫健壯的Bash shell指令碼


許多人都能很快的碼出一些shell程式碼來完成簡單的任務,而且這種寫法將會一直持續下去。問題是編寫的shell指令碼經常會包含著許多足以導致指令碼執行失敗的細小的缺陷(subtle effects)。本文中我就將解釋編寫一個健壯的Bash指令碼所需要的一些技術,告訴你是能做到將這些問題減少到最小的。

top使用set -u
你是否會經常遇到因為變數沒有賦值而導致指令碼無法成功執行的情況呢?反正我是經常的遇到。
chroot=$1 ... 
rm -rf $chroot/usr/share/do
如果你在執行上述指令碼的時候忘記了提供一個引數的話,最後的結果是你會把所有系統文件都刪掉,而不是僅僅刪除$chroot下指定的文件。那你該怎麼辦呢?還好,Bash提供了一個選項set -u,使用這個選項可以使指令碼在使用未初始化的變數時直接退出。這個選項的另一個可讀性更強點的寫法是set -o nounset。
david% bash /tmp/shrink-chroot.sh 
$chroot= 
david% bash -u /tmp/shrink-chroot.sh 
/tmp/shrink-chroot.sh: line 3: $1: unbound variable 
david%

top使用set -e
你應該在你編寫的每個指令碼上方都加上set -e選項,開啟這個選項之後,指令碼在執行時碰到返回值不為0的語句之後會直接退出。使用-e選項的好處是使你能及早的發現問題,而不是讓錯誤越滾越大。同樣這個選項也有另外一種可讀性更強點的寫法set -o errexit。
-e選項可以為你做免費的錯誤檢查,如果你忘了檢查的話,它會替你完成。不過不好的是你無法在使用$?來進行檢查了,因為如果返回值不為0的話語句根本就執行不到檢查$?的那一步的。解決方法是重寫下程式碼:
用
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
如果你有一個命令它就是返回0或者是你根本就不關心返回值那怎麼辦呢?你可以使用command || true,或者假如你要對很長的一段程式碼都如此處理的話,你可以暫時關閉錯誤處理,不過我的建議是儘量少用。
set +e 
command1 
command2 
set -e
另外有個和這個有點相關的說明:預設情況下Bash返回最後一個管道命令的執行結果,你可能不希望是這樣,例如false | true執行返回值為是0,成功;如果你想它是失敗的話,執行set -o pipefail就可以了。

top防禦型程式設計 – 未雨綢繆
你的指令碼應該要考慮到應對一些比如檔案不存在或者無法建立目錄之類的異常情況,採取一些措施避免在碰到這些情況時發生錯誤。比如說如果在建立一個目錄時,如果上級目錄不存在,mkdir就會返回錯誤,如果你在使用mkdir是加上-p選項的話就能一併建立所有不存在的上級目錄了。另一個例子就是rm,如果你要rm一個不存在的檔案,那rm就會報錯,指令碼也會退出(應該在用-e選項了吧,對不?)你可以通過加上-f引數來解決這個,這個引數會保證在檔案不存在時安靜的執行下一步。

top注意檔名中的空格

有人總喜歡在檔名或者是命令列引數中使用空格,所以你在寫指令碼時一定要注意這點,特別注意要在變數上加上引號。
if [ $filename = "foo" ];
上述程式碼在$filename中包含空格時會失敗,可以這樣修正:
if [ "$filename" = "foo" ];
在使用[email protected]變數時,總要記得使用雙引號,這樣才能保證當引數值中存在空格時不會被解析成單獨的詞。
david% foo() { for i in [email protected]; do echo $i; done }; foo bar "baz quux" 
bar 
baz 
quux 
david% foo() { for i in "[email protected]"; do echo $i; done }; foo bar "baz quux" 
bar 
baz quux
我想不出有什麼時候是需要你用[email protected]代替”[email protected]”的,所以當你存疑時,就加上雙引號吧。

top設定trap(Setting traps)
指令碼在執行之中意外的退出經常會讓檔案系統處於一種不一致的狀態,就像鎖檔案(lock file)、臨時檔案或者是你更新了一個檔案卻在更新另外一個檔案的時候發生了錯誤,如果我們能有什麼方法能解決這個問題好了,使得我們的程式執行出現問題的時候能夠刪除掉鎖檔案或是回滾到一個已知的正常的狀態。好在Bash提供了trap命令,這個命令能讓執行中的指令碼接受到unix訊號的時候執行指定的命令或是函式。
trap command signal [signal ...]
trap命令能捕獲很多的unix訊號(可以通過kill -l來得到unix訊號的清單),不過只為了清理的話只需要關注3個訊號量就行了,這3個就是:INT, TERM和EXIT。你也可以使用-作為trap命令中的command引數重置trap的狀態。
訊號量 描述
INT   中斷 – 使用CTRL+C組合鍵時會向指令碼傳送這個訊號
TERM  終止 – kill命令使用這個訊號來終止程序
EXIT  退出 – 這是個偽訊號量(pseudo-signal),在指令碼執行結束或者是使用exit命令退出指令碼時都會被觸發。
通常你可能會寫出如下使用鎖檔案的指令碼:
if [ ! -e $lockfile ]; then 
 touch $lockfile 
 critical-section 
 rm $lockfile 
else 
 echo "critical-section is already running" 
fi
當指令碼還在critical-section階段執行時被別人殺掉會發生什麼呢?鎖檔案會一直留在那裡,而你的程式也無法再運行了,解決方法就是
if [ ! -e $lockfile ]; then 
 trap "rm -f $lockfile; exit" INT TERM EXIT 
 touch $lockfile 
 critical-section 
 rm $lockfile 
 trap - INT TERM EXIT 
else 
 echo "critical-section is already running" 
fi
現在即使指令碼執行中被殺掉,鎖檔案也能被正常的刪除。注意我們要在trap命令執行之後使用exit退出,要不然程式將會從接收到訊號量的那個地方恢復執行。
競爭條件
這裡要指出來一下上面那個例子由於建立鎖檔案和檢查鎖檔案的時間不一樣會存在小的競爭條件的問題,一個可行的解決方案就是使用IO重定向加上使用Bash的noclobber模式,noclobber模式不允許重定向到一個存在的檔案上,程式碼如下所示:
if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; then 
 trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT 
 critical-section 
 rm -f "$lockfile" 
 trap - INT TERM EXIT 
else 
 echo "Failed to acquire lockfile: $lockfile." 
 echo "Held by $(cat $lockfile)" 
fi
另一個更復雜點的問題就是當需要一次更新多個檔案的時候,如果程式中途退出,你得讓它退出的更加優雅些,就是要做到要改變的東西被正確的改變,或是做到像什麼都沒有發生一樣。假設你用下面的指令碼增加使用者:
add_to_passwd $user 
cp -a /etc/skel /home/$user 
chown $user /home/$user -R
當磁碟空間不足或是程序中途被殺的話就會有問題了,這種情況下你可能就希望這個使用者以及相應的檔案都清理掉:
rollback() { 
 del_from_passwd $user 
 if [ -e /home/$user ]; then 
 rm -rf /home/$user 
 fi 
 exit 
} 
 trap rollback INT TERM EXIT 
add_to_passwd $user 
cp -a /etc/skel /home/$user 
chown $user /home/$user -R 
trap - INT TERM EXIT
這裡必須在指令碼的最後重置trap狀態為預設值,要不然在指令碼退出的時候rollback函式也會被執行的,然後所有的辛苦工作都白費了。

top保持原子性(Be atomic)
有時你需要更新一個資料夾下面的多個檔案,比如說你需要將網站的url從一個主機重寫為另一個主機,你可能會寫下如下指令碼:
for file in $(find /var/www -type f -name "*.html"); do 
 perl -pi -e 's/www.example.net/www.example.com/' $file 
done
現在如果指令碼執行途中出現問題退出的話,很可能就會造成網站的一部分已經換成了www.example.com而另一部分還是指向www.example.net,你可以通過恢復備份或者是使用trap來修復這個問題,但是在替換的過程中還是會有不一致的問題的。
解決方法就是將整個更改當成一個(接近於)原子操作來執行。就是先將資料備份一份,然後在備份檔案上面做變更,變更完以後接著將原檔案移走,用變更後的檔案替換到原來位置上。在此過程中要確保新舊檔案都存在於同一個分割槽上,這樣就可以利用unix檔案系統的快速移動資料夾的特性,因為這樣需要更改的只是目錄的inode。
cp -a /var/www /var/www-tmp 
for file in $(find /var/www-tmp -type f -name "*.html"); do 
 perl -pi -e 's/www.example.net/www.example.com/' $file 
done 
mv /var/www /var/www-old 
mv /var/www-tmp /var/www
這樣做就可以保證一旦更改出現問題,當前執行的系統不會受到影響,同時受影響的時間也就是兩個mv操作所花費的時間了,這個通常是非常快的,因為只需要更改目錄的inode,不需要移動任何的檔案。

這種做法的缺點一個是你需要兩倍的磁碟空間,再一個是如果有的程序需要一直開啟檔案的話那麼變換目錄之後這些程序開啟的還是舊檔案,而非新檔案,在這種情況下你就需要重啟這些程序了。如果使用apache的話這不會有問題,因為它在每次請求的時候都會重新開啟檔案,你也可以使用lsof命令來檢查那些檔案正在被開啟。好處就是你現在有一個變更前系統檔案的備份了,這樣一旦你後悔了還有回來的機會。

相關推薦

編寫健壯Bash shell指令碼

許多人都能很快的碼出一些shell程式碼來完成簡單的任務,而且這種寫法將會一直持續下去。問題是編寫的shell指令碼經常會包含著許多足以導致指令碼執行失敗的細小的缺陷(subtle effects)。本文中我就將解釋編寫一個健壯的Bash指令碼所需要的一些技術,告訴你是

編寫第一個Shell指令碼【TLCL】

怎樣編寫一個 Shell 指令碼 編寫一個指令碼 使指令碼檔案可執行 把指令碼放到Shell能夠找到的地方 指令碼檔案格式 #!/bin/bash # This is our first script. echo 'Hello World!' #

linux編寫自啟動shell指令碼

1.需求分析    在很多情況下,程式設計師都做著重複枯燥的工作,雖然這些工作也是必須的,其實這些重複性的工作可以執行指令碼替代;今天筆者就如何編寫自啟動shell指令碼減少程式設計師開啟伺服器後的環境開啟工作; 2.配置環境 linux版本:centos-6.3

Bash shell指令碼備忘

已開通新的部落格,後續文字都會發到新部落格 http://www.0xfree.top --- shell在linux中是很常用的一種解釋型別的指令碼,包含很多型別,使用範圍較廣的為bashshell ,在讀android原始碼中shell檔案隨處可見,故對此作一備忘(詳解shell都可

bash shell 指令碼變數的學習

Shell 指令碼(shell script),是一種為 shell 編寫的指令碼程式。 業界所說的 shell 通常都是指 shell 指令碼,但讀者朋友要知道,shell 和 shell script 是兩個不同的概念。 (需要有一丁點點點點的 Linux命令 基礎,

編寫第一個shell指令碼檔案

vi test.sh 這樣就新建了一個名為test.sh的指令碼檔案,之後按i進入編輯模式,輸入以下內容: echo "What is your name?" read PERSON echo "Hello, $PERSON" 按esc鍵退出編輯模式,再輸

編寫簡單的shell指令碼釋出war包到tomcat

下面是指令碼的完整程式碼,大家需要修改自己的home目錄,和war包名稱,最後只需要用bash命令去執行就可以了(例如bash tr_admin.sh) #!/bin/ksh home=/home/admin-tomcat START=$home/bin/startup.s

bash shell:指令碼中修改profile檔案更新LD_LIBRARY_PATH的示例

當我們的一個專案完成,需要在linux下安裝,就要寫安裝指令碼,有時還需要修改profile檔案修改環境變數。這時就要用到sed編輯器。sed編輯器被稱作流編輯器(stream editor),跟普通互動式檔案編輯器相反。在互動式編輯器中(比如vim),你可以用

Linux系統下編寫shell指令碼傳入引數列印系統當前執行緒數到指定檔案

  最近在做效能測試,要檢視系統執行過程中執行緒數,很簡單輸入命令:netstat -anp |grep java |wc -l,可以查詢。但是如何在執行過程定時列印系統執行緒數且將結果輸出到指定檔案呢?也很簡單我們直接寫一個shell指令碼執行下就可以了。以

shell指令碼編寫改密功能

#! /bin/bash read -p "請輸入修改的使用者名稱" user num=` cat /etc/passwd | cut -f1 -d':' |grep -w $user -c ` #查詢user是否在/etc/passwd ,並計算個數 #grep -q "$usern

《Linux命令列與shell指令碼程式設計大全》讀書筆記————第三章 基本的bash shell命令

本章內容 1、使用shell 2、bash手冊 3、瀏覽檔案系統 4、檔案和目錄列表 5、管理檔案和目錄 6、檢視檔案內容   3.3 bash手冊 命令: man  xterm  作用:檢視檢視xterm使用者手冊 man命

Shell指令碼編寫可能遇到的問題

      在編寫shell指令碼過程中,由於格式和語法問題,可能導致執行指令碼不成功。 梳理一些可能的原因如下: 1. No such file or directory沒有那個檔案或目錄問題分析: 1、將windows 下編寫好的SHELL檔案,傳到linu

10 個實戰及面試常用 Shell 指令碼編寫

注意事項 1)開頭加直譯器:#!/bin/bash 2)語法縮排,使用四個空格;多加註釋說明。 3)命名建議規則:變數名大寫、區域性變數小寫,函式名小寫,名字體現出實際作用。 4)預設變數是全域性的,在函式中變數local指定為區域性變數,避免汙染其他作用域。 5)有兩個命令能幫助我除錯指令碼:set -e

shell指令碼編寫流程!!!

ubuntu終端:ctrl +art + T  cd /home/thomas 到thomas目錄下 一、vi bigdata.sh 開啟vim編輯器 二、i 輸入內容 #!/bin/bash echo  Hadoop Hive Hbase

編寫shell指令碼一鍵啟動zookeeper叢集!!

踩了一個多小時坑終於解決了: 這裡分享給大家,更主要的目的是記住這些坑,避免以後重複走!!! 首先,這裡採用ssh祕鑰方式進行叢集主機之間免密登入執行啟動命令 這裡簡單說下原理: 通過ssh去另外一臺機器執行命令,直接執行還不行,因為需要環境變數,而ssh登入之後不在同一個程序裡面,所以環境變數不

Windows編寫shell指令碼,在linux上無法執行

  前兩天由於要查一個數據庫的binlog日誌,經常用命令寫比較麻煩,想著寫一個簡單的指令碼,自動去刷一下資料庫的binlog日誌,就直接在windows上面寫了,然後拷貝到linux中去執行,其實很簡單的指令碼,具體如下: #!/bin/bash #flush mysql logs every da

如何編寫一個優雅的Shell指令碼(三)

如何編寫一個優雅的Shell指令碼(三) 簡介 awk語法 awk內建變數 awk內建函式 awk實踐 awk檔案關聯 awk檔案拆分 總結 簡介 awk是shell腳本里面文字處理神奇

Linux:高效編寫shell指令碼的10個建議

轉載地址: https://mp.weixin.qq.com/s/YmROxFBkfMxuh_VaaI4wtg   【Linux命令】 linux下高效編寫shell指令碼的10個建議   在Linux環境下工作 ,shell指令碼的編寫應該是一個必備的基本技能了

一個小坑: -bash: ./backup.sh: /bin/bash^M: bad interpreter: No such file or directory 由於shell指令碼檔案被我在Windows下編輯過,出現上面錯誤的原因之一是指令碼檔案是DOS格式的, 即每一行的行尾以\r\n來標識

    由於shell指令碼檔案被我在Windows下編輯過,出現上面錯誤的原因之一是指令碼檔案是DOS格式的, 即每一行的行尾以\r\n來標識, 使用vim編輯器開啟指令碼, 執行::set ff? 可以看到DOS或UNIX的字樣. 使用se

Shell 程式設計 shell 指令碼編寫

原創轉載自 自海牛部落-青牛,http://hainiubl.com/topics/173 1 Vim 編輯器 1.1 vim 常用命令 1.2 針對程式設計師的vim 配置 配置方式: /etc/vimrc 全域性配置 ~/.vimrc 使用者級配置 ~/.vimin