Linux之shell程式設計

walkingLL發表於2018-09-22

shell程式設計

shell歷史

       Shell的作用是解釋執行使用者的命令,使用者輸入一條命令,Shell就解釋執行一條,這種方式稱為互動式(Interactive),Shell還有一種執行命令的方式稱為批處理(Batch),使用者事先寫一個Shell指令碼(Script),其中有很多條命令,讓Shell一次把這些命令執行完,而不必一條一條地敲命令。Shell指令碼和程式語言很相似,也有變數和流程控制語句,但Shell指令碼是解釋執行的,不需要編譯,Shell程式從指令碼中一行一行讀取並執行這些命令,相當於一個使用者把指令碼中的命令一行一行敲到Shell提示符下執行。

       由於歷史原因,UNIX系統上有很多種Shell:

  1. sh(Bourne Shell):由Steve Bourne開發,各種UNIX系統都配有sh。
  2. csh(C Shell):由Bill Joy開發,隨BSD UNIX釋出,它的流程控制語句很像C語言,支援很多Bourne Shell所不支援的功能:作業控制,命令歷史,命令列編輯。
  3. ksh(Korn Shell):由David Korn開發,向後相容sh的功能,並且新增了csh引入的新功能,是目前很多UNIX系統標準配置的Shell,在這些系統上/bin/sh往往是指向/bin/ksh的符號連結。
  4. tcsh(TENEX C Shell):是csh的增強版本,引入了命令補全等功能,在FreeBSD、MacOS X等系統上替代了csh。
  5. bash(Bourne Again Shell):由GNU開發的Shell,主要目標是與POSIX標準保持一致,同時兼顧對sh的相容,bash從csh和ksh借鑑了很多功能,是各種Linux發行版標準配置的Shell,在Linux系統上/bin/sh往往是指向/bin/bash的符號連結。雖然如此,bash和sh還是有很多不同的,一方面,bash擴充套件了一些命令和引數,另一方面,bash並不完全和sh相容,有些行為並不一致,所以bash需要模擬sh的行為:當我們通過sh這個程式名啟動bash時,bash可以假裝自己是sh,不認擴充套件的命令,並且行為與sh保持一致。

itcast$ vim /etc/passwd

       其中最後一列顯示了使用者對應的shell型別

        root:x:0:0:root:/root:/bin/bash

        nobody:x:65534:65534:nobody:/nonexistent:/bin/sh

        syslog:x:101:103::/home/syslog:/bin/false

        itcast:x:1000:1000:itcast,,,:/home/itcast:/bin/bash

        ftp:x:115:125:ftp daemon,,,:/srv/ftp:/bin/false

       使用者在命令列輸入命令後,一般情況下Shell會fork並exec該命令,但是Shell的內建命令例外,執行內建命令相當於呼叫Shell程式中的一個函式,並不建立新的程式。以前學過的cd、alias、umask、exit等命令即是內建命令,凡是用which命令查不到程式檔案所在位置的命令都是內建命令,內建命令沒有單獨的man手冊,要在man手冊中檢視內建命令,應該執行

itcast$ man bash-builtins

       如export、shift、if、eval、[、for、while等等。內建命令雖然不建立新的程式,但也會有Exit Status,通常也用0表示成功非零表示失敗,雖然內建命令不建立新的程式,但執行結束後也會有一個狀態碼,也可以用特殊變數$?讀出。

執行指令碼

         編寫一個簡單的指令碼test.sh:

#! /bin/sh

cd ..

ls

        Shell指令碼中用#表示註釋,相當於C語言的//註釋。但如果#位於第一行開頭,並且是#!(稱為Shebang)則例外,它表示該指令碼使用後面指定的直譯器/bin/sh解釋執行。如果把這個指令碼檔案加上可執行許可權然後執行:

itcast$ chmod a+x test.sh

itcast$ ./test.sh

       Shell會fork一個子程式並呼叫exec執行./test.sh這個程式,exec系統呼叫應該把子程式的程式碼段替換成./test.sh程式的程式碼段,並從它的_start開始執行。然而test.sh是個文字檔案,根本沒有程式碼段和_start函式,怎麼辦呢?其實exec還有另外一種機制,如果要執行的是一個文字檔案,並且第一行用Shebang指定了直譯器,則用直譯器程式的程式碼段替換當前程式,並且從直譯器的_start開始執行,而這個文字檔案被當作命令列引數傳給直譯器。因此,執行上述指令碼相當於執行程式

itcast$ /bin/sh ./test.sh

        以這種方式執行不需要test.sh檔案具有可執行許可權。

        如果將命令列下輸入的命令用()括號括起來,那麼也會fork出一個子Shell執行小括號中的命令,一行中可以輸入由分號;隔開的多個命令,比如:

itcast$ (cd ..;ls -l)

         和上面兩種方法執行Shell指令碼的效果是相同的,cd ..命令改變的是子Shell的PWD,而不會影響到互動式Shell。然而命令

itcast$ cd ..;ls -l

         則有不同的效果,cd ..命令是直接在互動式Shell下執行的,改變互動式Shell的PWD,然而這種方式相當於這樣執行Shell指令碼:

itcast$ source ./test.sh

        或者

itcast$ . ./test.sh

        source或者.命令是Shell的內建命令,這種方式也不會建立子Shell,而是直接在互動式Shell下逐行執行指令碼中的命令。

基本語法

變數

按照慣例,Shell變數通常由字母加下劃線開頭,由任意長度的字母、數字、下劃線組成。有兩種型別的Shell變數:

       1、環境變數

        環境變數可以從父程式傳給子程式,因此Shell程式的環境變數可以從當前Shell程式傳給fork出來的子程式。用printenv命令可以顯示當前Shell程式的環境變數。

       2、本地變數

        只存在於當前Shell程式,用set命令可以顯示當前Shell程式中定義的所有變數(包括本地變數和環境變數)和函式。

        環境變數是任何程式都有的概念,而本地變數是Shell特有的概念。在Shell中,環境變數和本地變數的定義和用法相似。在Shell中定義或賦值一個變數:

itcast$ VARNAME=value

        注意等號兩邊都不能有空格,否則會被Shell解釋成命令和命令列引數。

        一個變數定義後僅存在於當前Shell程式,它是本地變數,用export命令可以把本地變數匯出為環境變數,定義和匯出環境變數通常可以一步完成:

itcast$ export VARNAME=value

          也可以分兩步完成:

itcast$ VARNAME=value

itcast$ export VARNAME

         用unset命令可以刪除已定義的環境變數或本地變數。

itcast$ unset VARNAME

        如果一個變數叫做VARNAME,用 ' VARNAME ' 可以表示它的值,在不引起歧義的情況下也可以用VARNAME表示它的值。通過以下例子比較這兩種表示法的不同:

itcast$ echo $SHELL

       注意,在定義變數時不用“'”取變數值時要用。和C語言不同的是,Shell變數不需要明確定義型別,事實上Shell變數的值都是字串,比如我們定義VAR=45,其實VAR的值是字串45而非整數。Shell變數不需要先定義後使用,如果對一個沒有定義的變數取值,則值為空字串。

檔名代換(Globbing)

         這些用於匹配的字元稱為萬用字元(Wildcard),如:* ? [ ] 具體如下:

     * 匹配0個或多個任意字元

      ? 匹配一個任意字元

       [若干字元] 匹配方括號中任意一個字元的一次出現

 

itcast$ ls /dev/ttyS*

itcast$ ls ch0?.doc

itcast$ ls ch0[0-2].doc

itcast$ ls ch[012] [0-9].doc

        注意,Globbing所匹配的檔名是由Shell展開的,也就是說在引數還沒傳給程式之前已經展開了,比如上述ls ch0[012].doc命令,如果當前目錄下有ch00.doc和ch02.doc,則傳給ls命令的引數實際上是這兩個檔名,而不是一個匹配字串。

命令代換

         由“`”反引號括起來的也是一條命令,Shell先執行該命令,然後將輸出結果立刻代換到當前命令列中。例如定義一個變數存放date 命令的輸出:

itcast$ DATE=`date`

itcast$ echo $DATE

        命令代換也可以用$()表示:

itcast$ DATE=$(date)

算術代換

        使用$(()),用於算術計算,(())中的Shell變數取值將轉換成整數,同樣含義的$[ ]等價例如:

itcast$ VAR=45

itcast$ echo $(($VAR+3)) 等價於 echo $[VAR+3]或 $[$VAR+3]

       $(())中只能用+-*/和()運算子,並且只能做整數運算。

       $[base#n],其中base表示進位制,n按照base進位制解釋,後面再有運算數,按十進位制解釋。

echo $[2#10+11]

echo $[8#10+11]

echo $[16#10+11]

轉義字元

       和C語言類似,\在Shell中被用作轉義字元,用於去除緊跟其後的單個字元的特殊意義(回車除外),換句話說,緊跟其後的字元取字面值。例如:

itcast$ echo $SHELL

/bin/bash

itcast$ echo \$SHELL

$SHELL

itcast$ echo \\

\

        比如建立一個檔名為“$ $”的檔案($間含有空格)可以這樣:

itcast$ touch \$\ \$

       還有一個字元雖然不具有特殊含義,但是要用它做檔名也很麻煩,就是-號。如果要建立一個檔名以-號開頭的檔案,這樣是不正確的:

itcast$ touch -hello

touch: invalid option -- h

Try `touch --help' for more information.

         即使加上\轉義也還是報錯:

itcast$ touch \-hello

touch: invalid option -- h

Try `touch --help' for more information.

       因為各種UNIX命令都把-號開頭的命令列引數當作命令的選項,而不會當作檔名。如果非要處理以-號開頭的檔名,可以有兩種辦法:

itcast$ touch ./-hello

       或者

itcast$ touch -- -hello

       \還有一種用法,在\後敲回車表示續行,Shell並不會立刻執行命令,而是把游標移到下一行,給出一個續行提示符>,等待使用者繼續輸入,最後把所有的續行接到一起當作一個命令執行。例如:

itcast$ ls \

> -l

(ls -l命令的輸出)

單引號

        和C語言同,Shell指令碼中的單引號和雙引號一樣都是字串的界定符(雙引號下一節介紹),而不是字元的界定符。單引號用於保持引號內所有字元的字面值,即使引號內的\和回車也不例外,但是字串中不能出現單引號。如果引號沒有配對就輸入回車,Shell會給出續行提示符,要求使用者把引號配上對。例如:

itcast$ echo '$SHELL'

$SHELL

itcast$ echo 'ABC\(回車)

> DE'(再按一次回車結束命令)

ABC\

DE

雙引號

          被雙引號用括住的內容,將被視為單一字串。它防止萬用字元擴充套件,但允許變數擴充套件。這點與單引號的處理方式不同

itcast$ DATE=$(date)

itcast$ echo "$DATE"

itcast$ echo '$DATE'

         再比如:

itcast$ VAR=200

itcast$ echo $VAR

200

itcast$ echo '$VAR'

$VAR

itcast$ echo "$VAR"

200

 

Shell指令碼語法

條件測試

         命令test或 [ 可以測試一個條件是否成立,如果測試結果為真,則該命令的Exit Status為0,如果測試結果為假,則命令的Exit Status為1(注意與C語言的邏輯表示正好相反)。例如測試兩個數的大小關係:

itcast@ubuntu:~$ var=2

itcast@ubuntu:~$ test $var -gt 1

itcast@ubuntu:~$ echo $?

0

itcast@ubuntu:~$ test $var -gt 3

itcast@ubuntu:~$ echo $?

1

itcast@ubuntu:~$ [ $var -gt 3 ]

itcast@ubuntu:~$ echo $?

1

itcast@ubuntu:~$

       雖然看起來很奇怪,但左方括號 [ 確實是一個命令的名字,傳給命令的各引數之間應該用空格隔開,比如:$VAR、-gt、3、] 是 [ 命令的四個引數,它們之間必須用空格隔開。命令test或 [ 的引數形式是相同的,只不過test命令不需要 ] 引數。以 [ 命令為例,常見的測試命令如下表所示:

[ -d DIR ] 如果DIR存在並且是一個目錄則為真

[ -f FILE ] 如果FILE存在且是一個普通檔案則為真

[ -z STRING ] 如果STRING的長度為零則為真

[ -n STRING ] 如果STRING的長度非零則為真

[ STRING1 = STRING2 ] 如果兩個字串相同則為真

[ STRING1 != STRING2 ] 如果字串不相同則為真

[ ARG1 OP ARG2 ] ARG1和ARG2應該是整數或者取值為整數的變數,OP是-eq(等於)-ne(不等於)-lt(小於)-le(小於等於)-gt(大於)-ge(大於等於)之中的一個

         和C語言類似,測試條件之間還可以做與、或、非邏輯運算:

[ ! EXPR ] EXPR可以是上表中的任意一種測試條件,!表示“邏輯反(非)”

[ EXPR1 -a EXPR2 ] EXPR1和EXPR2可以是上表中的任意一種測試條件,-a表示“邏輯與”

[ EXPR1 -o EXPR2 ] EXPR1和EXPR2可以是上表中的任意一種測試條件,-o表示“邏輯或”

       例如:

$ VAR=abc

$ [ -d Desktop -a $VAR = 'abc' ]

$ echo $?

0

        注意,如果上例中的$VAR變數事先沒有定義,則被Shell展開為空字串,會造成測試條件的語法錯誤(展開為[ -d Desktop -a = ‘abc’ ]),作為一種好的Shell程式設計習慣應該總是把變數取值放在雙引號之中(展開為[ -d Desktop -a “” = ‘abc’ ]):

$ unset VAR

$ [ -d Desktop -a $VAR = 'abc' ]

bash: [: too many arguments

$ [ -d Desktop -a "$VAR" = 'abc' ]

$ echo $?

1

分支

if/then/elif/else/fi

       和C語言類似,在Shell中用if、then、elif、else、fi這幾條命令實現分支控制。這種流程控制語句本質上也是由若干條Shell命令組成的,例如先前講過的

if [ -f ~/.bashrc ]; then

    . ~/.bashrc

fi

      其實是三條命令,if [ -f ∼/.bashrc ]是第一條,then . ∼/.bashrc是第二條,fi是第三條。如果兩條命令寫在同一行則需要用;號隔開,一行只寫一條命令就不需要寫;號了,另外,then後面有換行,但這條命令沒寫完,Shell會自動續行,把下一行接在then後面當作一條命令處理。和[命令一樣,要注意命令和各引數之間必須用空格隔開。if命令的引數組成一條子命令,如果該子命令的Exit Status為0(表示真),則執行then後面的子命令,如果Exit Status非0(表示假),則執行elif、else或者fi後面的子命令。if後面的子命令通常是測試命令,但也可以是其它命令。Shell指令碼沒有{}括號,所以用fi表示if語句塊的結束。見下例:

#! /bin/sh

 

if [ -f /bin/bash ]

then

       echo "/bin/bash is a file"

else

       echo "/bin/bash is NOT a file"

fi

if :; then echo "always true"; fi

        “:”是一個特殊的命令,稱為空命令,該命令不做任何事,但Exit Status總是真。此外,也可以執行/bin/true或/bin/false得到真或假的Exit Status。再看一個例子:

#! /bin/sh

 

echo "Is it morning? Please answer yes or no."

read YES_OR_NO

if [ "$YES_OR_NO" = "yes" ]; then

       echo "Good morning!"

elif [ "$YES_OR_NO" = "no" ]; then

       echo "Good afternoon!"

else

       echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."

       exit 1

fi

exit 0

       上例中的read命令的作用是等待使用者輸入一行字串,將該字串存到一個Shell變數中。

       此外,Shell還提供了&&和||語法,和C語言類似,具有Short-circuit特性,很多Shell指令碼喜歡寫成這樣:

test "$(whoami)" != 'root' && (echo you are using a non-privileged account; exit 1)

        &&相當於“if…then…”,而||相當於“if not…then…”。&&和||用於連線兩個命令,而上面講的-a和-o僅用於在測試表示式中連線兩個測試條件,要注意它們的區別,例如:

test "$VAR" -gt 1 -a "$VAR" -lt 3

          和以下寫法是等價的

test "$VAR" -gt 1 && test "$VAR" -lt 3

case/esac

       case命令可類比C語言的switch/case語句,esac表示case語句塊的結束。C語言的case只能匹配整型或字元型常量表示式,而Shell指令碼的case可以匹配字串和Wildcard,每個匹配分支可以有若干條命令,末尾必須以;;結束,執行時找到第一個匹配的分支並執行相應的命令,然後直接跳到esac之後,不需要像C語言一樣用break跳出。

#! /bin/sh

 

echo "Is it morning? Please answer yes or no."

read YES_OR_NO

case "$YES_OR_NO" in

yes|y|Yes|YES)

        echo "Good Morning!";;

[nN]*)

        echo "Good Afternoon!";;

*)

         echo "Sorry, $YES_OR_NO not recognized. Enter yes or no."

         exit 1;;

esac

exit 0

       使用case語句的例子可以在系統服務的指令碼目錄/etc/init.d中找到。這個目錄下的指令碼大多具有這種形式(以/etc/init.d/nfs-kernel-server為例):

case "$1" in

       start)

             ...

       ;;

       stop)

            ...

        ;;

        reload | force-reload)

            ...

        ;;

       restart)

            ...

       *)

           log_success_msg"Usage: nfs-kernel-server {start|stop|status|reload|force-reload|restart}"

           exit 1

       ;;

esac

        啟動nfs-kernel-server服務的命令是

$ sudo /etc/init.d/nfs-kernel-server start

       $1是一個特殊變數,在執行指令碼時自動取值為第一個命令列引數,也就是start,所以進入start)分支執行相關的命令。同理,命令列引數指定為stop、reload或restart可以進入其它分支執行停止服務、重新載入配置檔案或重新啟動服務的相關命令。

迴圈

for/do/done

        Shell指令碼的for迴圈結構和C語言很不一樣,它類似於某些程式語言的foreach迴圈。例如:

#! /bin/sh

 

for FRUIT in apple banana pear; do

      echo "I like $FRUIT"

done

        FRUIT是一個迴圈變數,第一次迴圈$FRUIT的取值是apple,第二次取值是banana,第三次取值是pear。再比如,要將當前目錄下的chap0、chap1、chap2等檔名改為chap0~、chap1~、chap2~等(按慣例,末尾有~字元的檔名錶示臨時檔案),這個命令可以這樣寫:

$ for FILENAME in chap?; do mv $FILENAME $FILENAME~; done

        也可以這樣寫:

$ for FILENAME in `ls chap?`; do mv $FILENAME $FILENAME~; done

while/do/done

       while的用法和C語言類似。比如一個驗證密碼的指令碼:

#! /bin/sh

 

echo "Enter password:"

read TRY

while [ "$TRY" != "secret" ]; do

       echo "Sorry, try again"

       read TRY

done

          下面的例子通過算術運算控制迴圈的次數:

#! /bin/sh

 

COUNTER=1

while [ "$COUNTER" -lt 10 ]; do

       echo "Here we go again"

       COUNTER=$(($COUNTER+1))

done

        另,Shell還有until迴圈,類似C語言的do…while。如有興趣可在課後自行擴充套件學習。

break和continue

       break[n]可以指定跳出幾層迴圈;continue跳過本次迴圈,但不會跳出迴圈。

       即break跳出,continue跳過。

       練習:將上面驗證密碼的程式修改一下,如果使用者輸錯五次密碼就報錯退出。

位置引數和特殊變數

       有很多特殊變數是被Shell自動賦值的,我們已經遇到了$?和$1。其他常用的位置引數和特殊變數在這裡總結一下:

$0 相當於C語言main函式的argv[0]

$1、$2... 這些稱為位置引數(Positional Parameter),相當於C語言main函式的argv[1]、argv[2]...

$# 相當於C語言main函式的argc - 1,注意這裡的#後面不表示註釋

$@ 表示引數列表"$1" "$2" ...,例如可以用在for迴圈中的in後面。

$* 表示引數列表"$1" "$2" ...,同上

$? 上一條命令的Exit Status

$$ 當前程式號

       位置引數可以用shift命令左移。比如shift 3表示原來的$4現在變成$1,原來的$5現在變成$2等等,原來的$1、$2、$3丟棄,$0不移動。不帶引數的shift命令相當於shift 1。例如:

#! /bin/sh

 

echo "The program $0 is now running"

echo "The first parameter is $1"

echo "The second parameter is $2"

echo "The parameter list is $@"

shift

echo "The first parameter is $1"

echo "The second parameter is $2"

echo "The parameter list is $@"

輸入輸出

echo

       顯示文字行或變數,或者把字串輸入到檔案。

echo [option] string

-e 解析轉義字元

-n 不回車換行。預設情況echo回顯的內容後面跟一個回車換行。

echo "hello\n\n"

echo -e "hello\n\n"

echo "hello"

echo -n "hello"

管道

          可以通過 | 把一個命令的輸出傳遞給另一個命令做輸入。

cat myfile | more

ls -l | grep "myfile"

df -k | awk '{print $1}' | grep -v "檔案系統"

df -k 檢視磁碟空間,找到第一列,去除“檔案系統”,並輸出

tee

         tee命令把結果輸出到標準輸出,另一個副本輸出到相應檔案。

df -k | awk '{print $1}' | grep -v "檔案系統" | tee a.txt

        tee -a a.txt表示追加操作。

df -k | awk '{print $1}' | grep -v "檔案系統" | tee -a a.txt

檔案重定向

cmd > file 把標準輸出重定向到新檔案中

cmd >> file 追加

cmd > file 2>&1 標準出錯也重定向到1所指向的file裡

cmd >> file 2>&1

cmd < file1 > file2 輸入輸出都定向到檔案裡

cmd < &fd 把檔案描述符fd作為標準輸入

cmd > &fd 把檔案描述符fd作為標準輸出

cmd < &- 關閉標準輸入

函式

        和C語言類似,Shell中也有函式的概念,但是函式定義中沒有返回值也沒有引數列表。例如:

#! /bin/sh

 

foo(){ echo "Function foo is called";}

echo "-=start=-"

foo

echo "-=end=-"

       注意函式體的左花括號 { 和後面的命令之間必須有空格或換行,如果將最後一條命令和右花括號 } 寫在同一行,命令末尾必須有分號;。但,不建議將函式定義寫至一行上,不利於指令碼閱讀。

        在定義foo()函式時並不執行函式體中的命令,就像定義變數一樣,只是給foo這個名一個定義,到後面呼叫foo函式的時候(注意Shell中的函式呼叫不寫括號)才執行函式體中的命令。Shell指令碼中的函式必須先定義後呼叫,一般把函式定義語句寫在指令碼的前面,把函式呼叫和其它命令寫在指令碼的最後(類似C語言中的main函式,這才是整個指令碼實際開始執行命令的地方)。

       Shell函式沒有引數列表並不表示不能傳引數,事實上,函式就像是迷你指令碼,呼叫函式時可以傳任意個引數,在函式內同樣是用$0、$1、$2等變數來提取引數,函式中的位置引數相當於函式的區域性變數,改變這些變數並不會影響函式外面的$0、$1、$2等變數。函式中可以用return命令返回,如果return後面跟一個數字則表示函式的Exit Status。

      下面這個指令碼可以一次建立多個目錄,各目錄名通過命令列引數傳入,指令碼逐個測試各目錄是否存在,如果目錄不存在,首先列印資訊然後試著建立該目錄。

#! /bin/sh

 

is_directory()

{

      DIR_NAME=$1

       if [ ! -d $DIR_NAME ]; then

                return 1

        else

                return 0

         fi

}

for DIR in "$@"; do

      if is_directory "$DIR"

      then :

      else

             echo "$DIR doesn't exist. Creating it now..."

              mkdir $DIR > /dev/null 2>&1

              if [ $? -ne 0 ]; then

                   echo "Cannot create directory $DIR"

                   exit 1

              fi

      fi

done

       注意:is_directory()返回0表示真返回1表示假。

Shell指令碼除錯方法

Shell提供了一些用於除錯指令碼的選項,如:

      -n 讀一遍指令碼中的命令但不執行,用於檢查指令碼中的語法錯誤。

      -v 一邊執行指令碼,一邊將執行過的指令碼命令列印到標準錯誤輸出。

      -x 提供跟蹤執行資訊,將執行的每一條命令和結果依次列印出來。

這些選項有三種常見的使用方法:

1、在命令列提供引數。如:

$ sh -x ./script.sh

2、在指令碼開頭提供引數。如:

#! /bin/sh -x

3、在指令碼中用set命令啟用或禁用引數。如:

#! /bin/sh

 

if [ -z "$1" ]; then

    set -x

    echo "ERROR: Insufficient Args."

    exit 1

    set +x

fi

       set -x和set +x分別表示啟用和禁用-x引數,這樣可以只對指令碼中的某一段進行跟蹤除錯。

正規表示式

        以前我們用grep在一個檔案中找出包含某些字串的行,比如在標頭檔案中找出一個巨集定義。其實grep還可以找出符合某個模式(Pattern)的一類字串。例如找出所有符合xxxxx@xxxx.xxx模式的字串(也就是email地址),要求x字元可以是字母、數字、下劃線、小數點或減號,email地址的每一部分可以有一個或多個x字元,例如abc.d@ef.com、1_2@987-6.54,當然符合這個模式的不全是合法的email地址,但至少可以做一次初步篩選,篩掉a.b、c@d等肯定不是email地址的字串。再比如,找出所有符合yyy.yyy.yyy.yyy模式的字串(也就是IP地址),要求y是0-9的數字,IP地址的每一部分可以有1-3個y字元。

       如果要用grep查詢一個模式,如何表示這個模式,這一類字串,而不是一個特定的字串呢?從這兩個簡單的例子可以看出,要表示一個模式至少應該包含以下資訊:

        字元類(Character Class):如上例的x和y,它們在模式中表示一個字元,但是取值範圍是一類字元中的任意一個。

       數量限定符(Quantifier): 郵件地址的每一部分可以有一個或多個x字元,IP地址的每一部分可以有1-3個y字元。

各種字元類以及普通字元之間的位置關係:例如郵件地址分三部分,用普通字元@和.隔開,IP地址分四部分,用.隔開,每一部分都可以用字元類和數量限定符描述。為了表示位置關係,還有位置限定符(Anchor)的概念,將在下面介紹。

       規定一些特殊語法表示字元類、數量限定符和位置關係,然後用這些特殊語法和普通字元一起表示一個模式,這就是正規表示式(Regular Expression)。例如email地址的正規表示式可以寫成[a-zA-Z0-9.-]+@[a-zA-Z0-9.-]+.[a-zA-Z0-9_.-]+,IP地址的正規表示式可以寫成[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}。下一節介紹正規表示式的語法,我們先看看正規表示式在grep中怎麼用。例如有這樣一個文字檔案testfile:

192.168.1.1

1234.234.04.5678

123.4234.045.678

abcde

$ egrep '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' testfile

192.168.1.1

1234.234.04.5678

        egrep相當於grep -E,表示採用Extended正規表示式語法。grep的正規表示式有Basic和Extended兩種規範,它們之間的區別下一節再解釋。另外還有fgrep命令,相當於grep -F,表示只搜尋固定字串而不搜尋正規表示式模式,不會按正規表示式的語法解釋後面的引數。

        注意正規表示式引數用單引號括起來了,因為正規表示式中用到的很多特殊字元在Shell中也有特殊含義(例如),只有用單引號括起來才能保證這些字元原封不動地傳給grep命令,而不會被Shell解釋掉。

      192.168.1.1符合上述模式,由三個.隔開的四段組成,每段都是1到3個數字,所以這一行被找出來了,可為什麼1234.234.04.5678也被找出來了呢?因為grep找的是包含某一模式的行,這一行包含一個符合模式的字串234.234.04.567。相反,123.4234.045.678這一行不包含符合模式的字串,所以不會被找出來。

        grep是一種查詢過濾工具,正規表示式在grep中用來查詢符合模式的字串。其實正規表示式還有一個重要的應用是驗證使用者輸入是否合法,例如使用者通過網頁表單提交自己的email地址,就需要用程式驗證一下是不是合法的email地址,這個工作可以在網頁的Javascript中做,也可以在網站後臺的程式中做,例如PHP、Perl、Python、Ruby、Java或C,所有這些語言都支援正規表示式,可以說,目前不支援正規表示式的程式語言實在很少見。除了程式語言之外,很多UNIX命令和工具也都支援正規表示式,例如grep、vi、sed、awk、emacs等等。“正規表示式”就像“變數”一樣,它是一個廣泛的概念,而不是某一種工具或程式語言的特性。

基本語法

       我們知道C的變數和Shell指令碼變數的定義和使用方法很不相同,表達能力也不相同,C的變數有各種型別,而Shell指令碼變數都是字串。同樣道理,各種工具和程式語言所使用的正規表示式規範的語法並不相同,表達能力也各不相同,有的正規表示式規範引入很多擴充套件,能表達更復雜的模式,但各種正規表示式規範的基本概念都是相通的。本節介紹egrep(1)所使用的正規表示式,它大致上符合POSIX正規表示式規範,詳見regex(7)(看這個man page對你的英文絕對是很好的鍛鍊)。希望讀者仿照上一節的例子,一邊學習語法,一邊用egrep命令做實驗。

字元類

數量限定符

          再次注意grep找的是包含某一模式的行,而不是完全匹配某一模式的行。

          例如有如下文字:

aaabc

aad

efg

       查詢a*這個模式的結果。會發現,三行都被找了出來。

$ egrep 'a*' testfile

aaabc

aad

efg

        a匹配0個或多個a,而第三行包含0個a,所以也包含了這一模式。單獨用a這樣的正規表示式做查詢沒什麼意義,一般是把a*作為正規表示式的一部分來用。

位置限定符

      位置限定符可以幫助grep更準確地查詢。

      例如上一節我們用[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}查詢IP地址,找到這兩行

192.168.1.1

1234.234.04.5678

       如果用^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$查詢,就可以把1234.234.04.5678這一行過濾掉了。

其它特殊字元

Basic正則和Extended正則區別

        以上介紹的是grep正規表示式的Extended規範,Basic規範也有這些語法,只是字元?+{}|()應解釋為普通字元,要表示上述特殊含義則需要加\轉義。如果用grep而不是egrep,並且不加-E引數,則應該遵照Basic規範來寫正規表示式。

grep

1、作用

           Linux系統中grep命令是一種強大的文字搜尋工具,它能使用正規表示式搜尋文字,並把匹 配的行列印出來。grep全稱是Global Regular Expression Print,表示全域性正規表示式版本,它的使用許可權是所有使用者。

          grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的擴充套件,支援更多的re元字元, fgrep就是fixed grep或fast grep,它們把所有的字母都看作單詞,也就是說,正規表示式中的元字元表示回其自身的字面意義,不再特殊。linux使用GNU版本的grep。它功能更強,可以通過-G、-E、-F命令列選項來使用egrep和fgrep的功能。

2、格式及主要引數

grep [options]

主要引數:  grep --help可檢視

                 -c:只輸出匹配行的計數。

                  -i:不區分大小寫。

                  -h:查詢多檔案時不顯示檔名。

                   -l:查詢多檔案時只輸出包含匹配字元的檔名。

                  -n:顯示匹配行及 行號。

                  -s:不顯示不存在或無匹配文字的錯誤資訊。

                 -v:顯示不包含匹配文字的所有行。

                 --color=auto :可以將找到的關鍵詞部分加上顏色的顯示。

        pattern正規表示式主要引數:

\: 忽略正規表示式中特殊字元的原有含義。

^:匹配正規表示式的開始行。

$: 匹配正規表示式的結束行。

\<:從匹配正則表達 式的行開始。

\>:到匹配正規表示式的行結束。

[ ]:單個字元,如[A]即A符合要求 。

[ - ]:範圍,如[A-Z],即A、B、C一直到Z都符合要求 。

.:所有的單個字元。

*:所有字元,長度可以為0。

3、grep命令使用簡單例項

itcast$ grep ‘test’ d*

顯示所有以d開頭的檔案中包含 test的行

 

itcast $ grep ‘test’ aa bb cc

顯示在aa,bb,cc檔案中匹配test的行。

 

itcast $ grep ‘[a-z]\{5\}’ aa

顯示所有包含每個字串至少有5個連續小寫字元的字串的行。

 

itcast $ grep ‘w\(es\)t.*\1′ aa

如果west被匹配,則es就被儲存到記憶體中,並標記為1,然後搜尋任意個字元(.*),這些字元後面緊跟著 另外一個es(\1),找到就顯示該行。如果用egrep或grep -E,就不用”\”號進行轉義,直接寫成’w(es)t.*\1′就可以了。

4、grep命令使用複雜例項

明確要求搜尋子目錄:

grep -r

或忽略子目錄

grep -d skip

如果有很多輸出時,您可以通過管道將其轉到’less’上閱讀:

itcast$ grep magic /usr/src/Linux/Documentation/* | less

這樣,您就可以更方便地閱讀。

有一點要注意,您必需提供一個檔案過濾方式(搜尋全部檔案的話用 *)。如果您忘了,’grep’會一直等著,直到該程式被中斷。如果您遇到了這樣的情況,按 ,然後再試。

下面還有一些有意思的命令列引數:

grep -i pattern files :不區分大小寫地搜尋。預設情況區分大小寫,

grep -l pattern files :只列出匹配的檔名,

grep -L pattern files :列出不匹配的檔名,

grep -w pattern files :只匹配整個單詞,而不是字串的一部分(如匹配’magic’,而不是’magical’),

grep -C number pattern files :匹配的上下文分別顯示[number]行,

grep pattern1 | pattern2 files :顯示匹配 pattern1 或 pattern2 的行,

例如:grep "abc\|xyz" testfile 表示過濾包含abc或xyz的行

grep pattern1 files | grep pattern2 :顯示既匹配 pattern1 又匹配 pattern2 的行。

grep -n pattern files 即可顯示行號資訊

grep -c pattern files 即可查詢總行數

還有些用於搜尋的特殊符號:\< 和 \> 分別標註單詞的開始與結尾。

例如:

grep man * 會匹配 ‘Batman’、’manic’、’man’等,

grep ‘\<man’ * 匹配’manic’和’man’,但不是’Batman’,

grep ‘\<man\>’ 只匹配’man’,而不是’Batman’或’manic’等其他的字串。

‘^’: 指匹配的字串在行首,

‘$’: 指匹配的字串在行 尾,

find

由於find具有強大的功能,所以它的選項也很多,其中大部分選項都值得我們花時間來了解一下。即使系統中含有網路檔案系統( NFS),find命令在該檔案系統中同樣有效,只要你具有相應的許可權。

在執行一個非常消耗資源的find命令時,很多人都傾向於把它放在後臺執行,因為遍歷一個大的檔案系統可能會花費很長的時間(這裡是指30G位元組以上的檔案系統)。

一、find 命令格式

1、find命令的一般形式為

find pathname -options [-print -exec -ok ...]

2、find命令的引數;

pathname: find命令所查詢的目錄路徑。例如用.來表示當前目錄,用/來表示系統根目錄,遞迴查詢。

-print: find命令將匹配的檔案輸出到標準輸出。

-exec: find命令對匹配的檔案執行該引數所給出的shell命令。相應命令的形式為'command' {} \;,注意{}內部無空格,和\;之間含有一個空格分隔符。

-ok: 和-exec的作用相同,只不過以一種更為安全的模式來執行該引數所給出的shell命令,在執行每一個命令之前,都會給出提示,讓使用者來確定是否執行。

3、find命令選項

-name 按照檔名查詢檔案。

-perm 按照檔案許可權來查詢檔案。

-prune 使用這一選項可以使find命令不在當前指定的目錄中查詢,如果同時使用-depth選項,那麼-prune將被

find命令忽略。

-user 按照檔案屬主來查詢檔案。

-group 按照檔案所屬的組來查詢檔案。

-mtime -n +n 按照檔案的更改時間來查詢檔案,-n表示檔案更改時間距現在n天以內,+n表示檔案更改時間距現在

n天以前。find命令還有-atime和-ctime 選項,但它們都和-m time選項。

-nogroup 查詢無有效所屬組的檔案,即該檔案所屬的組在/etc/groups中不存在。

-nouser 查詢無有效屬主的檔案,即該檔案的屬主在/etc/passwd中不存在。

-newer file1 ! file2 查詢更改時間比檔案file1新但比檔案file2舊的檔案。

-type 查詢某一型別的檔案,諸如:

b - 塊裝置檔案。

d - 目錄。

c - 字元裝置檔案。

p - 管道檔案。

l - 符號連結檔案。

f - 普通檔案。

-size n:[c] 查詢檔案長度為n塊的檔案,帶有c時表示檔案長度以位元組計。

-depth 在查詢檔案時,首先查詢當前目錄中的檔案,然後再在其子目錄中查詢。

-fstype 查詢位於某一型別檔案系統中的檔案,這些檔案系統型別通常可以在配置檔案/etc/fstab中找到,該配

置檔案中包含了本系統中有關檔案系統的資訊。

-mount 在查詢檔案時不跨越檔案系統mount點。

-follow 如果find命令遇到符號連結檔案,就跟蹤至連結所指向的檔案。

另外,下面三個的區別:

-amin n 查詢系統中最後N分鐘訪問的檔案

-atime n 查詢系統中最後n*24小時訪問的檔案

-cmin n 查詢系統中最後N分鐘被改變檔案狀態的檔案

-ctime n 查詢系統中最後n*24小時被改變檔案狀態的檔案

-mmin n 查詢系統中最後N分鐘被改變檔案資料的檔案

-mtime n 查詢系統中最後n*24小時被改變檔案資料的檔案

4、使用exec或ok來執行shell命令

        使用find時,只要把想要的操作寫在一個檔案裡,就可以用exec來配合find查詢,很方便的。

         在有些作業系統中只允許-exec選項執行諸如ls或ls -l這樣的命令。大多數使用者使用這一選項是為了查詢舊檔案並刪除它們。建議在真正執行rm命令刪除檔案之前,最好先用ls命令看一下,確認它們是所要刪除的檔案。

         exec選項後面跟隨著所要執行的命令或指令碼,然後是一對兒{},一個空格和一個\,最後是一個分號。為了使用exec選項,必須要同時使用print選項。如果驗證一下find命令,會發現該命令只輸出從當前路徑起的相對路徑及檔名。

         例如:為了用ls -l命令列出所匹配到的檔案,可以把ls -l命令放在find命令的-exec選項中

# find . -type f -exec ls -l {} \;

        上面的例子中,find命令匹配到了當前目錄下的所有普通檔案,並在-exec選項中使用ls -l命令將它們列出。

       在/logs目錄中查詢更改時間在5日以前的檔案並刪除它們:

$ find logs -type f -mtime +5 -exec rm {} \;

       記住:在shell中用任何方式刪除檔案之前,應當先檢視相應的檔案,一定要小心!當使用諸如mv或rm命令時,可以使用-exec選項的安全模式。它將在對每個匹配到的檔案進行操作之前提示你。

       在下面的例子中, find命令在當前目錄中查詢所有檔名以.LOG結尾、更改時間在5日以上的檔案,並刪除它們,只不過在刪除之前先給出提示。

$ find . -name "*.conf" -mtime +5 -ok rm {} \;

< rm ... ./conf/httpd.conf > ? n

        按y鍵刪除檔案,按n鍵不刪除。

       任何形式的命令都可以在-exec選項中使用。

      在下面的例子中我們使用grep命令。find命令首先匹配所有檔名為“ passwd*”的檔案,例如passwd、passwd.old、passwd.bak,然後執行grep命令看看在這些檔案中是否存在一個itcast使用者。

# find /etc -name "passwd*" -exec grep "itcast" {} \;

itcast:x:1000:1000::/home/itcast:/bin/bash

 

二、find命令的例子;

1、查詢當前使用者主目錄下的所有檔案:

下面兩種方法都可以使用

$ find $HOME -print

$ find ~ -print

2、讓當前目錄中檔案屬主具有讀、寫許可權,並且檔案所屬組的使用者和其他使用者具有讀許可權的檔案;

$ find . -type f -perm 644 -exec ls -l {} \;

3、為了查詢系統中所有檔案長度為0的普通檔案,並列出它們的完整路徑;

$ find / -type f -size 0 -exec ls -l {} \;

4、查詢/var/logs目錄中更改時間在7日以前的普通檔案,並在刪除之前詢問它們;

$ find /var/logs -type f -mtime +7 -ok rm {} \;

5、為了查詢系統中所有屬於root組的檔案;

$find . -group root -exec ls -l {} \;

6、find命令將刪除當目錄中訪問時間在7日以來、含有數字字尾的admin.log檔案。

該命令只檢查三位數字,所以相應檔案的字尾不要超過999。先建幾個admin.log*的檔案 ,才能使用下面這個命令

$ find . -name "admin.log[0-9][0-9][0-9]" -atime -7 -ok rm {} \;

7、為了查詢當前檔案系統中的所有目錄並排序;

$ find . -type d | sort

三、xargs

xargs - build and execute command lines from standard input

       在使用find命令的-exec選項處理匹配到的檔案時, find命令將所有匹配到的檔案一起傳遞給exec執行。但有些系統對能夠傳遞給exec的命令長度有限制,這樣在find命令執行幾分鐘之後,就會出現 溢位錯誤。錯誤資訊通常是“引數列太長”或“引數列溢位”。這就是xargs命令的用處所在,特別是與find命令一起使用。

       find命令把匹配到的檔案傳遞給xargs命令,而xargs命令每次只獲取一部分檔案而不是全部,不像-exec選項那樣。這樣它可以先處理最先獲取的一部分檔案,然後是下一批,並如此繼續下去。

        在有些系統中,使用-exec選項會為處理每一個匹配到的檔案而發起一個相應的程式,並非將匹配到的檔案全部作為引數一次執行;這樣在有些情況下就會出現程式過多,系統效能下降的問題,因而效率不高;

        而使用xargs命令則只有一個程式。另外,在使用xargs命令時,究竟是一次獲取所有的引數,還是分批取得引數,以及每一次獲取引數的數目都會根據該命令的選項及系統核心中相應的可調引數來確定。

        來看看xargs命令是如何同find命令一起使用的,並給出一些例子。

        下面的例子查詢系統中的每一個普通檔案,然後使用xargs命令來測試它們分別屬於哪類檔案

#find . -type f -print | xargs file

在當前目錄下查詢所有使用者具有讀、寫和執行許可權的檔案,並收回相應的寫許可權:

# ls -l

# find . -perm -7 -print | xargs chmod o-w

# ls -l

用grep命令在所有的普通檔案中搜尋hello這個詞:

# find . -type f -print | xargs grep "hello"

用grep命令在當前目錄下的所有普通檔案中搜尋hello這個詞:

# find . -name \* -type f -print | xargs grep "hello"

         注意,在上面的例子中, \用來取消find命令中的*在shell中的特殊含義。

         find命令配合使用exec和xargs可以使使用者對所匹配到的檔案執行幾乎所有的命令。

四、find 命令的引數

           下面是find一些常用引數的例子,有用到的時候查查就行了,也可以用man。

1、使用name選項

            檔名選項是find命令最常用的選項,要麼單獨使用該選項,要麼和其他選項一起使用。

            可以使用某種檔名模式來匹配檔案,記住要用引號將檔名模式引起來。

            不管當前路徑是什麼,如果想要在自己的根目錄HOME中查詢檔名符合∗.txt的檔案,使用‘~’作為 ‘pathname’ 的引數,波浪號代表了你的HOME目錄。

$ find ~ -name "*.txt" -print

想要在當前目錄及子目錄中查詢所有的‘ *.txt’檔案,可以用:

$ find . -name "*.txt" -print

想要的當前目錄及子目錄中查詢檔名以一個大寫字母開頭的檔案,可以用:

$ find . -name "[A-Z]*" -print

想要在/etc目錄中查詢檔名以host開頭的檔案,可以用:

$ find /etc -name "host*" -print

想要查詢$HOME目錄中的檔案,可以用:

$ find ~ -name "*" -print 或find . –print

要想讓系統高負荷執行,就從根目錄開始查詢所有的檔案:

$ find / -name "*" -print

如果想在當前目錄查詢檔名以兩個小寫字母開頭,跟著是兩個數字,最後是.txt的檔案,下面的命令就能夠返回例如名為ax37.txt的檔案:

$find . -name "[a-z][a-z][0-9][0-9].txt" -print

2、用perm選項

         按照檔案許可權模式用-perm選項,按檔案許可權模式來查詢檔案的話。最好使用八進位制的許可權表示法。

        如在當前目錄下查詢檔案許可權位為755的檔案,即檔案屬主可以讀、寫、執行,其他使用者可以讀、執行的檔案,可以用:

$ find . -perm 755 -print

還有一種表達方法:在八進位制數字前面要加一個橫槓-,表示都匹配,如-007就相當於777,-006相當於666

# ls -l

# find . -perm 006

# find . -perm -006

-perm mode:檔案許可正好符合mode

-perm +mode:檔案許可部分符合mode

-perm -mode: 檔案許可完全符合mode

3、忽略某個目錄

       如果在查詢檔案時希望忽略某個目錄,因為你知道那個目錄中沒有你所要查詢的檔案,那麼可以使用-prune選項來指出需要忽略的目錄。在使用-prune選項時要當心,因為如果你同時使用了-depth選項,那麼-prune選項就會被find命令忽略。

        如果希望在/apps目錄下查詢檔案,但不希望在/apps/bin目錄下查詢,可以用:

$ find /apps -path "/apps/bin" -prune -o -print

4、使用find查詢檔案的時候怎麼避開某個檔案目錄

比如要在/home/itcast目錄下查詢不在dir1子目錄之內的所有檔案

find /home/itcast -path "/home/itcast/dir1" -prune -o -print

避開多個資料夾

find /home \( -path /home/itcast/f1 -o -path /home/itcast/f2 \) -prune -o -print

注意(前的\,注意(後的空格。

5、使用user和nouser選項

按檔案屬主查詢檔案,如在$HOME目錄中查詢檔案屬主為itcast的檔案,可以用:

$ find ~ -user itcast -print

在/etc目錄下查詢檔案屬主為uucp的檔案:

$ find /etc -user uucp -print

       為了查詢屬主帳戶已經被刪除的檔案,可以使用-nouser選項。這樣就能夠找到那些屬主在/etc/passwd檔案中沒有有效帳戶的檔案。在使用-nouser選項時,不必給出使用者名稱;find命令能夠為你完成相應的工作。

      例如,希望在/home目錄下查詢所有的這類檔案,可以用:

$ find /home -nouser -print

6、使用group和nogroup選項

就像user和nouser選項一樣,針對檔案所屬於的使用者組, find命令也具有同樣的選項,為了在/apps目錄下查詢屬於itcast使用者組的檔案,可以用:

$ find /apps -group itcast -print

要查詢沒有有效所屬使用者組的所有檔案,可以使用nogroup選項。下面的find命令從檔案系統的根目錄處查詢這樣的檔案

$ find / -nogroup -print

7、按照更改時間或訪問時間等查詢檔案

        如果希望按照更改時間來查詢檔案,可以使用mtime,atime或ctime選項。如果系統突然沒有可用空間了,很有可能某一個檔案的長度在此期間增長迅速,這時就可以用mtime選項來查詢這樣的檔案。

       用減號-來限定更改時間在距今n日以內的檔案,而用加號+來限定更改時間在距今n日以前的檔案。

      希望在系統根目錄下查詢更改時間在5日以內的檔案,可以用:

$ find / -mtime -5 -print

為了在/var/adm目錄下查詢更改時間在3日以前的檔案,可以用:

$ find /var/adm -mtime +3 -print

8、查詢比某個檔案新或舊的檔案

如果希望查詢更改時間比某個檔案新但比另一個檔案舊的所有檔案,可以使用-newer選項。它的一般形式為:

newest_file_name ! oldest_file_name

其中,!是邏輯非符號。

9、使用type選項

在/etc目錄下查詢所有的目錄,可以用:

$ find /etc -type d -print

在當前目錄下查詢除目錄以外的所有型別的檔案,可以用:

$ find . ! -type d -print

在/etc目錄下查詢所有的符號連結檔案,可以用

$ find /etc -type l -print

10、使用size選項

       可以按照檔案長度來查詢檔案,這裡所指的檔案長度既可以用塊(block)來計量,也可以用位元組來計量。以位元組計量檔案長度的表達形式為N c;以塊計量檔案長度只用數字表示即可。

      在按照檔案長度查詢檔案時,一般使用這種以位元組表示的檔案長度,在檢視檔案系統的大小,因為這時使用塊來計量更容易轉換。 在當前目錄下查詢檔案長度大於1 M位元組的檔案:

$ find . -size +1000000c -print

在/home/apache目錄下查詢檔案長度恰好為100位元組的檔案:

$ find /home/apache -size 100c -print

在當前目錄下查詢長度超過10塊的檔案(一塊等於512位元組):

$ find . -size +10 -print

11、使用depth選項

       在使用find命令時,可能希望先匹配所有的檔案,再在子目錄中查詢。使用depth選項就可以使find命令這樣做。這樣做的一個原因就是,當在使用find命令向磁帶上備份檔案系統時,希望首先備份所有的檔案,其次再備份子目錄中的檔案。

        在下面的例子中, find命令從檔案系統的根目錄開始,查詢一個名為CON.FILE的檔案。

        它將首先匹配所有的檔案然後再進入子目錄中查詢。

$ find / -name "CON.FILE" -depth -print

12、使用mount選項

         在當前的檔案系統中查詢檔案(不進入其他檔案系統),可以使用find命令的mount選項。

         從當前目錄開始查詢位於本檔案系統中檔名以XC結尾的檔案:

$ find . -name "*.XC" -mount -print

練習:請找出你10天內所訪問或修改過的.c和.cpp檔案。

sed

         sed意為流編輯器(Stream Editor),在Shell指令碼和Makefile中作為過濾器使用非常普遍,也就是把前一個程式的輸出引入sed的輸入,經過一系列編輯命令轉換為另一種格式輸出。sed和vi都源於早期UNIX的ed工具,所以很多sed命令和vi的末行命令是相同的。

sed命令列的基本格式為

sed option 'script' file1 file2 ...

sed option -f scriptfile file1 file2 ...

選項含義:

--version 顯示sed版本。

--help 顯示幫助文件。

-n,--quiet,--silent 靜默輸出,預設情況下,sed程式在所有的指令碼指令執行完畢後,將自動列印模式空間中的內容,這些選項可以遮蔽自動列印。

-e script 允許多個指令碼指令被執行。

-f script-file,

--file=script-file 從檔案中讀取指令碼指令,對編寫自動指令碼程式來說很棒!

-i,--in-place 直接修改原始檔,經過指令碼指令處理後的內容將被輸出至原始檔(原始檔被修改)慎用!

-l N, --line-length=N 該選項指定l指令可以輸出的行長度,l指令用於輸出非列印字元。

--posix 禁用GNU sed擴充套件功能。

-r, --regexp-extended 在指令碼指令中使用擴充套件正規表示式

-s, --separate 預設情況下,sed將把命令列指定的多個檔名作為一個長的連續的輸入流。而GNU sed則允許把他們當作單獨的檔案,這樣如正規表示式則不進行跨檔案匹配。

-u, --unbuffered 最低限度的快取輸入與輸出。

以上僅是sed程式本身的選項功能說明,至於具體的指令碼指令(即對檔案內容做的操作)後面我們會詳細描述,這裡就簡單介紹幾個指令碼指令操作作為sed程式的例子。

a, append 追加

i, insert 插入

d, delete 刪除

s, substitution 替換

如:$ sed "2a itcast" ./testfile 在輸出testfile內容的第二行後新增"itcast"。

$ sed "2,5d" testfile

       sed處理的檔案既可以由標準輸入重定向得到,也可以當命令列引數傳入,命令列引數可以一次傳入多個檔案,sed會依次處理。sed的編輯命令可以直接當命令列引數傳入,也可以寫成一個指令碼檔案然後用-f引數指定,編輯命令的格式為:

/pattern/action

          其中pattern是正規表示式,action是編輯操作。sed程式一行一行讀出待處理檔案,如果某一行與pattern匹配,則執行相應的action,如果一條命令沒有pattern而只有action,這個action將作用於待處理檔案的每一行。

常用sed命令

/pattern/p 列印匹配pattern的行

/pattern/d 刪除匹配pattern的行

/pattern/s/pattern1/pattern2/ 查詢符合pattern的行,將該行第一個匹配pattern1的字串替換為pattern2

/pattern/s/pattern1/pattern2/g 查詢符合pattern的行,將該行所有匹配pattern1的字串替換為pattern2

        使用p命令需要注意,sed是把待處理檔案的內容連同處理結果一起輸出到標準輸出的,因此p命令表示除了把檔案內容列印出來之外還額外列印一遍匹配pattern的行。比如一個檔案testfile的內容是

123

abc

456

列印其中包含abc的行

$ sed '/abc/p' testfile

123

abc

abc

456

要想只輸出處理結果,應加上-n選項,這種用法相當於grep命令

$ sed -n '/abc/p' testfile

abc

使用d命令就不需要-n引數了,比如刪除含有abc的行

$ sed '/abc/d' testfile

123

456

注意,sed命令不會修改原檔案,刪除命令只表示某些行不列印輸出,而不是從原檔案中刪去。

使用查詢替換命令時,可以把匹配pattern1的字串複製到pattern2中,比如:

$ sed 's/bc/-&-/' testfile

123

a-bc-

456

pattern2中的&表示原檔案的當前行中與pattern1相匹配的字串

再比如:

$ sed 's/\([0-9]\)\([0-9]\)/-\1-~\2~/' testfile

-1-~2~3

abc

-4-~5~6

pattern2中的\1表示與pattern1的第一個()括號相匹配的內容,\2表示與pattern1的第二個()括號相匹配的內容。sed預設使用Basic正規表示式規範,如果指定了-r選項則使用Extended規範,那麼()括號就不必轉義了。如:

sed -r 's/([0-9])([0-9])/-\1-~\2~/' out.sh

替換結束後,所有行,含有連續數字的第一個數字前後都新增了“-”號;第二個數字前後都新增了“~”號。

可以一次指定多條不同的替換命令,用“;”隔開:

$ sed 's/yes/no/;s/static/dhcp/' ./testfile

注:使用分號隔開指令。

也可以使用 -e 引數來指定不同的替換命令,有幾個替換命令需新增幾個 -e 引數:

$ sed -e 's/yes/no/' -e 's/static/dhcp/' testfile

注:使用-e選項。

如果testfile的內容是

<html><head><title>Hello World</title></head>

<body>Welcome to the world of regexp!</body></html>

現在要去掉所有的HTML標籤,使輸出結果為:

Hello World

Welcome to the world of regexp!

怎麼做呢?如果用下面的命令

$ sed 's/<.*>//g' testfile

結果是兩個空行,把所有字元都過濾掉了。這是因為,正規表示式中的數量限定符會匹配儘可能長的字串,這稱為貪心的(Greedy)。比如sed在處理第一行時,<.*>匹配的並不是<html>或<head>這樣的標籤,而是

<html><head><title>Hello World</title>

這樣一整行,因為這一行開頭是<,中間是若干個任意字元,末尾是>。那麼這條命令怎麼改才對呢?留給同學們思考練習。

awk

       sed以行為單位處理檔案,awk比sed強的地方在於不僅能以行為單位還能以列為單位處理檔案。awk預設的行分隔符是換行,預設的列分隔符是連續的空格和Tab,但是行分隔符和列分隔符都可以自定義,比如/etc/passwd檔案的每一行有若干個欄位,欄位之間以:分隔,就可以重新定義awk的列分隔符為:並以列為單位處理這個檔案。awk實際上是一門很複雜的指令碼語言,還有像C語言一樣的分支和迴圈結構,但是基本用法和sed類似,awk命令列的基本形式為:

awk option 'script' file1 file2 ...

awk option -f scriptfile file1 file2 ...

         和sed一樣,awk處理的檔案既可以由標準輸入重定向得到,也可以當命令列引數傳入,編輯命令可以直接當命令列引數傳入,也可以用-f引數指定一個指令碼檔案,編輯命令的格式為:

/pattern/{actions}

condition{actions}

      和sed類似,pattern是正規表示式,actions是一系列操作。awk程式一行一行讀出待處理檔案,如果某一行與pattern匹配,或者滿足condition條件,則執行相應的actions,如果一條awk命令只有actions部分,則actions作用於待處理檔案的每一行。比如檔案testfile的內容表示某商店的庫存量:

ProductA 30

ProductB 76

ProductC 55

列印每一行的第二列:

$ awk '{print $2;}' testfile

30

76

55

       自動變數$1、$2分別表示第一列、第二列等,類似於Shell指令碼的位置引數,而$0表示整個當前行。再比如,如果某種產品的庫存量低於75則在行末標註需要訂貨:

$ awk '$2<75 {printf "%s\t%s\n", $0, "REORDER";} $2>=75 {print $0;}' testfile

ProductA 30 REORDER

ProductB 76

ProductC 55 REORDER

       可見awk也有和C語言非常相似的printf函式。awk命令的condition部分還可以是兩個特殊的condition-BEGIN和END,對於每個待處理檔案,BEGIN後面的actions在處理整個檔案之前執行一次,END後面的actions在整個檔案處理完之後執行一次。

awk命令可以像C語言一樣使用變數(但不需要定義變數),比如統計一個檔案中的空行數

$ awk '/^ *$/ {x=x+1;} END {print x;}' testfile

就像Shell的環境變數一樣,有些awk變數是預定義的有特殊含義的:

awk常用的內建變數

FILENAME 當前輸入檔案的檔名,該變數是隻讀的

NR 當前行的行號,該變數是隻讀的,R代表record

NF 當前行所擁有的列數,該變數是隻讀的,F代表field

OFS 輸出格式的列分隔符,預設是空格

FS 輸入檔案的列分融符,預設是連續的空格和Tab

ORS 輸出格式的行分隔符,預設是換行符

RS 輸入檔案的行分隔符,預設是換行符

例如列印系統中的使用者帳號列表

$ awk 'BEGIN {FS=":"} {print $1;}' /etc/passwd

awk也可以像C語言一樣使用if/else、while、for控制結構。可自行擴充套件學習。

C程式中使用正則

        POSIX規定了正規表示式的C語言庫函式,詳見regex(3)。我們已經學習了很多C語言庫函式的用法,讀者應該具備自己看懂man手冊的能力了。本章介紹了正規表示式在grep、sed、awk中的用法,學習要能夠舉一反三,請讀者根據regex(3)自己總結正規表示式在C語言中的用法,寫一些簡單的程式,例如驗證使用者輸入的IP地址或email地址格式是否正確。

        C語言處理正規表示式常用的函式有regcomp()、regexec()、regfree()和regerror(),一般分為三個步驟,如下所示:

        C語言中使用正規表示式一般分為三步:

編譯正規表示式 regcomp()

匹配正規表示式 regexec()

釋放正規表示式 regfree()

       下邊是對三個函式的詳細解釋

        這個函式把指定的正規表示式pattern編譯成一種特定的資料格式compiled,這樣可以使匹配更有效。函式regexec 會使用這個資料在目標文字串中進行模式匹配。執行成功返回0。

int regcomp (regex_t *compiled, const char *pattern, int cflags)

 

         regex_t 是一個結構體資料型別,用來存放編譯後的正規表示式,它的成員re_nsub 用來儲存正規表示式中的子正規表示式的個數,子正規表示式就是用圓括號包起來的部分表示式。

         pattern 是指向我們寫好的正規表示式的指標。

        cflags 有如下4個值或者是它們或運算(|)後的值:

                  REG_EXTENDED 以功能更加強大的擴充套件正規表示式的方式進行匹配。

                  REG_ICASE 匹配字母時忽略大小寫。

                  REG_NOSUB 不用儲存匹配後的結果,只返回是否成功匹配。如果設定該標誌位,那麼在regexec將忽略nmatch和pmatch兩個引數。

                   REG_NEWLINE 識別換行符,這樣'$'就可以從行尾開始匹配,'^'就可以從行的開頭開始匹配。

       當我們編譯好正規表示式後,就可以用regexec 匹配我們的目標文字串了,如果在編譯正規表示式的時候沒有指定cflags的引數為REG_NEWLINE,則預設情況下是忽略換行符的,也就是把整個文字串當作一個字串處理。

       執行成功返回0。

       regmatch_t 是一個結構體資料型別,在regex.h中定義:

typedef struct {

        regoff_t rm_so;

        regoff_t rm_eo;

} regmatch_t;

       成員rm_so 存放匹配文字串在目標串中的開始位置,rm_eo 存放結束位置。通常我們以陣列的形式定義一組這樣的結構。因為往往我們的正規表示式中還包含子正規表示式。陣列0單元存放主正規表示式位置,後邊的單元依次存放子正規表示式位置。

int regexec (regex_t *compiled, char *string, size_t nmatch, regmatch_t matchptr[], int eflags)

compiled 是已經用regcomp函式編譯好的正規表示式。

string 是目標文字串。

nmatch 是regmatch_t結構體陣列的長度。

matchptr regmatch_t型別的結構體陣列,存放匹配文字串的位置資訊。

eflags 有兩個值:

         REG_NOTBOL 讓特殊字元^無作用

         REG_NOTEOL 讓特殊字元$無作用

       當我們使用完編譯好的正規表示式後,或者要重新編譯其他正規表示式的時候,我們可以用這個函式清空compiled指向的regex_t結構體的內容,請記住,如果是重新編譯的話,一定要先清空regex_t結構體。

void regfree (regex_t *compiled)

        當執行regcomp 或者regexec 產生錯誤的時候,就可以呼叫這個函式而返回一個包含錯誤資訊的字串。

size_t regerror (int errcode, regex_t *compiled, char *buffer, size_t length)

 

errcode 是由regcomp 和 regexec 函式返回的錯誤代號。

compiled 是已經用regcomp函式編譯好的正規表示式,這個值可以為NULL。

buffer 指向用來存放錯誤資訊的字串的記憶體空間。

length 指明buffer的長度,如果這個錯誤資訊的長度大於這個值,則regerror 函式會自動截斷超出的字串,但他仍然會返回完整的字串的長度。所以我們可以用如下的方法先得到錯誤字串的長度。

例如: size_t length = regerror (errcode, compiled, NULL, 0);

測試用例:

#include <sys/types.h>
#include <regex.h>
#include <stdio.h>

int main(int argc, char ** argv)
{
	if (argc != 3) {
		printf("Usage: %s RegexString Text\n", argv[0]);
		return 1;
	}
	const char * pregexstr = argv[1];
	const char * ptext = argv[2];
	regex_t oregex;
	int nerrcode = 0;
	char szerrmsg[1024] = {0};
	size_t unerrmsglen = 0;
	if ((nerrcode = regcomp(&oregex, pregexstr, REG_EXTENDED|REG_NOSUB)) == 0) {
		if ((nerrcode = regexec(&oregex, ptext, 0, NULL, 0)) == 0)	{
			printf("%s matches %s\n", ptext, pregexstr);
			regfree(&oregex);
			return 0;
		}
	}
	unerrmsglen = regerror(nerrcode, &oregex, szerrmsg, sizeof(szerrmsg));
	unerrmsglen = unerrmsglen < sizeof(szerrmsg) ? unerrmsglen : sizeof(szerrmsg) - 1;
	szerrmsg[unerrmsglen] = '\0';
	printf("ErrMsg: %s\n", szerrmsg);
	regfree(&oregex);

	return 1;
}

匹配網址:

./a.out "http:\/\/www\..*\.com" "http://www.taobao.com"

匹配郵箱:

./a.out "^[a-zA-Z0-9]+@[a-zA-Z0-9]+.[a-zA-Z0-9]+" "itcast123@itcast.com"

./a.out "\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*" "itcast@qq.com"

注:\w匹配一個字元,包含下劃線

        匹配固話號碼:請同學們自己編寫。

        除了gnu提供的函式外,還常用PCRE處理正則,全稱是Perl Compatible Regular Ex-pressions。從名字我們可以看出PCRE庫是與Perl中正規表示式相相容的一個正規表示式庫。PCRE是免費開源的庫,它是由C語言實現的,這裡是它的官方主頁:http://www.pcre.org/,感興趣的朋友可以在這裡瞭解更多的內容。 要得到PCRE庫,可以從這裡下載:http://sourceforge.net/projects/pcre/files/

     PCRE++是一個對PCRE庫的C++封裝,它提供了更加方便、易用的C++介面。這裡是它的官方主頁:http://www.daemon.de/PCRE,感興趣的朋友可以在這裡瞭解更多的內容。 要得到PCRE++庫,可以從這裡下載:http://www.daemon.de/PcreDownload

          另外c++中常用 boost regex。

相關文章