shell的編程結構體(函數、條件結構、循環結構)


bash&shell系列文章:http://www.cnblogs.com/f-ck-need-u/p/7048359.html


1.1 shell函數

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

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

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

(2).如果函數名和命令別名同名,則優先執行別名。也就是說,在優先級方面:別名>函數>命令自身

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

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

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

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

(7).函數可以遞歸,遞歸層次可以無限。

函數的語法結構:

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

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

上面的語法結構中定義了一個名為name的函數:

  • 關鍵字function是可選的,如果使用了function關鍵字,則name后的括號可以省略。
  • compound-cmd是函數體,通常使用大括號{}包圍。由於歷史原因,大括號本身也是關鍵字,所以為了不產生歧義,函數體和大括號之間必須使用空格、制表符、換行符分隔開來。
  • 同理,大括號中的每一個命令都必須使用分號";"、"&"結束或換行書寫。如果使用"&"結束某條命令,這表示該命令放入后台執行。
  • 還可以指定可選的函數重定向功能,這樣當函數被調用的時候,指定的重定向也會被執行。

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

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

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

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

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

為了讓函數在子shell(例如腳本)中也可以使用,使用export的"-f"選項將其導出為全局函數。取消函數的導出則使用export的"-n"選項。

export -f rm
export -n rm

另外需要注意的是,函數支持無限遞歸。這可能在不經意間出錯,導致崩潰。例如,寫一個名為"ls"的函數。

function ls() { ls -l; }

這時執行ls命令會卡住,和想象中的"ls -l"效果完全不同,因為函數體中的ls也遞歸成了函數,這將無限遞歸下去。

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

(8).shell函數也接受位置變量$0、$1、$2...,但函數的位置參數是調用函數時傳遞給函數的,而非傳遞給腳本的參數。所以腳本的位置變量和函數的位置變量是不同的,但是$0和腳本的位置變量$0是一致的。另外,函數也接受特殊變量"$#",和腳本的"$#"一樣,它也表示位置變量的個數。

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

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

(11).只有先定義了函數,才可以調用函數。不允許函數調用語句在函數定義語句之前。

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@xuexi ~]# 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@xuexi ~]# 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@xuexi ~]# for i in 1 2 3 4;do echo $i;done
1
2
3
4
[root@xuexi ~]# 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@xuexi ~]# for ((i=1;i<=3;++i));do echo $i;done
1
2
3
[root@xuexi ~]# 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@xuexi ~]# 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@xuexi ~]# 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?

如果沒注意寫法一中while是在子shell運行,很可能會一直疑惑,為什么在while循環里設置好的變量或數組在循環一結束就成了空值呢。

陷阱二:

關於這幾種while循環的寫法,還有一點要注意:寫法一和寫法四傳遞數據的源都是一個單獨的進程,它們傳遞的數據一被while循環讀取,所有數據就丟棄了,而以實體文件作為重定向傳遞的數據,while讀取了之后並不會丟棄。更標准一些的說法是,當標准輸入是非實體文件時(如管道傳遞的、獨立進程產生的)只供一次讀取;當標准輸入是直接重定向實體文件時,可供多次讀取,但只要某一次讀取了該文件的全部內容就無法再提供讀取。

舉個例子,老師讓我們聽寫10個單詞,而我記憶力比較爛,他念完10個單詞時我可能只寫出了3個,剩余的7個因為記不住就沒法再寫出來。但如果我有小抄,我就可以慢悠悠的一個一個寫,寫了一個還可以等一段時間再寫第二個,但當我寫完10個之后,小抄這種東西就應該銷毀掉。

回到IO重定向上,無論什么數據資源,只要被讀取完畢或者主動丟棄,那么該資源就不可再得。①對於獨立進程傳遞的數據(管道左側進程產生的數據、進程替換產生的數據),它們都是"虛擬"數據,要不被一次讀取完畢,要不讀一部分剩余的丟棄,這是真正的一次性資源。②而實體文件重定向傳遞的數據,只要不是一次性被全部讀取,它就是可再得資源,直到該文件數據全部讀取結束,這是"偽"一次性資源。其實①是進程間通信時數據傳遞的現象,只不過這個問題容易被人忽略。

大多數時候,獨立進程傳遞的數據和文件直接傳遞的數據並沒有什么區別,但有些命令可以標記當前讀取到哪個位置,使得下次該命令的讀取動作可以從標記位置處恢復並繼續讀取,特別是這些命令用在循環中時。據我到目前的總結,這樣的命令有"head -n N"和"grep -m",經測試,tail並沒有位置標記的功能,因為tail讀取的是后幾行,所以它必然要讀取到最后一行並計算要輸出的行,所以tail的性能比head要差。

說了這么多,現在終於開始驗證。下面的循環中,本該head每次讀取2行,但實際執行結果中總共就只讀取了一次2行。

[root@xuexi ~]# i=0
[root@xuexi ~]# cat /etc/fstab | while head -n 2 ; [[ "$i" -le 3 ]];do echo $i;let ++i;done     

#
0
1
2
3

使用進程替換的結果是一樣的。

[root@xuexi ~]# i=0
[root@xuexi ~]# while head -n 2; [[ "$i" -le 3 ]];do echo $i;let ++i;done < <(cat /etc/fstab)

#
0
1
2
3

但如果是直接將實體文件進行重定向傳遞給head,則結果和上面的不一樣。

[root@xuexi ~]# i=0;while head -n 2 ; [[ "$i" -le 3 ]];do echo $i;let ++i;done < /etc/fstab

#
0
# /etc/fstab
# Created by anaconda on Thu May 11 04:17:44 2017
1
#
# Accessible filesystems, by reference, are maintained under '/dev/disk'
2
# See man pages fstab(5), findfs(8), mount(8) and/or blkid(8) for more info
#
3
UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 /                       xfs     defaults        0 0
UUID=367d6a77-033b-4037-bbcb-416705ead095 /boot                   xfs     defaults        0 0

可以看到結果中每次讀取兩行並echo一次"$i",而且每次讀取的兩行是不同的,后一次讀取的兩行是從前一次讀取結束的地方開始的,這是因為head有"讀取到指定行數后做上位置標記"的功能。

要確定命令、工具是否具有做位置標記的能力,只需像下面例子一樣做個簡單的測試。以head和sed為例,即使sed的"q"命令能讓sed匹配到內容就退出,但卻不做位置標記,而且數據資源使用一次就丟棄,所以sed測試中,第二個sed完全是廢的,因為/etc/fstab這個資源在被第一個sed讀取后就丟掉了。

[root@xuexi ~]# (head -n 2;head -n 2) </etc/fstab 

#
# /etc/fstab
# Created by anaconda on Thu May 11 04:17:44 2017
[root@xuexi ~]# (sed -n /default/'{p;q}' ;sed -n /default/'{p;q}') </etc/fstab     
UUID=b2a70faf-aea4-4d8e-8be8-c7109ac9c8b8 /                       xfs     defaults        0 0

其實在實際應用過程中,這根本就不是個問題,因為搜索和處理文本數據的工具雖然不少,但絕大多數都是用一次文本就"丟"一次,幾乎不可能因此而產生問題。之所以說這么多廢話,主要是想說上面的5種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時,才退出循環。

當判斷test_cmd_list最后一個命令的狀態滿足退出條件時直接退出循環,也就是說循環是在test_cmd_list最后一個命令處退出的。

例如:

[root@xuexi ~]# 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層的循環。默認n=1,表示繼續當前層的下一循環,n=2表示繼續上一層的下一循環。

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

唯一需要注意的是,return並非只能用於function內部,絕大多數人都有這樣的誤解。如果return用在function之外,但在  .  或者 source 命令的執行過程中,則直接停止該執行操作,並返回給定狀態碼n(如果未給定,則為0)。如果return在function之外,且不在source或" . "的執行過程中,則這是一個錯誤用法。

[root@xuexi ~]# return
-bash: return: can only `return' from a function or sourced script

可能有些人不理解為什么不直接使用exit來替代這時候的return。下面給個例子就能清楚地區分它們。

#!/bin/bash

if [ "$1" = "exit" ];then 
        echo "exit current shell..."
        exit 0
else 
        echo "return 0"
        return 0
fi

 當執行 source c.sh 的時候,直接return,而當給定exit參數,即 source c.sh exit 的時候,將直接退出當前shell。

如果了解source的特性"在當前shell而非子shell執行指定腳本中的代碼"的話,就能理解為什么會這樣。

可能你想象不出在source執行中的return有何用處。從source來考慮,它除了用在某些腳本中加載其他環境,更主要的是在bash環境初始化腳本中使用,例如/etc/profile、~/.bashrc等,如果你在/etc/profile中用exit來替代function外面的return(想象一下將上面c.sh中的"return 0"換成"exit 0",然后在profile中source這個文件),那么你永遠也登陸不上bash。

以下是/etc/profile.d/proxy.sh的內容,用於看情況設置代理的環境變量。

proxy="http://127.0.0.1:8118"
function exp_proxy() {
        export http_proxy=$proxy
        export https_proxy=$proxy
        export ftp_proxy=$proxy
        export no_proxy=localhost
}

case $1 in 
        set) exp_proxy;;
        unset) unset http_proxy https_proxy ftp_proxy no_proxy;;
        *) return 0
esac

當進入bash時,什么代理環境變量都不會設置。如果需要設置,使用 source /etc/profile.d/proxy.sh set 即可,如果想取消設置,使用unset參數即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM