2015/03/18

bash 預設變數

$0 : 執行的命令全名
$1, $2... : 各項參數
$? : 前一個指令執行的傳回值,例如某執行檔的 int main() { return 1; },則 $? 就是 1
$# :代表後接的參數『個數』,命令本身不算在內;
$@ :代表『 "$1" "$2" "$3" "$4" 』之意,每個變數是獨立的(用雙引號括起來);
$* :代表『 "$1c$2c$3c$4" 』,其中 c 為 $IFS 指定的分隔字元,預設為空白, 所以本例中代表『 "$1 $2 $3 $4" 』之意。
$! :取得前一個指令的 PID

例如:
IFS_SAVE="$IFS"
IFS="."
/tmp/scripttest opt1 opt2 opt3
$0 = "/tmp/scripttest"
$1 = "opt1"
$2 = "opt2"
$3 = "opt3"
$# = 3
$@ = "opt1" "opt2" "opt3"
$* = "opt1.opt2.opt3"
IFS="$IFS_SAVE"

bash 流程控制

Include

我們的 shell script 大到一定程度時,用單一檔案會變得不好管理維護,於是會把 shell script 寫成多個檔案,再用 include 的方式把其他檔案加進來。
bash 的 include 是用 . 開頭,後面接檔名。例如
#!/bin/sh
. /etc/init.d/getmibs
就會將 /etc/init.d/getmibs 的內容插進來。而取得 mibs 內容的 script 就用 /etc/init.d/getmibs 來維護就好,整個 script 看起來會變得很乾淨。

Function

例用 function 來結構化 script,可以增加 script 的可讀性,例如:
#!/bin/sh
hello()
{
    echo "Hello World!"
}
hello

夾帶參數

呼叫 function 時如果有參數,會在 function 裡面以 $1, $2.... 呈現,因為字串在 shell script 的定義並不是很嚴謹,所以要特別注意字串的傳遞。例如:
#!/bin/sh
hello()
{
    echo "----------"
    echo "var1 = $1"
    echo "var2 = $2"
}
hello Hello World!
hello "Hello World!"
從上面的例子觀察其結果,會發現第二次呼叫會把 "Hello World!" 當成一個變數來用,而第一是呼叫的會變成兩個,不得不防。

if 條件式

if [ 條件 ]; then
    條件成立時執行的內容
fi
if [ 條件 ]; then
    條件成立時執行的內容
else
    條件不成立時執行的內容
fi
if [ 條件1 ]; then
    條件1 成立時執行的內容
elif [ 條件2 ]; then
    條件1 不成立而條件2 成立時執行的內容
else
    條件1 跟條件2 都不成立時執行的內容
fi

case 條件式

case "$變數" in
    "值1")
        當 "$變數" == "值1" 時執行的內容
        ;;
    "值2")
        當 "$變數" == "值2" 時執行的內容
        ;;
    ... ...
    *)
        以上都不滿足時執行的內容
        ;;
esac

while / until 迴圈

while [ 條件 ]
do
    迴圈執行的內容
done
until [ 條件 ]
do
    迴圈執行的內容
done
while 是當條件成立時,執行回圈的內容,而 until 則是執行到條件成立時退出。迴圈執行期間也可以用 break 跳離迴圈。

for xxx in ooo 迴圈

for 變數 in 值1 值2 值3 ...
do
    迴圈執行內容
done
第一次執行迴圈內容時 $變數=值,第二次 $變數=值2 ...,值到所有變數都跑過之後結束。同樣要注意字串的處理。例如
#!/bin/sh
DNS1=8.8.8.8
DNS2=8.8.4.4
DNS3=0.0.0.0
for DNS_ADDR in $DNS1 $DNS2 $DNS3
do
    echo "DNS_ADDR=$DNS_ADDR"
done
for DNS_ADDR in "$DNS1 $DNS2 $DNS3"
do
    echo "DNS_ADDR=$DNS_ADDR"
done

數字迴圈
for 變數 in {開始..結束}
do
    迴圈執行內容
done

例如:
#!/bin/sh
for i in {1..65535}
do
   echo "$i time"
done

bash 判斷式

在 bash 環境下,可以使用 test 條件判斷 && 成立時執行 || 不成立時執行,例如
test 1 -eq 1 && echo "good" || echo "bad"
因為 1 永遠等於 1 ,所以上式會輸出 good
寫在 shell script 裡面,會用 [ ] 來表示判斷式,例如上面的例子就會變成
[ 1 -eq 1 ] &&
{
    echo "good"
} || {
    echo "bad"
}

或用另外一種 if 條件判斷的結構
if [ 1 -eq 1 ]; then
    echo "good"
else
    echo "bad"
fi

檔案存在

-e    檔名是否存在(無論是檔案或資料夾)
    [ -e "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,存在則傳回 .TRUE. 不存在則傳回 .FALSE.
-f    檔名是否以檔案存在
    [ -f "/etc/resolv.conf" ] 會去檢查 /etc/resolve.conf 是否存在而且為檔案
-d    檔名是否以資料夾存在
    [ -d "/etc/ppp" ] 會去檢查 /etc/ppp 是否存在而且是個資料夾
-b    檔名是否以 block device 存在
    [ -b "/dev/sda" ] 會去檢查 /etc/sda 是否存在,而且是個 block device
-c    檔名是否以 character device 存在
    [ -c "/dev/tty" ] 會去檢查 /dev/tty 是否存在,而且是個 character device
-S    檔名是否以 socket 存在
    [ -S "/tmp/.X11-unix/X0" ] 會去檢查 /tmp/.X11-unix/X0 是否存在,而且是個 socket file
-p    檔名是否以 FIFO (pipe) 存在
    [ -p "/tmp/mypipe" ] 會去檢查 /tmp/mypipe 是否存在,而且是個 FIFO file
-L    檔名是否以連結方式存在
    [ -L "/tmp" ] 會去檢查 /tmp 是否存在,而且是個連結
block device 是一種以 block 為單位傳輸的裝置,因此每次傳輸都會是 block 的倍數,屬於固定長度傳輸。主要用於隨機存取裝置,例如硬碟、光碟機。而 Character Device 則是以字元為傳輸單位,屬於非固定長度傳輸。主要用於印表機,終端機...。

檔案權限與屬性

-r    檔名是否存在,而且具有讀取權限
    [ -r "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,而且具有可讀取的權限
-w    檔名是否存在,而且具有寫入權限
    [ -w "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,而且具有可寫入的權限
-x    檔名是否存在,而且具有執行權限
    [ -x "/bin/busybox" ] 會去檢查 /bin/busybox 是否存在,而且具有可執行的權限
-u    檔名是否存在,而且具有 SUID 屬性
    [ -u "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,而且具有 SUID 屬性
-g    檔名是否存在,而且具有 SGID 屬性
    [ -g "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,而且具有 SGID 屬性
-k    檔名是否存在,而且具有 Sticky bit 屬性
    [ -k "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,而且具有 Sticky bit 屬性
-s    檔名存在,而且不是空白檔案
    [ -s "/etc/resolv.conf" ] 會去檢查 /etc/resolv.conf 是否存在,而且不是空白檔案

整數比較判斷

-eq    兩個整數是否相等 (equal)
    [ $n1 -eq $n2 ]
-ne    兩個整數是否不相等 (not equal)
    [ $n1 -ne $n2 ]
-gt    n1 是否大於 n2 (greater then)
    [ $n1 -gt $n2 ]
-lt    n1 是否小於 n2 (lesser then)
    [ $n1 -lt $n2 ]
-ge    n1 是否大於等於 n2 (greater then or equal)
    [ $n1 -ge $n2 ]
-le    n1 是否小於等於 n2 (lesser then or equal)
    [ $n1 -le $n2 ]

字串判斷

-z    判斷是否為空字串
    [ -z "$str1" ]    檢查 str1 是不是空字串,是空字串傳回 .TRUE.,否則傳回 .FALSE.
-n    判斷是否不是空字串,跟 -z 剛好相反
    [ -n "$str1" ]    檢查 str1 是不是空字串,是空字串傳回 .FALSE.,否則傳回 .TRUE.
==    判斷字串是否相等
    [ "$str1" == "$str2" ]
!=    判斷字串是否不相等
    [ "$str1" != "$str2" ]

其他邏輯

-a    前後兩個條件用 and 作邏輯運算
    [ $n1 -eq $n2 -a "$str1" == "$str2" ]
-o    前後兩個條件用 or 作邏輯運算
    [ $n1 -eq $n2 -o "$str1" == "$str2" ]
!    反相邏輯運算
    [ ! -e "/etc/resolve.conf" ]

習慣與注意事項

因為 bash 在很多地方會把空格當成隔開的符號,尤其是條件判斷式的每個單元都要用空格格開,包含前後的括號 [ ] ,所以 [ 1 -eq 1 ] 不能寫成 [1 -eq 1]。而變數內往往也會存在空白符號,所以用在字串判斷的時候,最好能養成習慣把變數用 "" 刮起來,例如 "$str1"。舉個例子來說
    str1="This is a book."
    [ $str1 == "This is a book" ]

因為 $str1 會變成 This is a book 而不是 "This is a book" 所以上式會變成 [ This is a book == "This is a book" ],會造成參數解析上的錯誤,所以還是養成習慣,只要遇到字串變數,都加上 "",如上例寫成 [ "$str1" == "This is a book." ] 就不會出錯了。

bash 字串切割

設定值的內容使用某個字符區隔各單項內容是很常見的,如何在 script 將這些內容切割出來使用是很重要的課題,需要使用到的是內建的 $IFS 這個變數。嘗試執行下面的 script 可以很容易理解 $IFS 的運作。
#!/bin/sh
set - $(IFS=" "; echo This is a book)
echo $1
echo $2
echo $3
echo $4

得到的結果是 $1="This", $2="is", $3="a", $4="book",所以 set - $(IFS=" "; echo This is a book) 就是把後面的字串用空白字元 " " 當作區隔,把切割後的內容放到 $1, $2...。
以下的範例是利用 $IFS 來尋找執行檔存在哪個 $PATH 資料夾內,我習慣取名為 findpath
if [ 1 -eq $# ]; then
    RESULT="not found!"
    FILENAME=$1
    set - $(IFS=":"; echo $PATH)
    i=1
    eval "DIR=\$$i"
    until [ "" == "$DIR" ]
    do
        if [ -x "$DIR/$FILENAME" ]; then
            RESULT="$DIR/$FILENAME"
            DIR=""
        else
            i=$(($i+1))
            eval "DIR=\$$i"
        fi
    done
    echo $RESULT
else
    echo "Syntax: $0 [filename]"
    echo "    filename: The filenam you want to find in PATH"
fi

應用在 for 迴圈

findpath 的另一個寫法,先把原本的 IFS 存到 IFS_SAVE,然後指定冒號 ":" 為切割符號,這時候 for DIR in $PATH 就會把 $PATH 用冒號將每個單位切出來使用。
if [ 1 -eq $# ]; then
    RESULT="not found!"
    IFS_SAVE="$IFS"
    IFS=":"
    for DIR in $PATH
    do
        if [ -x "$DIR/$1" ]; then
            RESULT="$DIR/$1"
            break
        fi
    done
    IFS="$IFS_SAVE"
    echo $RESULT
else
    echo "Syntax: $0 [filename]"
    echo "    filename: The filenam you want to find in PATH"
fi

IFS 的初始值

執行 set | grep IFS 可以看到目前的 IFS 設定值,在還沒變更之前就是初始值,一般是
IFS=$' \t\n'

bash 用變數內容當變數名稱取值

在很多情況下我們會遇到無法預知個數的變數群,而且變數名稱最後是採用數字來當作區別,因為是未知個數,所以要使用一個動態迴圈來處理,如何取得個別數值來處理變得很重要。參考下面的範例:
DNS1="8.8.8.8"
DNS2="8.8.4.4"
DNS3="0.0.0.0"
DNS4="139.175.150.20"
DNS5="168.95.1.1"
DNS_CNT=5
RESOLV_CONF="/etc/resolv.conf"
i=1
echo "; generated by my script" > $RESOLV_CONF
while [ $i -le $DNS_CNT ]
do
    eval "DNS_ADDR=\$DNS$i"
    if [ "0.0.0.0" != "$DNS_ADDR" ]; then
        echo "nameserver $DNS_ADDR" >> $RESOLV_CONF
    fi
done

最主要看 do 迴圈的第一行,eval "DNS_ADDR=\$DNS$i" ,因為 eval 會去執行它後面帶出來的那一行,而 \$ 會是 $ 符號,$i 則是目前 i 的值,因此當 i=1 時 "DNS_ADDR=\$DNS$i" 就會是 DNS_ADDR=$DNS1,透過 eval 執行,就會把 DNS1 的值帶給 DNS_ADDR 以利後面處理。