進階Bash腳本程式設計指南
一本深入學習shell腳本藝術的書籍
版本 6.0.35
2009年6月29日
作者Mendel Cooper
該教程假設你以前沒有腳本知識或者程式設計知識,但是如果你具備該知識的話很快就能達到中級或者進階水準。整個書中涵蓋了UNIX®的特有智慧和知識。你可以把它當成自學教材、手冊或者shell腳本技術的參考資料。練習題和例子中的注釋能夠引起讀者的積極性,前提是真正學習腳本的唯一途徑就是寫腳本。
本書也适合作為教材來講解一般的介紹程式設計概念。
第一部分:介紹
Shell是一種指令解釋器。它不僅僅是作業系統核心和使用者之間的連接配接器,更是功能強大的程式設計語言。一個shell程式,被叫做一個腳本,是一個很容易使用的工具,可以通過系統調用、工具、實用程式或者編譯過的二進制連接配接在一起。幾乎所有的UNIX指令、實用程式和工具都可以通過shell腳本調用。如果這些還不夠,那麼shell本身類似于testing和loop循環結構的指令可以增加腳本的強力支援及靈活性。Shell腳本更加适合管理系統程序以及其他重複工作的程序,而不需要那些多餘的完全成熟的結構化變成語言。
目錄
1. Shell Programming!
2. Starting Off With a Sha-Bang
第一章 shell 程式設計
對于任何想精通系統管理的人來說,掌握shell腳本知識是必不可少的,即使他們之前沒有真正的寫過腳本。想想Linux機器的啟動過程,它是通過執行/etc/rc.d 目錄下的腳本去恢複系統配置及建立相關服務的。詳細的了解這些啟動腳本對于分析系統是非常重要的,同時還可能需要修改它。
掌握腳本并不難,因為這些腳本都可以分割成小的片段去學習,并且這些小的片段又是相對獨立的操作[1]。Shell文法很簡單也很直覺,類似于把一些實用程式在指令行連接配接起來調用,而且隻用到很少的規則。絕大多少短小的腳本第一次就可以很好的運作,即使調試一個比較長的腳本也是比較直覺的。
在20世紀70年代, BASIC語言适合在早期的微機上編寫程式。10年後,Bash腳本作為Linux或者UNIX的基礎知識被用在更強大的機器上。
Shell腳本在複雜的應用程式模型設計的簡捷方法。項目開發的第一階段,使用腳本完成部分功通常都是很有用的。使用這種方法,在使用C,C++,Java,Perl或者Python語言完成程式編碼之前,通過對應用程式結構測試、示範就能夠反映程式的主要缺陷。
Shell腳本遵循經典的UNIX體系,把複雜的項目拆分成簡單的單元,并且元件和程式之間互相連接配接。序貫觀點認為這種方法比較好,至少比使用新一代語言更加完美的解決問題,例如Perl語言,就是嘗試所有人用之去做所有事情,但是代價就是強迫你使用這種語言思考解決問題的方法。
根據Herbert Mayer的理論,“一種有用的語言需要數組、指針、以及泛型結構來建立資料結構”。根據這個标準,Shell腳本就不那麼“有用”,或許不能……
我們将開始用Bash,Bourne-Again Shell的首字母縮寫組合,也是Stephen Bourne的經典Bourne Shell。Bash已經成了主流UNIX的shell腳本,本書絕大部分原則涵蓋了其他shell,比如Korn Shell,Bash也包含了一些Korn Shell的特性,同時也包含了一些C Shell的變種。(注意C Shell程式設計由于存在内在的問題不被推薦,這在1993年10月已經由Tom Christiansen 在網絡上公告了)
第二章 與Sha-Bang一起出發
一個簡單的例子,一個腳本無非是将一些系統指令列在一個檔案中。最起碼的用處就是,在調用特殊的有順序的指令時,可以節省工作。
例 2-1 清除:一個清除/var/log 目錄下日志檔案的腳本
1 # Cleanup 2 # Run as root, of course. 3 4 cd /var/log 5 cat /dev/null > messages 6 cat /dev/null > wtmp 7 echo "Logs cleaned up." |
這沒有什麼異常的,隻不過是在控制台指令行或者終端視窗一個接一個的調用一些指令,好處是不用每次都去重新敲指令。這個腳本就是一個程式——一個工具——能夠很容易修改或者定制。
例 2-2 清除:一個改進的清除腳本
1 #!/bin/bash 2 # Proper header for a Bash script. 3 4 # Cleanup, version 2 5 6 # Run as root, of course. 7 # Insert code here to print error message and exit if not root. 8 9 LOG_DIR=/var/log 10 # Variables are better than hard-coded values. 11 cd $LOG_DIR 12 13 cat /dev/null > messages 14 cat /dev/null > wtmp 15 16 17 echo "Logs cleaned up." 18 19 exit # The right and proper method of "exiting" from a script. |
現在讓我們開始看一個真正的腳本,這樣我們能夠做更多事情……
例 2-3 清除:一個增強的和普遍的清除腳本的版本
1 #!/bin/bash 2 # Cleanup, version 3 3 4 # Warning: 5 # ------- 6 # This script uses quite a number of features that will be explained 7 #+ later on. 8 # By the time you've finished the first half of the book, 9 #+ there should be nothing mysterious about it. 10 11 12 13 LOG_DIR=/var/log 14 ROOT_UID=0 # Only users with $UID 0 have root privileges. 15 LINES=50 # Default number of lines saved. 16 E_XCD=86 # Can't change directory? 17 E_NOTROOT=87 # Non-root exit error. 18 19 20 # Run as root, of course. 21 if [ "$UID" -ne "$ROOT_UID" ] 22 then 23 echo "Must be root to run this script." 24 exit $E_NOTROOT 25 fi 26 27 if [ -n "$1" ] 28 # Test whether command-line argument is present (non-empty). 29 then 30 lines=$1 31 else 32 lines=$LINES # Default, if not specified on command-line. 33 fi 34 35 36 # Stephane Chazelas suggests the following, 37 #+ as a better way of checking command-line arguments, 38 #+ but this is still a bit advanced for this stage of the tutorial. 39 # 40 # E_WRONGARGS=85 # Non-numerical argument (bad argument format). 41 # 42 # case “$1” in 43 # “” ) lines=50 44 # *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;; 45 # * ) lines=$1;; 46 # esac 47 # 48 #* Skip ahead to "Loops" chapter to decipher all this. 49 50 51 cd $LOG_DIR 52 53 if [ `pwd` != "$LOG_DIR" ] # or if [ "$PWD" != "$LOG_DIR" ] 54 # Not in /var/log? 55 then 56 echo "Can't change to $LOG_DIR." 57 exit $E_XCD 58 fi # Doublecheck if in right directory before messing with log file. 59 60 # Far more efficient is: 61 # 62 # cd /var/log || { 63 # echo "Cannot change to necessary directory." >&2 64 # exit $E_XCD; 65 # } 66 67 68 69 70 tail -n $lines messages > mesg.temp # Save last section of message log file. 71 mv mesg.temp messages # Becomes new log directory. 72 73 74 # cat /dev/null > messages 75 #* No longer needed, as the above method is safer. 76 77 cat /dev/null > wtmp # ': > wtmp' and '> wtmp' have the same effect. 78 echo "Logs cleaned up." 79 80 exit 0 81 # A zero return value from the script upon exit indicates success 82 #+ to the shell. |
因為你也不希望清除所有的系統日志,是以這個改進的腳本保留了最後的部分完整日志。你可以不斷的發現新的方法完善上面的腳本,提高效力。
***
腳本開頭的#!符号告訴系統這個檔案的指令需要指令解釋器來解釋。#!實際上就是1個2位元組的幻數,表示這一個特殊的标記,表明該檔案類型,或者表示本例是可知性shell腳本(鍵入man magic 來擷取詳更詳細的豐富的主題)。緊跟這sha-bang的是路徑名,這個路徑在腳本中是解釋指令是否是一個shell,或者是一個程式設計語言,或者是實用程式。然後指令解釋器從第一行(sha-bang下面的一行)開始執行腳本中的指令,執行過程中忽略注釋。
1 #!/bin/sh 2 #!/bin/bash 3 #!/usr/bin/perl 4 #!/usr/bin/tcl 5 #!/bin/sed -f 6 #!/usr/awk -f |
2.1 調用腳本
腳本寫好後,可以通過 sh 腳本名 或者 bash 腳本名 來調用它(不推薦使用 sh <腳本名 調用,因為不能有效的讀取腳本中的标準輸入)更有效的方法是直接賦予腳本本身可執行權限。
或者:
chmod 555 scriptname (賦予任何人 讀/執行 權限)
或者:
chmod +rx scriptname(賦予任何人 讀/執行 權限)
chmod u+rx scriptname (賦予腳本自身讀/執行 權限)
當腳本是可執行的,就可以通過 ./腳本名字 來測試它。若果腳本是以“sha-bang”行開始的,調用腳本需要正确的指令解釋器來運作。
最後一步,測試和調試之後你可能想把它移到/usr/local/bin目錄(當然,移動需要以root使用者才可以),這樣的話腳本就可以在系統中被所有使用者執行,然後使用者就可以通過在指令行簡單鍵入腳本名 [回車] 來調用腳本。
2.2 初步練習
1. 系統管理者常常需要寫腳本使得系統一些公共程序能夠自動執行,舉一些例子說明這類腳本的用途。
2. 寫一個腳本調用一些程式顯示出系統的日期和時間,列出所有登入過的使用者,顯示出系統的正常運作時間,然後腳本把這些資訊儲存到日志檔案。
第二部分 基礎知識
目錄
3. 特殊字元
4. 變量和參數介紹
5. 引用
6. 退出和退出狀态
7. 測試
8. 操作符相關主題
第三章 特殊字元
什麼是特殊字元?如果按照字面意思是一個詞轉意了,然後就成了特殊字元。
在腳本或其他地方找特殊字元
#
注釋,以 # 符号開始的行是注釋(#!是例外),不會被執行
1 # This line is a comment. |
注釋也可以跟在指令結尾後面
1 echo "A comment will follow." # 注釋. 2 # ^ 注意 #前面有空格 |
注釋也可以在行的空格後面
1 # A tab precedes this comment. |
注釋也可能嵌在管道符裡面
1 initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '/n' |/ 2 # Delete lines containing ‘#’ 注釋字元 3 sed –e `s//.//. /g’ –e ‘s/_/_ /g’` ) 4 # Excerpted from life.sh script |
同一行上指令不能跟在注釋後面,沒有辦法結束注釋接着開始新的指令,要開始新指令就需要另起一行。
當然在echo語句中,引用或者轉碼中的“#”不能作為注釋的。同樣的,#号出現在特定的參數替換結構中和數字常量表達式中,也不作為注釋的。
1 echo "The # 不是注釋" 2 echo 'The # 不是注釋' 3 echo The /# 不是注釋 4 echo The # 注釋 5 6 echo ${PATH#*:} # 參數替換,不是注釋 7 echo $(( 2#101011 )) # 基數轉換,不是注釋 8 9 # Thanks, S.C. |
标準的應用字元和轉意字元(“ ‘ /)也可以轉意“#”号,固定的比對操作也可以用“#”号。
;
指令分隔符【分号】。允許兩條或者多條指令在同一行。
1 echo hello; echo there 2 3 4 if [ -x "$filename" ]; then # 注意分号後面的空格 5 #+ ^^ 6 echo "File $filename exists."; cp $filename $filename.bak 7 else # ^^ 8 echo "File $filename not found."; touch $filename 9 fi; echo "File test complete." |
注意“;”有時候需要被轉意。
;;
終止選擇【雙分号】
1 case “$variable” in 2 abc) echo “/$variable = abc” ;; 3 xyz) echo “/$variable = xyz” ;; 4 esac |
;;&,;&
終止選擇【第4版以後的Bash】
.
“.”指令【句号】,相當于來源【參考例子14-22】,這是一個bash内建函數。
“.”作為檔案名的組成部分,是隐藏檔案的字首,當執行ls指令時不能顯示出檔案名。
[[email protected] tmp]# touch .hidden-file [[email protected] tmp]# ls -l total 100 -rw------- 1 root root 9248 Jul 13 14:34 grub-install.img.fV3362 -rw------- 1 root root 0 Jul 13 14:34 grub-install.log.vE3363 -rw-r--r-- 1 root root 73406 Aug 18 2008 netconfig-0.8.24-1.2.2.1.i386.rpm [[email protected] tmp]# [[email protected] tmp]# ls -al total 136 drwxrwxrwx 4 root root 4096 Jul 14 16:35 . drwxr-xr-x 25 root root 4096 Jul 9 11:57 .. drwxrwxrwt 2 root root 4096 Jan 9 2002 .font-unix -rw------- 1 root root 9248 Jul 13 14:34 grub-install.img.fV3362 -rw------- 1 root root 0 Jul 13 14:34 grub-install.log.vE3363 -rw-r--r-- 1 root root 0 Jul 14 16:35 .hidden-file drwxrwxrwt 2 root root 4096 Jan 9 2002 .ICE-unix -rw-r--r-- 1 root root 73406 Aug 18 2008 netconfig-0.8.24-1.2.2.1.i386.rpm [[email protected] tmp]# |
作為目錄名,“.”表示目前目錄,“..”則表示上一級目錄
bash$ pwd /home/bozo/projects bash$ cd . bash$ pwd /home/bozo/projects bash$ cd .. bash$ pwd /home/bozo/ |
“.”經常出現在移動指令中,由于這個原因,“.”表示的是目前目錄。
bash$ cp /home/zozo/current_work/junk/* . |
把junk目錄下所有檔案複制到$PWD目錄(即目前目錄)。
. 字元比對。“.”作為正規表達式的一部分,比對單個字元。
“
部分引用【雙引号】。“STRING”保留了STRING中大多數特殊字元,詳細見第5章。
‘
全部引用【單引号】。‘STRING’保留了STRING中的所有特殊字元,‘STRING’的引用是“STRING”的較強形式。參見第5章。
,
逗号操作符。逗号操作符連接配接着連續的算術操作,所有的都是可以計算的,隻有最後一個被傳回。
1 let “t2 = ((a=9,15/3()” #set “a = 2 9” and “t2 = 15 / 3” |
逗号操作符也可以連接配接字元串。
1 for file in /{,usr/}bin/*calc 2 # ^ 查找所有以“calc”結尾的可執行檔案 3 #+ 在 /bin 目錄和 /usr/bin 目錄 4 do 5 if [ -x “$file” ] 6 then 7 echo $file 8 fi 9 done 10 下面是查找結果 11 # /bin/ipcalc 12 # /usr/bin/kcalc 13 # /usr/bin/oidcalc 14 # /usr/bin/oocalc 15 16 17 # Thank you, Rory Winston, for pointing this out. |
” ’
參數替代小寫字母(第4版bash中增加)。
/
換碼【反斜杠】。單字元引用
/x 引用x 字元,它引用x 等同于‘X’。“/”常常被“ 和‘ 引用,是以這些表達簡潔。
參考第5章,更詳細的說明反斜杠。
/
檔案名路徑分隔符【向前斜線】。分隔開檔案名名字中的各級路徑(如/home/bozo/projects/Makefile)。
這也是算數運算中的除法操作。
`
指令替換。`command` 結構把指令正确的輸出給變量。這也是通常所說的backquotes或者backticks。
:
空指令【冒号】。這個shell指令等同于“NOP”(沒有操作,一個空操作)。它通常被認為是shell内建的“真”的同義詞,“:”指令是bash内部的函數,退出狀态是true(即0)。
1 : 2 Echo $? # 0 |
死循環,例如:
1 while : 2 do 3 operation-1 4 operation-2 5 … 6 operation –n 7 done 8 9 # 與下面相同 10 # while true 11 # do 12 … 13 done |
在if/then 例子中占位符
1 if condition 2 then : # Do nothing and branch ahead 3 else # or else … 4 take-some-action 5 fi |
在預期的二進制操作中提供占位符。
1 : ${username=`whoami `} 2 # ${username=`whoami `} 如果不以:開始就報錯 3 # 除非“username”是一個指令或者内建函數 … |
用替換參數計算變量中的字元串(例 9-16)。
1 :${HOSTNAME} ${USER?} ${MAIL?} 2 # Prints errot message 3 #+ if one or more essential environmental variables not set. |
變量擴充/參數替換
在和重定向符号>相結合時,在不改變檔案權限的情況下清空檔案内容,如果檔案不存在則建立檔案。
1 : > data.xxx # “data.xxx” 檔案變成空 2 3 # 效果同 cat /dev/null > data.xxx # 然而這不能 産生一個新程序,因為“:”是内建函數 |
也可以參考例 15-15
在和重定向操作符>>連接配接時,對已有的目标檔案不起作用(: >> target_file),如果檔案不存在則建立檔案。
這使用語規則的檔案,不包含管道符、連接配接符和某些特殊的檔案。
也可能用在注釋行的開始,盡管這中情況不被推薦。使用#注釋,不對該行剩餘内容錯誤檢測,是以注釋内容可以寫任何東西。然而,使用“:”情況則不同:
1 : This is a comment that generates an error, ( if [ 4x –eq 3 ] ). |
“:”在/etc/passwd中和在$PATH變量中是字段分隔符。
Bash$ echo $PATH /usr/local/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/sbin:/usr/games |
!
取反(或否定)。!操作反置了适當指令的退出狀态(參考例6-2),意思剛好和實驗的意思相反。舉個例子,它能夠将等于(=)變成不等于(!=),!是Bash腳本的一個關鍵字。
在不同的環境中,!還用在變量引用中。
在指令行中,!還能夠調用曆史記錄,注意,在腳本中曆史記錄是隐藏的。
*
通用字元【星号】。*作為通配符用在檔案名的替換中,替換給定目錄的所有檔案明。
Bash$ echo * abs-book.sgml add-drive.sh agram.sh alias.sh |
在正規表達式中,*可以替代任何數字字元(或者0)。
*
算數運算。在算數運算中,*表示乘法運算。
雙星代表幂運算或者在檔案名替換中代表補充的檔案名。
?
假設操作。在某些表達式中,?代表假設條件。
在雙括号結構中,?可以作為C語言風格中的三元操作元素,如 ?:
1 (( vr0 = var1<98?9:21 )) 2 # ^ ^ 3 4 # if [ “$var1” –lt 98 ] 5 # then 6 # var0=9 7 # else 8 # var0=21 9 # fi |
在 參數替換中,?表示假設變量是否是設定的。
?
通配符。在檔案名替換中,?代表一個單個的字元,也代表正規表達式中的一個字元。
$
變量替換。(屬于變量)
1 var1=5 2 var2=23skidoo 3 4 echo $var1 # 5 5 echo $var2 # 23skidoo |
一個變量名字的$字首表示跟着的是變量的值。
$
行尾。在正規表達式中,文本的末尾常常有個$符号。
${}
替換參數。
$*,[email protected]
定位參數。
$?
退出變量。$?變量保持一個指令、一個函數或者腳本本身處于退出狀态。
$$
程序式号。$$變量儲存了一個程序出現在腳本中的序号。
()
指令組。
1 (a=hello; echo $a) |
括号中的指令清單作為subshell運作。
括号中的變量,隻在内subshell中運作,對括号外的腳本程式不起作用,父程序,也就是腳本本身不能讀取子程序中建立的變量,也就是在subshell中建立的變量。
1 a=123 2 ( a=321; ) 3 4 echo "a = $a" # a = 123 5 # "a" within parentheses acts like a local variable. |
數組初始化。
1 Array=(element1 element2 element3) |
{xxx,yyy,zzz,…}
大括号。
1 echo /"{These,words,are,quoted}/" # " 字首和字尾 2 # 輸出的結果如 "These" "words" "are" "quoted" 3 4 5 cat {file1,file2,file3} > combined_file 6 # 将file1, file2, file3合并成combined_file. 7 8 cp file22.{txt,backup} 9 # 複制"file22.txt" 到"file22.backup" |
一個指令可能對大括号中以逗号分開的檔案清單規格起作用[3]。檔案名擴充(檔案名替換)适用于大括号中的檔案規格。
大括号中不允許有空格,除非括号中的空格是引用或者轉碼。
echo {file1,file2}/ :{/ A," B",' C'}
file1 : A file1 : B file1 : C file2 : A file2 : B file2 : C #上面語句的結果
{a..z}
大括号擴充。
1 echo {a..z} # 結果是 a b c d e f g h i j k l m n o p q r s t u v w x y z 2 # 傳回 a 到 z 之間的字母 3 4 echo {0..3} # 0 1 2 3 5 # 傳回 0 到 3 之間的數字 |
{}
代碼塊【花括号】。也被當作構造函數組,事實上建立了一個匿名函數(一個沒有名字的函數)。然而,與“标準”函數不同的是,代碼塊中的變量對于腳本來說還是可見的。
bash$ { local a; a=123; } bash: local: can only be used in a function |
1 a=123 2 { a=321; } 3 echo "a = $a" # a = 321 結果是代碼組裡面的 4 5 # Thanks, S.C. |
大括号中的代碼輸入/輸出重定向。
例 3-1 代碼 I/O 重定向
1 #!/bin/bash 2 # 從 /etc/fstab中讀行 3 4 File=/etc/fstab 5 6 { 7 read line1 8 read line2 9 } < $File 10 11 echo "First line in $File is:" 12 echo "$line1" 13 echo 14 echo "Second line in $File is:" 15 echo "$line2" 16 17 exit 0 18 19 # 現在考慮一下,你怎樣分析單獨的每一行? 20 # 提示: 可以使用 awk, 或者其他方式 . . . 21 # . . . Hans-Joerg Diers 建議使用bash内建函數 "set" |
例 3-2 将代碼塊的輸出結果儲存在一個檔案中
1 #!/bin/bash 2 # rpm-check.sh 3 4 # 查尋一個rpm檔案描述、清單 5 #+ 無論這個包能不能安裝 6 # 将輸出結果儲存在一個檔案中. 7 # 8 # 這個腳本距離說明了代碼塊 9 10 SUCCESS=0 11 E_NOARGS=65 12 13 if [ -z "$1" ] 14 then 15 echo "Usage: `basename $0` rpm-file" 16 exit $E_NOARGS 17 fi 18 19 { # Begin code block. 20 echo 21 echo "Archive Description:" 22 rpm -qpi $1 # Query description. 23 echo 24 echo "Archive Listing:" 25 rpm -qpl $1 # Query listing. 26 echo 27 rpm -i --test $1 # Query whether rpm file can be installed. 28 if [ "$?" -eq $SUCCESS ] 29 then 30 echo "$1 can be installed." 31 else 32 echo "$1 cannot be installed." 33 fi 34 echo # End code block. 35 } > "$1.test" # Redirects output of everything in block to file. 36 37 echo "Results of rpm test in file $1.test" 38 39 # See rpm man page for explanation of options. 40 41 exit 0 |
和()指令不同的是{}封裝的代碼常常不能建立subshell。
{}
文本占位标志。{}用在xargs –i(替換字元串操作)之後表示文本占位。{}在輸出文本中表示占位。
1 ls . | xargs -i -t cp ./{} $1 2 # ^^ ^^ 3 4 # From "ex42.sh" (copydir.sh) example. |