天天看點

ABS_Guide_Cn_1

進階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.