程式扎記: [Linux 小學堂] Ubuntu : Shell 編程基礎

標籤

2012年8月13日 星期一

[Linux 小學堂] Ubuntu : Shell 編程基礎

來源自 這裡 
前言 : 
我們可以使用任意一種文字編輯器,比如gedit、kedit、emacs、vi等來編寫shell腳本,它必須以如下行開始(必須放在文件的第一行): 
#!/bin/sh
...

符號 #! 用來告訴系統如何執行該腳本的程序,本例使用 /bin/sh. 編輯結束並保存後,如果要執行該腳本,必須先使其可執行 : 
# chmod +x filename

此後在該腳本所在目錄下,輸入 ./filename 即可執行該腳本. 

變量賦值和引用 : 
Shell編程中,使用變量無需事先聲明,同時變量名的命名須遵循如下規則 : 
1. 首個字符必須為字母(az,AZ)
2. 中間不能有空格,可以使用下劃線(_)
3. 不能使用標點符號
4. 不能使用bash裡的關鍵字(可用help命令查看保留關鍵字)

要取用一個變量的值, 只需在變量名前面加一個 $ ( 注意: 給變量賦值的時候,不能在"="兩邊留空格 ) 
- test01.sh : 
  1. #!/bin/sh  
  2. # Note : Assing value to variable  
  3. a="Hello world"  # Don't keep white space around '='  
  4. # Print variable a  
  5. echo "A is: $a"  
執行結果如下 : (記得 chmod +x test01.sh
$ ./test01.sh
A is: Hello world

有時候變量名可能會和其它文字混淆,比如 : 
  1. num=2  
  2. echo "this is the $numnd"  
上述腳本並不會輸出"this is the 2nd" 而是"this is the ";這是由於shell會去搜索變量numnd的值,而實際上這個變量此時並沒有值. 這時, 我們可以用大括號來告訴 shell 要打印的是 num 變量 : 
- test02.sh : 
  1. #!/bin/sh  
  2. num=2  
  3. echo "this is the ${num}nd"  
執行結果 : 
$ ./test02.sh
this is the 2nd

需要注意shell的默認賦值是字符串賦值. 比如 : 
  1. var=1  
  2. var=$var+1  
  3. echo $var  
打印出來的不是2而是1+1! 為了達到我們想要的效果可以 : 
- test03.sh 
  1. #!/bin/sh  
  2. var1=1  
  3. var2=$(($var1+1))  
  4. echo "var1=$var1"  
  5. echo "var2=$var2"  
執行結果 : 
$ ./test03.sh
var1=1
var2=2

Shell裡的流程控制 : 

- if 語句 
"if" 表達式如果條件為真,則執行then後的部分 : 
  1. if ....; then  
  2.   ....  
  3. elif ....; then  
  4.   ....  
  5. else  
  6.   ....  
  7. fi  
大多數情況下,可以使用測試命令來對條件進行測試,比如可以比較字符串、判斷文件是否存在及是否可讀等等……通常用" [ ] "來表示條件測試,注意這裡的空格很重要,要確保方括號前後的空格 : 
[ -f "somefile" ] : 判斷是否是一個文件
[ -x "/bin/ls" ] : 判斷/bin/ls是否存在並有可執行權限
[ -n "​​$var" ] : 判斷 $var 變量是否有值
[ "$a" = "$b" ] : 判斷$a和$b是否相等

執行 man test 可以查看所有測試表達式可以比較和判斷的類型. 下面是一個簡單的 if 語句 : 
- test04.sh : 
  1. #!/bin/sh  
  2.   
  3. if [ ${SHELL} = "/bin/bash" ]; then  
  4.         echo "Your login shell is the bash."  
  5. else  
  6.         echo "Your login shell is not bash but '${SHELL}'"  
  7. fi  
變量$SHELL包含有登錄 shell 的名稱,我們拿它和 '/bin/bash' 進行比較以判斷當前使用的shell是否為bash. 

- && 和 ||操作符 : 
熟悉 C語言 的朋友可能會喜歡下面的表達式 : 
  1. [ -f "/etc/shadow" ] && echo "This computer uses shadow passwords"  
這裡的&& 就是一個快捷操作符,如果左邊的表達式為真則執行右邊的語句,你也可以把它看作邏輯運算裡的與操作。上述腳本表示如果/etc/shadow文件存在,則打印 "This computer uses shadow passwords"。同樣shell編程中還可以用或操作(||),例如 : 
- test05.sh : 
  1. #!/bin/sh  
  2.   
  3. mailfolder=/var/spool/mail/james  
  4. [ -r "$mailfolder" ] || { echo "Can not read $mailfolder" ; exit 1; }  
  5. echo "$mailfolder has mail from:"  
  6. grep "^From " $mailfolder  
該腳本首先判斷mailfolder是否可讀,如果可讀則打印該文件中的"From" 一行。如果不可讀則或操作生效,打印錯誤信息後腳本退出. 另外這邊我們使用大括號以匿名函數的形式將兩個命令放到一起作為一個命令使用;普通函數稍後再作說明. 即使不用與和或操作符,我們也可以用if表達式完成任何事情,但是使用與或操作符會更便利很多. 

- case 語句 
case表達式可以用來匹配一個給定的字符串,而不是數字(可別和C語言裡的switch...case混淆): 
  1. case ... in  
  2.    ...) do something here   
  3. esac  
file命令可以辨別出一個給定文件的文件類型,如:file lf.gz,其輸出結果為 : 
$ file test01.tar.gz
test01.tar.gz: gzip compressed data, from Unix, last modified: Mon Aug 13 18:12:26 2012

我們利用這點寫了一個名為 smart zip 的腳本,該腳本可以自動解壓 bzip2, gzip和zip 類型的壓縮文件 : 
- test06.sh : 
  1. #!/bin/sh  
  2.   
  3. ftype=`file "$1"` # Note ' and ` is different  
  4. case "$ftype" in  
  5.     "$1: Zip archive"*)  
  6.         unzip "$1" ;;  
  7.     "$1: gzip compressed"*)  
  8.         gunzip "$1" ;;  
  9.     "$1: bzip2 compressed"*)  
  10.         bunzip2 "$1" ;;  
  11.     *) echo "File $1 can not be uncompressed with smartzip";;  
  12. esac  
你可能注意到上面使用了一個特殊變量 $1,該變量包含有傳遞給該腳本的第一個參數值。也就是說,當我們運行 : 
$ ./test06.sh test01.tar.gz

$1 就是字符串"test01.tar.gz" 

- select語句 
select表達式是bash的一種擴展應用,擅長於交互式場合。用戶可以從一組不同的值中進行選擇 : 
  1. select var in ... ; do  
  2.  break;  
  3. done  
  4. .... now $var can be used ....  
下面是一個簡單的示例 : 
  1. #!/bin/sh  
  2.   
  3. echo "What is your favourite OS?"  
  4. select var in "Linux" "Gnu Hurd" "Free BSD" "Other"do  
  5.   break;  
  6. done  
  7. echo "You have selected $var"  
如果以上腳本運行出現select :NOT FOUND 將#!/bin/sh 改為 #!/bin/bash 該腳本的運行結果如下 : 
 

- while/for循環 
在shell中,可以使用如下循環 : 
  1. while ...; do  
  2.    ....  
  3. done  
只要測試表達式條件為真,則 while 循環將一直運行。關鍵字 "break" 用來跳出循環,而關鍵字 ”continue” 則可以跳過一個循環的餘下部分,直接跳到下一次循環中. 

for循環會查看一個字符串列表(字符串用空格分隔),並將其賦給一個變量 : 
  1. for var in ....; do  
  2.    ....  
  3. done  
下面是一個實用的腳本showrpm,其功能是打印一些RPM包的統計信息 : 
- test08.sh : 
  1. #!/bin/sh  
  2.   
  3. # list a content summary of a number of RPM packages  
  4. # USAGE: showrpm rpmfile1 rpmfile2 ...  
  5. # EXAMPLE: showrpm /cdrom/RedHat/RPMS/*.rpm  
  6. for rpmpackage in $*; do  
  7.    if [ -r "$rpmpackage" ];then  
  8.       echo "=============== $rpmpackage =============="  
  9.       rpm -qi -p $rpmpackage  
  10.    else  
  11.       echo "ERROR: cannot read file $rpmpackage"  
  12.    fi  
  13. done  
這裡出現了第二個特殊變量$*,該變量包含有輸入的所有命令行參數值。如果你運行./test08.sh openssh.rpm w3m.rpm webgrep.rpm,那麼 $* 就包含有3 個字符串,即 openssh.rpm, w3m.rpm和webgrep.rpm 

Shell 裡的一些特殊符號 : 
- 引號 
在向程序傳遞任何參數之前,程序會擴展通配符和變量。這裡所謂的擴展是指程序會把通配符(比如*)替換成適當的文件名,把變量替換成變量值。我們可以使用引號來防止這種擴展,先來看一個例子,假設在當前目錄下有兩個jpg文件 : mail.jpg和tux.jpg 
  1. #!/bin/sh  
  2.   
  3. echo *.jpg  
運行結果為 : 
mail.jpg tux.jpg
引號(單引號和雙引號)可以防止通配符*的擴展 : 
  1. #!/bin/sh  
  2.   
  3. echo "*.jpg"  
  4. echo '*.jpg'  
其運行結果為 : 
*.jpg
*.jpg

其中單引號更嚴格一些,它可以防止任何變量擴展;而雙引號可以防止通配符擴展但允許變量擴展 : 
- test09.sh : 
  1. #!/bin/sh  
  2.   
  3. echo $SHELL  
  4. echo '$SHELL'  
  5. echo "$SHELL"  
執行結果 : 
$ ./test09.sh
/bin/bash
$SHELL
/bin/bash

此外還有一種防止這種擴展的方法,即使用轉義字符——反斜桿 '\ ' 
  1. echo \*.jpg  
  2. echo \$SHELL  
Help Document : 
當要將幾行文字傳遞給一個命令時,用 help documents 是一種不錯的方法。對每個腳本寫一段幫助性的文字是很有用的,此時如果使用help documents就不必用echo函數一行行輸出。Help document 以 << 開頭,後面接上一個字符串,這個字符串還必須出現在help document的末尾. 下面是一個例子,在該例子中,我們對多個文件進行重命名,並且使用help documents 打印幫助 : 
- test10.sh : 
  1. #!/bin/sh  
  2.   
  3. # we have less than 3 arguments. Print the help text:  
  4. if [ $# -lt 3 ] ; then  
  5. cat << HELP  
  6.   
  7. ren -- renames a number of files using sed regular expressions USAGE: ren 'regexp' 'replacement' files...  
  8.   
  9. EXAMPLE: rename all *.HTM files in *.html:  
  10.    ren 'HTM$' 'html' *.HTM  
  11.   
  12. HELP  
  13.    exit 0  
  14. fi  
  15. OLD="$1"  
  16. NEW="$2"  
  17. # The shift command removes one argument from the list of  
  18. # command line arguments.  
  19. shift  
  20. shift  
  21. # $* contains now all the files:  
  22. for file in $*; do  
  23.    if [ -f "$file" ] ; then  
  24.       newfile=`echo "$file" | sed "s/${OLD}/${NEW}/g"`  
  25.       if [ -f "$newfile" ]; then  
  26.        echo "ERROR: $newfile exists already"  
  27.       else  
  28.          echo "renaming $file to $newfile ..."  
  29.          mv "$file" "$newfile"  
  30.       fi  
  31.    fi  
  32. done  
這個示例有點複雜,我們需要多花點時間來說明一番。第一個 if 表達式判斷輸入命令行參數是否小於3個(特殊變量$# 表示包含參數的個數) . 如果輸入參數小於3個,則將幫助文字傳遞給 cat 命令,然後由 cat 命令將其打印在屏幕上。打印幫助文字後程序退出。如果輸入參數等於或大於3個,我們就將第一個參數賦值給變量 OLD,第二個參數賦值給變量 NEW。下一步,我們使用 shift 命令將第一個和第二個參數從參數列表中刪除,這樣原來的第三個參數就成為參數列表 $* 的第一個參數。然後我們開始循環,命令行參數列表被一個接一個地被賦值給變量 $file。接著我們判斷該文件是否存在,如果存在則通過 sed 命令搜索和替換來產生新的文件名。然後將反短斜線內命令結果賦值給 newfile。這樣我們就達到了目的 : 得到了舊文件名和新文件名。然後使用 mv 命令進行重命名. 

Shell 裡的函數 : 
如果你寫過比較複雜的腳本,就會發現可能在幾個地方使用了相同的代碼,這時如果用上函數,會方便很多。函數的大致樣子如下 : 
  1. functionname()  
  2. {  
  3. # inside the body $1 is the first argument given to the function  
  4. # $2 the second ...  
  5. body  
  6. }  
你需要在每個腳本的開始對函數進行聲明. 下面腳本可以改變終端窗口的名稱。這裡使用了一個名為help的函數,該函數在腳本中使用了兩次 : 
- test11.sh : 
  1. #!/bin/sh  
  2.   
  3. help()  
  4. {  
  5. cat << HELP  
  6. xtitlebar -- change the name of an x<200b><200b>term, gnome-terminal or kde konsole  
  7. USAGE: xtitlebar [-h] "string_for_titelbar"  
  8. OPTIONS: -h help text  
  9. EXAMPLE: xtitlebar "cvs"  
  10. HELP  
  11. exit 0  
  12. }  
  13. # in case of error or if -h is given we call the function help:  
  14. [ -z "$1" ] && help  
  15. "$1" = "-h" ] && help  
  16. # send the escape sequence to change the xterm titelbar:  
  17. echo -e "\033]0;$1\007"  
在腳本中提供幫助是一種很好的編程習慣,可以方便其他用戶(和自己)使用和理解腳本. 

命令行參數 : 
我們已經見過 $* 和 $1, $2 ... $9 等特殊變量,這些特殊變量包含了用戶從命令行輸入的參數。迄今為止,我們僅僅了解了一些簡單的命令行語法(比如一些強制性的參數和查看幫助的-h選項)。但是在編寫更複雜的程序時,您可能會發現您需要更多的自定義的選項。通常的慣例是在所有可選的參數之前加一個減號,後面再加上參數值 (比如文件名). 

有好多方法可以實現對輸入參數的分析,但是下面的使用ca​​se表達式的例子無疑是一個不錯的方法 : 
- test12.sh : 
  1. #!/bin/sh  
  2.   
  3. help()  
  4. {  
  5.    cat << HELP  
  6.    This is a generic command line parser demo.  
  7.    USAGE EXAMPLE: cmdparser -l hello -f -- -somefile1 somefile2  
  8. HELP  
  9.    exit 0  
  10. }  
  11.   
  12. while [ -n "$1" ]; do  
  13. case $1 in  
  14.    -h) help;shift 1;; # function help is called  
  15.    -f) opt_f=1;shift 1;; # variable opt_f is set  
  16.    -l) opt_l=$2;shift 2;; # -l takes an argument -> shift by 2  
  17.    --) shift;break;; # end of options  
  18.    -*) echo "error: no such option $1. -h for help";exit 1;;  
  19.    *) break;;  
  20. esac  
  21. done  
  22.   
  23. echo "opt_f is $opt_f"  
  24. echo "opt_l is $opt_l"  
  25. echo "first arg is $1"  
  26. echo "2nd arg is $2"  
你可以如下運行 : 
$ ./test12.sh -l hello -f -- -somefile1 somefile2
opt_f is 1
opt_l is hello
first arg is -somefile1
2nd arg is somefile2

這個腳本是如何工作的呢?腳本首先在所有輸入命令行參數中進​​行循環,將輸入參數與case表達式進行比較,如果匹配則設置一個變量並且移除該參數。根據unix系統的慣例,首先輸入的應該是包含減號的參數. 

腳本調試 : 
最簡單的調試方法當然是使用echo命令。你可以在任何懷疑出錯的地方用echo打印變量值,這也是大部分shell程序員花費80%的時間用於調試的原因。Shell腳本的好處在於無需重新編譯,而插入一個echo命令也不需要多少時間. 

shell也有一個真正的調試模式,如果腳本"test13.sh"出錯,可以使用如下命令進行調試 : 
$ sh -x test13.sh

上述命令會執行該腳本,同時顯示所有變量的值與執行過程. 

shell還有一個不執行腳本只檢查語法的模式,命令如下 : 
$ sh -x test13.sh

這個命令會返回所有語法錯誤. 

補充說明 : 
鳥哥 Linux 私房菜 : 第十三章、學習 Shell Scripts 

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!