首頁 > 軟體

shell的程式設計結構體(函數、條件結構、迴圈結構)

2020-06-16 17:09:00

本文目錄:

1.1 shell函數

1.2 條件結構:if

1.3 條件結構:case

1.4 條件結構:select

1.5 迴圈結構:for

1.6 迴圈結構:while

1.7 迴圈結構:until

1.8 exit、break、continue和return


 

1.1 shell函數

在shell中,函數可以被當作命令一樣執行,它是命令的組合結構體。可以將函數看成是一個普通命令或者一個小型指令碼。

首先給出幾個關於函數的結論:

(1).當在bash中直接呼叫函數時,如果函數名和命令名相同,則優先執行函數,除非使用command命令。例如:定義了一個名為rm的函數,在bash中輸入rm執行時,執行的是rm函數,而非/bin/rm命令,除非使用"command rm ARGS"。

(2).當前shell定義的函數只能在當前shell使用,子shell無法繼承父shell的函數定義。除非使用"export -f"將函數匯出為全域性函數。

(2).定義了函數後,可以使用unset -f移除當前shell中已定義的函數。

(3).除非出現語法錯誤,或者已經存在一個同名唯讀函數,否則函數的退出狀態碼是函數內部結構中最後執行的一個命令的退出狀態碼。

(4).可以使用typeset -f [func_name]或declare -f [func_name]檢視當前shell已定義的函數名和對應的定義語句。使用typeset -F或declare -F則只顯示當前shell中已定義的函數名。

(5).函數可以遞回,遞回層次可以無限。

函數的語法結構:

[ function ] name () compound-cmd [redirection]

上面的語法結構中定義了一個名為name的函數,關鍵字function是可選的,如果使用了function關鍵字,則name後的括號可以省略。compound-cmd是函數體,通常使用大括號{}包圍,由於歷史原因,大括號本身也是關鍵字,所以為了不產生歧義,函數體必須和大括號使用空格、製表符、換行符分隔開來。還可以指定可選的函數重定向功能,這樣當函數被呼叫的時候,指定的重定向也會被執行。

例如:定義一個名為rm的函數,該函數會將傳遞的所有檔案移動到"~/backup"目錄下,目的是替代rm命令,避免誤刪除的危險操作。

[root@linuxidc ~]# function rm () { [ -d ~/rmbackup ] || mkdir ~/rmbackup;/bin/mv -f $@ ~/rmbackup; } &>/dev/null

在呼叫rm函數時,只需是給rm函數傳遞引數即可。例如,要刪除/tmp/a.log。

[root@linuxidc ~]# rm /tmp/a.log

在執行函數時,會將執行可能輸出的資訊重定向到/dev/null中。

為了讓函數在子shell(例如指令碼)中也可以使用,使用export的"-f"選項將其匯出為全域性函數。取消函數的匯出則使用export的"-n"選項。

export -f rm
export -n rm

關於shell函數,還有幾個需要說明的知識點:

(6).shell函數也接受位置變數$0、$1、$2...,但函數的位置引數是呼叫函數時傳遞給函數的,而非傳遞給指令碼的引數。所以指令碼的位置變數和函數的位置變數是不同的,但是$0和指令碼的位置變數$0是一致的。另外,函數也接受特殊變數"$#",和指令碼的"$#"一樣,它也表示位置變數的個數。

(7).函數體內部可以使用return命令,當函數結構體中執行到return命令時將退出整個函數。return後可以帶一個狀態碼整數,即return n,表示函數的退出狀態碼,不給定狀態碼時預設狀態碼為0。

(8).函數結構體中可以使用local命令定義本地變數,例如:local i=3。本地變數只在函數內部(包括子函數)可見,函數外不可見。

 

1.2 條件結構:if

語法結構:

if test-commands1; then

    commands1;

[elif test-commands2; then

    commands2;]

...

[else

    commands3;]

fi

if的判斷很簡單,一切都以返回狀態碼是否為0為判決條件。如果test-commands1執行後的退出狀態碼為0(不是其執行結果為0),則執行commands1部分的結構體,否則如果test-commands2返回0則執行commands2部分的結構體,如果都不滿足,則執行commands3的結構體。

常見的test-commands有幾種型別:

(1).一條普通的命令。只要該命令退出狀態碼為0,則執行then後的語句體。例如:

if echo haha &>/dev/null;then echo go;fi

(2).測試語句。例如test、[]、[[]]。

if [ $((1+2)) -eq 3 ];then echo go;fi
if [[ "$name" =~ "long" ]];then echo go;fi

(3).使用邏輯運算子,包括!、&&和||。該特性主要是為普通命令而提供,因為測試語句自身就支援邏輯運算。所以,對於測試語句就提供了兩種寫法,一種是將邏輯運算子作為測試語句的一部分,一種是將邏輯運算子作為if語句的一部分。例如:

if ! id "$name" &>/dev/null;then echo "$name" miss;fi
if ! [ 3 -eq 3 ];then echo go;fi
if [ ! 3 -eq 3 ];then echo go;fi
if [ 3 -eq 3 ] && [ 4 -eq 4 ] ;then echo go;fi
if [ 3 -eq 3 -a 4 -eq 4 ];then echo go;fi
if [[ 3 -eq 3 && 4 -eq 4 ]];then echo go;fi

注意,在if語句中使用()不能改變優先順序,而是讓括號內的語句成為命令列表並進入子shell執行。因此,要改變優先順序時,需要在測試語句中完成。

 

1.3 條件結構:case

語法結構:

case word in

    [ [(] pattern [| pattern]…)

       command-list ;;]

    …

esac

sysV風格的服務啟動指令碼是shell指令碼中使用case語句最典型案例。例如:

case "$1" in
    start)
        start;;
    stop)
        stop;;
    restart)
        restart;;
    reload | force-reload)
        reload;;
    status)
        status;;
    *)
        echo $"Usage: $0 {start|stop|status|restart|reload|force-reload}"
        exit 2
esac

從上面的範例中,可以看出一些結論:

(1).case中的每個小分句都以雙分號";;"結尾,但最後一個小分句的雙分號可以省略。實際上,小分句除了使用";;"結尾,還可以使用";&"和";;&"結尾,只不過意義不同,它們用的不多,不過為了文章完整性,稍後還是給出說明。

(2).每個小分句中的pattern部分都使用括號"()"包圍,只不過左括號"("不是必須的。

(3).每個小分句的pattern支援萬用字元模式匹配(不是正則匹配模式,因此只有3種通配元字元:"*"、"?"和[...]),其中使用"|"分隔多個萬用字元pattern表示滿足其中一個pattern即可。例如"([yY] | [yY][eE][sS]])"表示即可以輸入單個字母的y或Y,還可以輸入yes三個字母的任意大小寫格式。

set -- y;case "$1" in ([yY]|[yY][eE][sS]) echo right;;(*) echo wrong;;esac

其中"set -- string_list"的作用是將輸入的string_list按照IFS分隔後分別賦值給位置變數$1、$2、$3...,因此此處是為$1賦值字元"y"。

(4).最後一個小分句使用的pattern是"*",表示無法匹配前面所有小分句時,將匹配該小分句。一般最後一個小分句都會使用"*"避免case語句無法匹配的情況,在shell指令碼中,此小分句一般用於提示使用者指令碼的使用方法,即給出指令碼的Usage。

(5).附加一個結論:如果任何模式都不匹配,該命令的返回狀態是零;否則,返回最後一個被執行的命令的返回值。

如果小分句不是使用雙分號";;"結尾,而是使用";&"或";;&"結尾,則case語句的行為將改變。

◇ ";;"結尾符號表示小分句執行完成後立即退出case語句。

◇ ";&"表示繼續執行下一個小分句中的command部分,而無需進行匹配動作,並由此小分句的結尾符號來決定是否繼續操作下一個小分句。

◇ ";;&"表示繼續向後(不止是下一個,而是一直向後)匹配小分句,如果匹配成功,則執行對應小分句中的command部分,並由此小分句的結尾符號來決定是否繼續向后匹配。

範例如下:

set -- y
case "$1" in
    ([yY]|[yY][eE][sS])
        echo yes;&
    ([nN]|[nN][oO])
        echo no;;
    (*)
        echo wrong;;
esac
yes
no

在此範例中,$1能匹配第一個小分句,但第一個小分句的結尾符號為";&",所以無需判斷地直接執行第二個小分句的"echo no",但第二個小分句的結尾符號為";;",於是直接退出case語句。因此,即使$1無法匹配第二個小分句,case語句的結果中也輸出了"yes"和"no"。

set -- y
case "$1" in
    ([yY]|[yY][eE][sS])
        echo yes;;&
    ([nN]|[nN][oO])
        echo no;;
    (*)
        echo wrong;;
esac
yes
wrong

在此範例中,$1能匹配第一個小分句,但第一個小分句的結尾符號為";;&",所以繼續向下匹配,第二個小分句未匹配成功,直到第三個小分句才被匹配上,於是執行第三個小分句中的"echo wrong",但第三個小分句的結尾符號為";;",於是直接退出case語句。所以,結果中輸出了"yes"和"wrong"。

 

1.4 條件結構:select

shell中提供選單選擇的條件判斷結構。例如:

[root@linuxidc ~]# select fname in cat dog sheep mouse;do echo your choice: "$REPLY) $fname";break;done
1) cat
2) dog
3) sheep
4) mouse
#? 3                      # 在此選擇序號3
your choice: "3) sheep"   # 將輸出序號3對應的內容

語法結構:

select name [ in word ] ; do cmd_list ; done

它的結構幾乎和for迴圈的結構相同。有以下幾個要點:

(1).in關鍵詞後的word將根據IFS變數進行分割,分割後的每一項都進行編號,作為選單序號被輸出,如果省略in word,則等價於"in $@",即將位置變數的內容作為選單項。

(2).當選擇選單序號後,該序號的內容將儲存到變數name中,並且所輸入的內容(一般是序號值,例如上面的例子中輸入的3,但不規定一定要輸入序號值,例如隨便輸入幾個字元)儲存儲存到特殊變數REPLY中。

(3).每次輸入選擇後,select語句都將重置,如果輸入的選單序號存在,則cmd_list會重新執行,變數name也將重置。如果沒有break命令,則select語句會一直執行,如果遇到break命令,將退出select語句。

仍然是上面的範例:但不是用break

[root@linuxidc ~]# select fname in cat dog sheep mouse;do echo your choice: "$REPLY) $fname";done  
1) cat
2) dog
3) sheep
4) mouse
#? 2
your choice: "2) dog"
#? habagou                    # 隨意輸入幾個字元
your choice: "habagou) "      # 變數fname被重置為空,變數REPLY被賦予了輸入的值habagou
#? 2 3
your choice: "2 3) "   
#? ^C                         # 直到殺掉進程select才結束

 

1.5 迴圈結構:for

for迴圈再shell指令碼中應用極其廣泛,它有兩種語法結構:

結構一:for name [ [ in [ word ... ] ] ; ] do cmd_list ; done

結構二:for (( expr1 ; expr2 ; expr3 )) ; do cmd_list ; done

結構一中:將擴充套件in word,然後按照IFS變數對word進行分割,並依次將分割的單詞賦值給變數name,每賦值一次,執行一次迴圈體cmd_list,然後再繼續將下一個單詞賦值給變數name,直到所有變數賦值結束。如果省略in word,則等價於"in $@",即展開位置變數並依次賦值給變數name。注意,如果word中使用引號包圍了某些單詞,這引號包圍的內容被分割為一個單詞。

例如:

[root@linuxidc ~]# for i in 1 2 3 4;do echo $i;done
1
2
3
4
[root@linuxidc ~]# for i in 1 2 "3 4";do echo $i;done
1
2
3 4

結構二中:該結構的expr部分只支援數學計算和比較。首先計算expr1,再判斷expr2的返回狀態碼,如果為0,則執行cmd_list,並將計算expr3的值,並再次判斷expr2的狀態碼。直到expr2的返回狀態碼不為0,迴圈結束。

例如:

[root@linuxidc ~]# for ((i=1;i<=3;++i));do echo $i;done
1
2
3
[root@linuxidc ~]# for ((i=1,j=3;i<=3 && j>=2;++i,--j));do echo $i $j;done
1 3
2 2

 

1.6 迴圈結構:while

使用while迴圈盡量要讓條件執行到可以退出迴圈,否則無限迴圈。一般都在命令體部分加上變數的改變行為。

語法結構:

while test_cmd_list; do cmd_list; done

首先執行test_cmd_list中的命令,當test_cmd_list的最後一個命令的狀態碼為0時,將執行一次cmd_list,然後回到回圈的開頭繼續執行test_cmd_list。只有test_cmd_list中最後一個測試命令的狀態碼非0時,迴圈才會退出。

例如:計算1到10的算術和。

[root@linuxidc ~]# let i=1,sum=0;while [ $i -le 10 ];do let sum=sum+i;let ++i;done;echo $sum         
55

在此例中,test_cmd_list中只有一個命令[ $i -le 10 ],所以它的狀態直接決定整個迴圈何時退出。

test_cmd_list中可以是多個命令,但千萬要考慮清楚,是否要讓決定退出迴圈的測試命令處在列表的尾部,否則將進入無線迴圈。

[root@linuxidc ~]# let i=1,sum=0;while echo $i;[ $i -le 10 ];do let sum=sum+i;let ++i;done;echo $sum 
1
2
3
4
5
6
7
8
9
10
11
55

對於while迴圈,有另外兩種常見的寫法:

(1).test_cmd_list部分使用一個冒號":"或者true命令,使得while進入無限迴圈。

while :;do        # 或者"while true;do"

    ...

done

(2).使用read命令從標準輸入中按行讀取值,然後儲存到變數line中(既然是read命令,所以可以儲存到多個變數中),讀取一行是一個迴圈。

由於標準輸入既可以來源於重定向,也可以來源於管道(本質還是重定向),所以有幾種常見的寫法:

寫法一:使用管道傳遞內容,這是最爛的寫法

echo "abc xyz" | while read field1 field2    # 按IFS分割,並賦給兩個變數

do 

    ...

done

寫法二:

while read line

do

    ...

done <<< "abc xyz"

寫法三:從檔案中讀取內容

while read line

do

    ...

done </path/filename

既然是讀取標准輸入,於是還可以衍生出幾種寫法:

方法四:while read var;do ...;done < <(cmd_list)          # 採用進程替換

方法五:exec <filename;while read var;do ...;done         # 改變標準輸入

儘管寫法有多種,但注意,它們並不等價。方法一中使用的是管道符號,這使得while語句在子shell中執行,這意味著while語句內部設定的變數、陣列、函數等在迴圈外部都不再生效例如:

#!/bin/bash
echo "abc xyz" | while read line
do
    new_var=$line
done
echo the variable new_var is null: $new_var?

該指令碼的執行結果中,$new_var的值將為空。

使用除寫法一外的任意一種寫法,在while迴圈外部都能繼續獲得while內的環境。例如,使用寫法二的here string代替寫法一:

#!/bin/bash
while read line
do
    new_var=$line
done <<< "abc xyz"
echo the variable new_var is null: $new_var?

由此可以發現,在上面的5種寫法中,大眾使用的最廣泛寫法一其實是最爛的一種,如果沒注意寫法一中while是在子shell執行,很可能會一直疑惑,為什麼在while迴圈裡設定好的變數或陣列在迴圈一結束就成了空值呢。

 

1.7 迴圈結構:until

until和while迴圈基本一致,所不同的僅僅只是test_cmd_list的意義。

語法結構:

until test_cmd_list; do cmd_list; done

首先判斷test_cmd_list中的最後一個命令,如果狀態碼為非0,則執行一次cmd_list,然後再返回回圈的開頭再次執行test_cmd_list,直到test_cmd_list的最後一個命令狀態碼為0時,才退出迴圈。

和while不同的是,當判斷test_cmd_list最後一個命令的狀態滿足退出條件時直接退出迴圈,也就是說迴圈是在test_cmd_list最後一個命令處退出的。

例如:

[root@linuxidc ~]# i=5;until echo haha;[ "$i" -eq 0 ];do let --i;echo $i;done
haha
4
haha
3
haha
2
haha
1
haha
0
haha

 

1.8 exit、break、continue和return

exit [n]         :退出當前shell,在指令碼中應用則表示退出整個指令碼(子shell)。其中數值n表示退出狀態碼。

break [n]     :退出整個迴圈,包括for、while、until和select語句。其中數值n表示退出的迴圈層次。

continue [n] :退出當前迴圈進入下一次迴圈。n表示繼續執行第n次迴圈。

return [n]     :退出整個函數。n表示函數的退出狀態碼。

本文永久更新連結地址http://www.linuxidc.com/Linux/2017-08/146538.htm


IT145.com E-mail:sddin#qq.com