位元詩人 [Shell Scripting] 教學:控制結構 (Control Structure)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

如同大部分的程式語言,shell script 的執行順序是由上而下、依序執行。控制結構則是用來改變電腦程式的運行順序。本文介紹 shell script 的控制結構。

布林表達式

布林表達式用來判斷特定條件的真偽。在控制結構中,用來決定該結構的起始或終止條件。Shell script 的布林表達式有兩種語法:

  • test expression
  • [ expression ]

expression 的部分填入條件即可。

if

if 是基本的選擇控制結構。其使用方式如以下虛擬碼:

if condition;
then
    # Run commands here.
fi

只有在 condition 為真時,才會執行 then 區塊內的程式碼。使用 if 的反向字 fi 當成 if 敘述的結尾是 shell script 的特色。

除了單一的 if 敘述外,也可以加入選擇性的二元敘述:

if condition;
then
    # Run commands here only if condition is true.
else
    # Run commands here only if condition is false.
fi

condition 為真時,執行 then 區塊內的程式碼;反之,則執行 else 區塊內的程式碼。

除了單一敘述和二元敘述外,還可以使用選擇性的多元敘述:

if condition_a;
then
    # Run commands here only if condition_a is true.
elif condition_b;
then
    # Run commands here only if condition_b is true.
else
    # Run commands here only if none of the above conditions is true.
fi

condition_a 為真時,執行第一個 then 區塊內的程式碼;當 condition_b 為真時,則執行第二個 then 區塊內的程式碼;若兩者皆為偽,則執行 else 區塊內的程式碼。

注意 else 區塊是選擇性的,不需要時可省略不寫。

以下是使用 if 的實際程式碼片段:

if true;
then
    echo "It's true";
fi

另一種寫法是將 ifthen 寫在同一行:

if true; then
    echo "It's true";
fi

這兩種寫法僅是風格的差異,讀者擇一使用即可。

以下 shell script 用兩個 if 敘述來檢視系統上的 Objective-C 環境是否存在:

#!/bin/sh

if ! gcc --version 2>/dev/null 1>&2;
then
    echo "GCC is not installed on your system" >&2;
    exit 1;
fi

gcc_ver=$(gcc --version | head -n1 | grep -oP "\d.\d.\d$");
if [ ! -d "/usr/lib/gcc/x86_64-linux-gnu/${gcc_ver}/include/objc" ];
then
    echo "Objective-C runtime is not installed on your system" >&2;
    exit 1;
fi

exit 0;

第一個 if 敘述檢查 GCC 是否存在。檢視的方式為執行 gcc --version 指令,以確認 GCC 可用。

第二個 if 敘述檢查 Objective-C 運行期函式庫是否存在。由於函式庫非執行檔,故改檢查標頭檔的目錄是否存在。

在非蘋果平台,常用的 Objetive-C 編譯器是 GCC 而非 Clang,故針對 GCC 來檢查。

case

case 是另一個選擇控制結構,其虛擬碼如下:

case $value in
    pattern_a)
        # Run code here if $value fits pattern_a
        ;;
    pattern_b)
        # Run code here if $value fits pattern_b
        ;;
    *)
        # Run default code here
        ;;
esac

case 敘述會根據 value 的值選擇分支。當 value 符合 pattern_a 時,執行第一個分支。當 value 符合 pattern_b 時,執行第二個分支。當 value 不符合任何模式時,則執行預設分支。

case 的分支都是選擇性的,包括預設分支。若不需要可省略。

注意 case 敘述以反字 esac 結尾,這是 shell script 的特色。

以下實例用來檢查系統上可用的 Objective-C 編譯器:

#!/bin/sh

if ! command -v ${CC:=gcc} 2>/dev/null 1>&2;
then
    echo "No C compiler on your system" >&2;
    exit 1;
fi

case $CC in
    *gcc*|*clang*)
        true;  # pass.
        ;;
    *)
        echo "Not a valid Objective-C compiler: $CC" >&2;
        exit 1;
        ;;
esac

exit 0;

if 敘述用來檢查環境變數 CC。若該變數為空,則填入預設值 gcc。使用 command 指令來確認 CC 所代表的 C 編譯器是否存在。

case 敘述則用來檢查該 C 編譯器是否也是合法的 Objective-C 編譯器。目前可用的 Objective-C 編譯器為 GCC 和 Clang,所以針對這兩種編譯器去檢查即可。

由於 shell script 的 case 敘述具有過濾字串的功能,和 if 不完全等義。有關 case 所用的樣式比對語法請看此處

while

while 是不定次數的迭代控制結構。其虛擬碼如下:

while condition;
do
    # Run code here while `condition` is true.
done

只要 condition 為真,就會執次一次 while 區塊內的程式碼,然後再重新檢查一次 condition。實際上寫 while 敘述時,要在區塊內寫終止該迴圈的指令,否則就變無窮迴圈了。

這裡用 while 迴圈寫一個 shell 版本的終極密碼遊戲:

#!/bin/sh

max=100;
min=1;

answer=$(shuf -i $min-$max -n 1);

while true;
do
    printf "Please input a number between %d and %d: " $min $max;
    read -r guess;

    echo "$guess" | grep -q -P "^[+-]?\d+$"
    if [ 0 -ne $? ];
    then
        echo "Not an integer" >&2;
        continue;
    fi

    if [ "$guess" -lt "$min" ] || [ "$guess" -gt "$max" ];
    then
        echo "Invalid number" >&2;
        continue;
    fi

    if [ "$guess" -gt "$answer" ];
    then
        echo "Too large";
        max=$guess;
    elif [ "$guess" -lt "$answer" ];
    then
        echo "Too small";
        min=$guess;
    else
        echo "You win";
        break;
    fi
done

exit 0;

一開始先用 shuf(1) 指令在 minmax 間選出一個任意數,將該數存到 answer。由於這個過程是隨機的,不會預先知道 answer 的值。

整個遊戲過程寫在一個大的 while 迴圈中。在遊戲結束之前,此遊戲會不斷地玩下去,所以直接用 true 指令當成該迴圈的條件。

遊戲一開始先以提示文字 (prompt) 請使用者輸入特定範圍的數字。由於提示文字不要換行,所以用 printf(1) 取代 echo(1)。前者對文字輸出有比較精細的操作。使用 read(1) 指令讀入使用者的輸入。

輸入的資料不一定是合理的數字,所以要進行必要的檢查。第一次檢查用 grep(1) 來確認輸入是否為整數。在 grep 中加上 -P 參數就可以用 Perl 的常規表示式,會比 grep 原本的常規表示式來得好用。第二次檢查則是確認 guessminmax 的範圍內。

最後就是比對 guessanswer,確認兩者是否相等。當兩者相等,代表使用者猜到正確的數字,就以 break 指令結束遊戲迴圈。若兩者不相對,則縮小數字的範圍,這樣會比較容易遊玩。

until

until 同樣是不定次數的迭代控制結構。其虛擬碼如下:

until condition;
do
    # Run code here until `condition` is false.
done

until 迴圈會反覆運行,直到 condition 為偽才停止。由於 until 隱含著反向條件,較 while 難用,只有在語義符合時才會使用。

將先前的終極密碼遊戲改用 until 迴圈改寫如下:

#!/bin/sh

max=100;
min=1;

answer=$(shuf -i $min-$max -n 1);

game_over=false
until $game_over;
do
    printf "Please input a number between %d and %d: " $min $max;
    read -r guess;

    echo "$guess" | grep -q -P "^[+-]?\d+$"
    if [ 0 -ne $? ];
    then
        echo "Not an integer" >&2;
        continue;
    fi

    if [ "$guess" -lt "$min" ] || [ "$guess" -gt "$max" ];
    then
        echo "Invalid number" >&2;
        continue;
    fi

    if [ "$guess" -gt "$answer" ];
    then
        echo "Too large";
        max=$guess;
    elif [ "$guess" -lt "$answer" ];
    then
        echo "Too small";
        min=$guess;
    else
        echo "You win";
        game_over=true;
    fi
done

exit 0;

為了因應 until 迴圈,這裡多用個變數 game_over,讓 until 迴圈用起來更自然。除了遊戲迴圈的終止條件外,其他的指令和前一節的程式雷同,故不重覆說明。

for

for 是有特定次數的迭代控制結構。其虛擬碼如下:

for var in list;
do
    # Run code one by one.
done

shell script 沒有真正的串列,這裡的 list 僅是以分隔符隔開的字串。預設的分隔符為空白。

以下 shell script 用於 Debian 相容系統,用來自動安裝 GNUstep 的相依函式庫:

#!/bin/sh

gnustep_debs=$(cat << DEBS
gobjc gobjc++ libjpeg-dev libtiff-dev libpng-dev libicns-dev
libxml2-dev libxslt-dev libgnutls-dev libffi-dev libicu-dev
libcairo2-dev libxft-dev libavahi-client-dev flite-dev libxt-dev
libportaudio-dev wmaker portaudio19-dev make cmake gnutls-dev
DEBS
);

for deb in $gnustep_debs;
do
    if [ "$(id -u)" -eq 0 ];
    then
        apt-get install -y "$deb";
    else
        sudo apt-get install -y "$deb";
    fi
done;

exit 0;

gnustep_debs 是一個跨越多行的字串,字串內容即為套件名稱。各個套件名稱間以空白隔開。

for 迴圈每次的迭代中,會取出一個套件名稱,存到變數 deb 中。由於此 shell script 的目標是全自動安裝套件,所以在執行 apt-get 指令時搭配 -y 參數,以略過詢問使用者的過程。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。