大家好,我是張晉濤。
提到 Shell 大家想必不會太陌生,我們通常認為 Shell 是我們和系統互動的介面,執行命令返回輸出,比如 bash 、zsh 等。偶爾也會有人把 Shell 和 Terminal(終端)混淆,但這和本文關係不大,暫且略過。
作為一名程式設計師,我們可能天天都會用到 Shell ,偶爾也會把一些命令組織到一起,寫個 Shell 指令碼之類的,以便提升我們的工作效率。
然而在看似簡單的 Shell 指令碼中,可能隱藏著很深的坑。這裡我先給出兩段簡單且相似的 Shell 指令碼,大家不妨來看看這兩段程式碼的輸出是什麼:
#!/bin/bash
set -e -u
i=0
while [ $i -lt 6 ]; do
echo $i
((i++))
done
答案是隻會輸出一個 0 。
#!/bin/bash
set -e -u
let i=0
while [ $i -lt 6 ]; do
echo $i
((i++))
done
答案是沒有任何輸出,直接退出。
如果你能解釋清楚上面兩段程式碼輸出結果的話, 那大概你可以跳過這篇文章後續的內容了。
我先來分解下這段程式碼中涉及到的主要知識點。
變數宣告
變數宣告有很多種辦法, 但是其行為卻各有不同。
我們必須先有個基礎認識: Bash 沒有型別系統,所有變數都是 string 。 基於這個原因,如果是讓變數進行算術運算時,不能像在其他的程式語言中那樣直接寫算術運算子。這會讓 bash 解釋為對 string 的操作,而不是對數字的操作。
直接宣告
(MoeLove)➜ ~ foo=1+1
(MoeLove)➜ ~ echo $foo
1+1
直接宣告最簡單,但正如前面提到的,直接宣告會預設當作 string 進行處理,不能在宣告時進行算術運算。
declare 宣告
(MoeLove)➜ ~ declare foo=1+1
(MoeLove)➜ ~ echo $foo
1+1
除去直接宣告變數外,比較常用的方法是用 declare
來宣告變數,但預設情況下,其宣告的變數都是按 string 處理的,無法進行正常的算術運算。
declare 整數屬性
declare 在宣告變數的時候,可以通過 -i
引數增加整數屬性,當變數被賦值時,將進行算術運算。
(MoeLove)➜ ~ declare -i bar=1+1
(MoeLove)➜ ~ echo $bar
2
但要注意的是,增加整數屬性後,如果將字串賦值給它,則會出現解析失敗的情況,即:將值設定為 0:
(MoeLove)➜ ~ bar=test
(MoeLove)➜ ~ echo $bar
0
let 宣告
另一種辦法,我們可以通過 let
命令進行變數的宣告,這種方式允許在宣告時進行算術運算,同時也支援將其他值賦值給此變數。
(MoeLove)➜ ~ let baz=1+1
(MoeLove)➜ ~ echo $baz
2
(MoeLove)➜ ~ baz=moelove.info
(MoeLove)➜ ~ echo $baz
moelove.info
while 迴圈
while list-1; do list-2; done
Bash 中 while 語法就是這樣,在 while 關鍵字後是一個序列(list),可以是一個或多個表示式/語句,
需要注意的是,當 list-1 返回值為 0 時, list-2 總是會被執行,並且 while 語句最後的返回值是 list-2 最後一次執行的返回值,或者,如果沒執行任何語句的話,則返回 0 。
bash 中的算數計算
這部分的內容大家想必常會用到。我來介紹幾種常用的方法:
算術擴充套件
Bash 中的擴充套件一共有 7 種,算術擴充套件只是其中之一。具體而言就是通過類似 $((expression))
這樣的形式,來計算表示式的值。例如:
(MoeLove)➜ ~ echo $((3+7))
10
(MoeLove)➜ ~ x=3;y=7
(MoeLove)➜ ~ echo $((x+y))
10
expr 命令
expr 是 coreutils 軟體包提供的一個命令,可對錶達式進行計算,或者比較大小之類的。
(MoeLove)➜ ~ x=3;y=7
(MoeLove)➜ ~ expr $x + $y
10
# 比較大小
(MoeLove)➜ ~ expr 2 \< 3
1
(MoeLove)➜ ~ expr 2 \< 1
0
bc 命令
按定義來說,bc 其實是一種支援任意精度和可互動執行的計算語言。它比上述提到的 expr
要強大的多,尤其是它還支援浮點數運算。例如:
一般浮點數計算
(MoeLove)➜ ~ echo "scale=2;7/3"|bc
2.33
(MoeLove)➜ ~ echo "7/3"|bc
2
注意: scale
需要手動指定,它表示小數點後的位數。預設情況下 scale
的值為 0 。
內建函式
bc 還有一些內建函式,可以方便我們進行一些快速的計算,比如可以利用 sqrt()
快速的計算平方根。
(MoeLove)➜ ~ echo "scale=2;sqrt(9)" |bc
3.00
(MoeLove)➜ ~ echo "scale=2;sqrt(6)" |bc
2.44
指令碼
此外, bc 還支援一種簡單的語法,可以支援宣告變數,編寫迴圈和判斷語句等。例如:我們可以列印20 以內可以被 3 整除的數:
(MoeLove)➜ ~ echo "for(i=1; i<=20; i++) {if (i % 3 == 0) i;}" |bc
3
6
9
12
15
18
bash 的除錯
其實 bash shell 中並沒有內建偵錯程式。很多情況下,都是採用重複執行加列印來進行除錯。但這種方式不夠高效。
這裡介紹一種比較直觀的,也比較方便的用來除錯 shell 程式碼的辦法。以下是一段示例 shell 程式碼。
(MoeLove)➜ ~ cat compare.sh
#!/bin/bash
read -p "請輸入任意數字: " val
real_val=66
if [ "$val" -gt "$real_val" ]
then
echo "輸入值大於等於預設值"
else
echo "輸入值比預設值小"
fi
為其增加執行許可權,或者使用 bash 執行:
(MoeLove)➜ ~ bash compare.sh
請輸入任意數字: 33
輸入值比預設值小
詳細模式
通過增加 -v
選項,即可開啟詳細模式,用於檢視所執行的命令。當然,我們也可以通過在 shebang 上直接增加 -v
選項, 或者增加 set -v
來開啟此模式
(MoeLove)➜ ~ bash -v compare.sh
which () { ( alias;
eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@"
}
#!/bin/bash
read -p "請輸入任意數字: " val
請輸入任意數字: 33
real_val=66
if [ "$val" -gt "$real_val" ]
then
echo "輸入值大於等於預設值"
else
echo "輸入值比預設值小"
fi
輸入值比預設值小
使用 xtrace 模式
我們可以通過增加 -x
引數來進入 xtrace 模式,用於除錯執行階段的變數值。
(MoeLove)➜ ~ bash -x compare.sh
+ read -p '請輸入任意數字: ' val
請輸入任意數字: 33
+ real_val=66
+ '[' 33 -gt 66 ']'
+ echo 輸入值比預設值小
輸入值比預設值小
識別未定義變數
以下示例中,我故意寫錯一個字元。執行指令碼後,你會發現沒有任何報錯,但結果並不是我們預期的。這類可能是手誤居多,所以我們需要檢查是否存在未繫結的變數。
(MoeLove)➜ ~ cat add.sh
#!/bin/bash
five=5
ten=10
total=$((five+tne))
echo $total
(MoeLove)➜ ~ bash add.sh
5
(MoeLove)➜ ~ bash -u add.sh
add.sh: line 4: tne: unbound variable
增加 -u
選項, 可以檢查變數是否未定義/繫結。
組合使用
以上是幾種比較常見的使用方式,當然,也可以把它進行組合使用。比如上面的變數未定義的問題, 組合使用 -vu
就可以直接看到具體出現問題的程式碼是什麼內容了。
(MoeLove)➜ ~ bash -vu add.sh
which () { ( alias;
eval ${which_declare} ) | /usr/bin/which --tty-only --read-alias --read-functions --show-tilde --show-dot "$@"
}
#!/bin/bash
five=5
ten=10
total=$((five+tne))
add.sh: line 4: tne: unbound variable
將除錯資訊輸出到指定檔案
這裡我開啟了一個特定 FD 上的 debug.log
檔案,注意這個 FD 需要與 BASH_XTRACEFD
配置的一致,另外我修改了 PS4
的變數內容,它的預設值是 +
看起來會比較亂,而且沒有有效資訊,我通過設定 PS4='$LINENO: '
讓它顯示行號。
然後在需要除錯的位置設定 set -x
,在結束的未知設定 set +x
,這樣除錯日誌中就只會記錄我需要除錯部分的日誌了。
(MoeLove)➜ ~ cat compare.sh
#!/bin/bash
exec 6> debug.log
PS4='$LINENO: '
BASH_XTRACEFD="6"
read -p "請輸入任意數字: " val
real_val=66
set -x
if [ "$val" -gt "$real_val" ]
then
echo "輸入值大於等於預設值"
else
echo "輸入值比預設值小"
fi
set +x
echo "End"
(MoeLove)➜ ~ bash compare.sh
請輸入任意數字: 88
輸入值大於等於預設值
End
(MoeLove)➜ ~ cat debug.log
8: '[' 88 -gt 66 ']'
10: echo $'\350\276\223\345\205\245\345\200\274\345\244\247\344\272\216\347\255\211\344\272\216\351\242\204\350\256\276\345\200\274'
14: set +x
這裡介紹了通過 set 設定選項 的方式較簡單,其他的比如使用 trap 加除錯的方式也推薦大家去嘗試下,這裡就不展開了。
回到開始的問題
那我們用剛從介紹的除錯方法來執行下開頭的兩個指令碼,並且進行問題的解答。
第一個
(MoeLove)➜ ~ bash -xv demo1.sh
#!/bin/bash
set -e -u
+ set -e -u
i=0
+ i=0
while [ $i -lt 6 ]; do
echo $i
((i++))
done
+ '[' 0 -lt 6 ']'
+ echo 0
0
+ (( i++ ))
從上述除錯結果可以看到,這個指令碼在輸出 0
然後執行完 ((i++))
後退出。為什麼呢? 主要是由於在指令碼頂部增加的 set -e
選項。
該選項在遇到首個非 0 值的時候會直接退出。 我們來解釋下:
(MoeLove)➜ ~ i=0
(MoeLove)➜ ~ $((i++))
(MoeLove)➜ ~ echo $?
1
可以看到,執行 ((i++))
後,返回值其實是 1 ,所以觸發了 set -e
的退出條件,指令碼便退出了。
第二個
(MoeLove)➜ ~ bash -xv demo2.sh
#!/bin/bash
set -e -u
+ set -e -u
let i=0
+ let i=0
第二個和第一個的最主要區別在於變數的賦值上, let i=0
的返回值是 1 ,所以也就會觸發 set -e
的退出條件了。我們嘗試將第二個指令碼修改下,再次執行:
[tao@moelove ~]$ cat demo2-1.sh
#!/bin/bash
set -e -u
let i=1
while [ $i -lt 6 ]; do
echo $i
((i++))
done
[tao@moelove ~]$ bash demo2-1.sh
1
2
3
4
5
將 let i=0
修改成 let i=1
即可按預期執行成功。
總結
本篇中,我們主要聊了 bash shell 中的變數宣告,迴圈,數學運算以及 bash shell 的除錯。是否對你有所啟發呢? 歡迎留言進行交流。
- 注:本文僅討論 Bash Shell
歡迎訂閱我的文章公眾號【MoeLove】