0
本文作者: 恒亮 | 2017-03-28 19:12 |
雷鋒網(wǎng)按:Bash,作為大部分 Linux 發(fā)行版的出廠預設 Shell,因其晦澀難懂的語法設置,以及需要特別留心的編程細節(jié),幾乎成為 Linux 區(qū)別于其他操作系統(tǒng)的代名詞。針對 Bash 中一些極容易出錯的細節(jié),我們在這里總結了 10 條編程注意事項,希望對各位泛 Linux 環(huán)境的開發(fā)者有所裨益。原文來自一位名叫 Julia Evans 的開發(fā)者博客,雷鋒網(wǎng)編譯。
作為一名 Bash 腳本編寫經(jīng)驗超過 10 年的老程序員,我通常不用 Bash 處理復雜的編程任務。但作為一款我們在日常 Linux 使用中幾乎無法避免的通用工具,Bash 的確有許多與我們習以為常的 C++ 和 Java 等高級語言非常不同的基礎特性。在這里我并不打算討論 Bash 編程的高階應用,而是僅僅針對 Bash 中那些與眾不同的基礎特性做一簡單梳理和匯總。希望對各位有所幫助。
當然,如果你對閱讀博客不感興趣,這里我再順便推薦兩個開源免費的小工具。一個是 Shell 語法檢查工具 shellcheck,可以在運行前對腳本進行全面的語法檢查;另一個是 shfmt,可以自動對寫好的 Shell 腳本按照要求格式化。
shellcheck 地址:https://www.shellcheck.net/
shfmt 地址:https://github.com/mvdan/sh
Bash 中的賦值語句通常都是這樣的:
VARIABLE=2
然后我們通過 $VARIABLE 引用該變量。這里有一點非常重要,也極容易忽視的就是:千萬不要在等號兩邊加空格。雖然加上空格也不會引起語法錯誤,但很可能造成意想不到的結果。例如 VARIABLE= 2 這個語句,解釋器很可能會將一個空字符串賦值給 VARIABLE,然后運行一個名字叫 2 的腳本。
一般常用的 Bash 變量都是字符串,我很少見到有數(shù)組的。另外,雖然解釋器也接受小寫,但 Bash 中默認是將變量名全部大寫的。
例如我定義了一個變量 MYVAR,內(nèi)容是字符串“file.txt”,然后想執(zhí)行如下命令:
mv $MYVAR $MYVAR__bak # wrong!
結果一定會報錯。因為解釋器會搜索 MYVAR__bak 這個變量,而我們根本沒有定義。因此,為了避免出現(xiàn)類似問題,最好的辦法是每次引用時都在變量兩邊加上括號,就像這樣:
mv ${MYVAR} ${MYVAR}__bak # right!
Bash 有三種變量:全局變量、局部變量和環(huán)境變量。其中最常用的是環(huán)境變量。
實際上每個 Linux 進程都有許多預設的環(huán)境變量(運行 env 命令可查看),Bash 中對環(huán)境的變量的應用非常簡單。例如,想要查看 MYVAR 環(huán)境變量的值,可以運行下面這條命令:
echo "$MYVAR"
想要設置環(huán)境變量,可以用這條命令:
export MYVAR=2
需要注意的是,一旦在進程中設置了環(huán)境變量,那么這個環(huán)境變量會在所有與其相關的子進程中生效,例如下面這個例子:
export MYVAR=2; python test.py
$MYVAR 環(huán)境變量也會在 test.py 腳本中生效。
另一種是全局變量,如下所示這樣的賦值語句實際上就是在定義全局變量:
MYVAR=2
全局變量就像其他編程語言一樣,會在整個代碼中生效。
最后一種是局部變量,這種變量通常只在一個循環(huán)語句或者 Bash 函數(shù)中有效。一般不常用。
通常我會用下面這段 for 循環(huán)打印輸出 1-10 這 10 個數(shù)字。
for i in `seq 1 10` # you can use {1..10} instead of `seq 1 10`
do
echo "$i"
done
如果把這些代碼寫到一行里,是這樣的:
for i in `seq 1 10`; do echo $i; done
這里我想強調(diào)的是,通過反引號(即鍵盤上Tab鍵上方的按鍵,注意不是單引號)將 seq 命令的輸出結果,嵌入了 for 循環(huán)中直接使用。通過類似這種命令替換的方式,我們可以大大減少代碼冗余,同時減少代碼的出錯幾率。常見的替換方式有如下兩種:
OUTPUT=`command`
# or
OUTPUT=$(command)
if 語句的判定條件同時支持單中括號([])和雙中括號([[]]),他們都可以用來隔離表達式和 if 關鍵詞。但這里推薦使用雙中括號,因為它的容錯率更高,而且支持更多功能。另外,在 Linux 中單中括號 [ 實際與 test 命令是等價的,因此用雙括號顯然能避免更多的麻煩。
例如下面這段代碼:
If [[ -e /tmp/awesome.txt ]]; then
echo "awesome"
fi
可以判斷 awesome.txt 文件是否存在。
再比如下面的場景:
$ [ 3 < 4 ] && echo "true"
bash: 4: No such file or directory
$ [[ 3 < 4 ]] && echo "true"
true
使用單中括號會報錯,但雙中括號就沒問題。
除了使用雙中括號之外,還可以用 test 命令的運行結果作為 if 語句的判斷條件,例如:
test -e /tmp/awesome.txt
如果 awesome.txt 文件存在,則命令返回 0,否則返回錯誤碼。
實際上,除了常見的 test 命令,所有返回固定數(shù)值的命令都可以作為 if 語句的判斷條件。例如下面的代碼:
if grep peanuts food-list.txt
then
echo "allergy allert!"
fi
利用 grep 搜索關鍵詞,然后根據(jù)結果打印警告信息。
在 Bash 中定義和使用函數(shù)非常簡單(特別是無參函數(shù))。例如:
my_function () {
echo "This is a function";
}
my_function # calls the function
代碼中定義了一個 my_function 函數(shù),調(diào)用時也只需要寫函數(shù)名。
前面第 2 條提到要用 ${} 限定變量名的范圍,這里要說的是利用引號限定變量值的范圍。
例如下面代碼:
X="i am awesome"
Y="i are awesome"
if [ $X = $Y ]; then
echo awesome
fi
實際上會報錯,因為解釋器會將 if 語句的判定條件理解為:
if [ i am awesome == i are awesome ]
為了避免這種錯誤,就必須用雙引號限定變量值的范圍。
X="i am awesome"
Y="i are awesome"
if [ "$X" = "$Y" ]; then # i put quotes because i know bash will betray me otherwise
echo awesome
fi
這樣寫就沒問題了。當然,如果變量值不包括空格,那不帶引號也能得到同樣的結果,但畢竟帶上雙引號會讓程序更可靠。
每一個 Linux 程序都有返回值,按照規(guī)范,這個返回值在 0-127 之間,0 表示成功,其他值是含義各不相同的錯誤碼。在 Bash 中充分利用這一點可以增加程序的靈活性。例如:
create_user && make_home_directory
這條語句,只有 create_user 返回 0 時,才會執(zhí)行 make_home_directory。
而
create_user; make_home_directory
則表示無論 create_user 的返回值是什么,都會執(zhí)行 make_home_directory。
類似的,你也可以通過:
create_user || make_home_directory
表示只有當 create_user 返回非 0 值時,才會執(zhí)行 make_home_directory。
在 Bash 中,可以通過在命令后添加 & 符號實現(xiàn)后臺多任務。例如:
long_running_command &
把進程放入后臺后,還可以通過 fg 命令將其切換到前臺。如果后臺命令過多,可以先通過 jobs 命令查看進程的 job ID,然后用 fg+job ID 的方式將指定的后臺進程切換到前臺。
另外,還可以通過 wait 命令控制多任務的執(zhí)行順序。例如:
long_running_command1
wait
long_running_command2
表示在命令 1 執(zhí)行結束后才執(zhí)行命令 2。
在其他語言中,通常遇到錯誤的語句時,編譯器就會報錯并停止運行,但 Bash 不會。例如下面的代碼:
python non_existant_file.py
echo "done"
無論 non_existant_file.py 腳本是否存在,Bash 都會打印輸出 done。因此為了保證代碼的安全性和正確性,我們可以在代碼中用
set -e
對 Bash 環(huán)境進行一些額外設置,-e 表示出現(xiàn)錯誤就停止。
類似的,在其他語言中,使用沒初始化的變量也會報錯,但 Bash 不會。例如下面的代碼:
rm -rf "$DIRECTORY/*"
如果 $DIRECTORY 沒有提前初始化,Bash 也并不會停下來,而是直接以空字符串對待,那么這句命令的含義就變成了:嘗試刪除根目錄下的所有文件,結果將非常嚴重。
這時就可以用
set -u
表示 Bash 不執(zhí)行未定義的變量。
除了 -e 和 -u 之外,還有
set -x
表示每條命令執(zhí)行之前必須先打印命令內(nèi)容。此外還可以通過 set -o 顯示所有可以設置的選項。
這也是為什么許多 shell 腳本都以 set -eu 或者 set -eux 等做為開頭的原因,因為這樣就可以讓腳本運行在更安全的環(huán)境下。
來源:jvns.ca,雷鋒網(wǎng)編譯
雷峰網(wǎng)版權文章,未經(jīng)授權禁止轉載。詳情見轉載須知。