在 shell 編程中,常需要處理文本,這裡介紹幾個文本處理命令。 一、grep 命令 grep 命令由來已久,用 grep 命令來查找 文本十分方便。在 POSIX 系統上,grep 可以在兩種正則表達式風格中選擇一種(BRE 和 ERE),或是執行簡單的字元串匹配。傳統上,有三種程式可以用來查找 ...
在 shell 編程中,常需要處理文本,這裡介紹幾個文本處理命令。
一、grep 命令
grep 命令由來已久,用 grep 命令來查找 文本十分方便。在 POSIX 系統上,grep 可以在兩種正則表達式風格中選擇一種(BRE 和 ERE),或是執行簡單的字元串匹配。傳統上,有三種程式可以用來查找整個文本文件:
1)grep:最早的文本匹配程式。使用 POSIX 標准定義的基本正則表達(Basic Regular Expression,BRE);
2)egrep:擴展 grep。使用擴展正則表達式(Extended Regular Expression,ERE);
3)fgrep:快速 grep。匹配固定字元串而非正則表達式,它使用優化的演算法,能更有效地匹配固定字元串。
在目前的 POSIX 標準中,這三個程式已經被整合成為一個程式 grep,通過對 grep 命令加以不同的選項進行選擇控制。
grep 命令由一個選項、一個要匹配的模式和要搜索的文件組成,語法如下:
grep [options] PATTERN [FILES]
如果沒有提供文件名,則 grep 命令將搜索標準輸入。grep 命令將會根據所提供的模式對文件進行匹配,發現匹配查找模式的行時,將該行顯示出來。當 grep 命令同時搜索多個文件時,將會在搜索結果每一行前面加上文件名與一個冒號。grep 命令的主要選項如下:
選項 | 功能 |
-E | 使用擴展正則表達式進行匹配(取代傳統的 egrep 命令) |
-F | 使用固定字元串進行匹配(取代傳統的 fgrep 命令) |
-c | 輸出匹配行的數目,而不是輸出匹配的行 |
-h | 取消每個輸出行的普通首碼,即匹配查詢模式的文件名 |
-i | 忽略大小寫 |
-l | 只列出包含匹配行的文件名,而不輸出真正的匹配行 |
-v | 對匹配模式取反,即搜索匹配不到的行 |
-n | 輸出行號 |
用例子演示一下:
[tongye@localhost ~]$ grep -ni ROOT /etc/passwd 1:root:x:0:0:root:/root:/bin/bash 10:operator:x:11:0:operator:/root:/sbin/nologin
該命令將會在 /etc/passwd 中查找有 root 的行,並將該行顯示出來, -n 選項輸出行號,-i 選項忽略大小寫。
二、sed 命令
sed( stream editor 流編輯器) ,可以用來在管道或者命令序列中編輯數據。sed 的語法如下:
sed option command file
其中,command 是命令部分,用來指示 sed 該執行何種操作,file 則是 sed 命令將要操作的對象,通常是一個文件,如果沒有文件,則使用標準輸入。option 是 sed 命令可以使用的選項,主要有三個選項: -n、-e、-f,在後面再介紹。
sed 命令讀取每一個文件,一次讀一行,將讀取的行放到記憶體的一個區域--稱為模式空間(pattern space),所有編輯上的操作都會應用到模式空間的內容。當所有操作完成後,sed 命令會將模式空間的最後內容列印到標準輸出,再回到開始處,讀取另一個輸入行。為了演示 sed 命令,筆者寫了一小段文本 test.txt 用作試驗的素材(英語差,語法問題請忽略)
hello,my name is tongye I want to write a program named HelloWorld.c now,let`s begin #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple writed by tongye end
2.1 使用 s 參數執行替換操作
sed 命令一個常用的功能是進行替換操作,sed 替換操作的一般格式如下:
sed 's/string1/string2/' file # 將文件中每行的第一個 string1 替換成 string2
在上述語句中,參數 s 表示這是一個替換操作,/ 字元是界定符,用於分隔正則表達式與替代文本。界定符可以是任何可顯示的字元,但是 / 字元是最常用的界定符。另外,在處理文件名稱時,一般使用分號、冒號或逗號作為界定符。string1 是被替換的文本,可以是正則表達式;string2 是替換文本。
[tongye@localhost Shell_Program]$ sed 's/tongye/ttyezi/' test.txt hello,my name is ttyezi I want to write a program named HelloWorld.c now,let`s begin #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple writed by ttyezi end
需要註意的是,上述語句只能替換第一個匹配到的文本,若想要將每一個匹配到的文本都替換掉,需要在結尾加上 g 參數(global),即:
sed 's/string1/string2/g' file # 將文件中所有的 string1 都替換成 string2
如果需要刪除文本中的一個字元串,可以在替換文本處不放入任何文本(即空)來實現,如下:
sed 's/string1//g' file # 刪除文件中所有的 string1
2.2 使用 -e 選項和 -f 選項同時執行多個編輯命令
當 sed 後面需要同時接多個編輯命令的時候,需要使用 -e 選項。每一個編輯命令都使用一個 -e 選項,如:
[tongye@localhost Shell_Program]$ sed -e 's/tongye/ttyezi/g' -e 's/HelloWorld/helloworld/g' test.txt hello,my name is ttyezi I want to write a program named helloworld.c now,let`s begin #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple writed by ttyezi end
當需要編輯的項目很多時,如果把每一個編輯命令都接到 sed 後面,無疑會讓代碼很複雜,不易閱讀且容易出錯。這時,可以將所有的編輯命令都寫進一個腳本,再使用 sed 搭配 -f 選項來操作:
# substitute.sed 存放著編輯命令 s/tongye/ttyezi/g s/HelloWorld/helloworld/g s;^\(.\).*\1$;The first letter of this line is the same as its last letter; [tongye@localhost Shell_Program]$ sed -f substitute.sed test.txt hello,my name is ttyezi I want to write a program named helloworld.c The first letter of this line is the same as its last letter #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple writed by ttyezi endView Code
2.3 使用 -n 選項與 p 參數列印特定的行
sed 預設情況下會把輸入的每一行都列印到標準輸出上,如果不想輸出所有行,可以使用 -n 選項。使用了 -n 選項的 sed 命令將不列印任何行, -n 選項通常與參數 p 配合起來使用,p 參數可以讓 sed 命令列印出符合指定範圍或模式的所有行:
[tongye@localhost Shell_Program]$ sed -n 's/tongye/ttyezi/p' test.txt hello,my name is ttyezi writed by ttyezi
該語句將文件中的 tongye 替換成 ttyezi,並且只列印發生替換操作的兩行;
[tongye@localhost Shell_Program]$ sed -n '1,3p' test.txt # 列印 1 到 3 行 hello,my name is tongye I want to write a program named HelloWorld.c now,let`s begin [tongye@localhost Shell_Program]$ sed -n '4p' test.txt # 只列印第 4 行 #include "stdio.h"
使用 sed -n '1,3p' 來只列印文件的前三行,註意這裡的 '1,3' 表示的是一個範圍;
[tongye@localhost Shell_Program]$ sed -n '/HelloWorld/p' test.txt I want to write a program named HelloWorld.c
該命令只列印包含 HelloWorld 的行。
2.4 使用 d 參數執行刪除操作
要刪除某一個特定的行,可以使用參數 d,只需要指定行號或者行範圍,就可以從輸入中刪除指定的行:
[tongye@localhost Shell_Program]$ sed '1,4d' test.txt # 刪除第 1 到 4 行,然後將剩餘的行列印到標準輸出 main(){ printf("Hello world"); } oh,it`s symple writed by tongye end
sed 也可以使用參數 d 來刪除符合匹配模式的行:
[tongye@localhost Shell_Program]$ sed '/tongye/d' test.txt # 刪除所有含有 tongye 的行 I want to write a program named HelloWorld.c now,let`s begin #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple end
2.5 使用 -i 選項或輸出重定向來保存 sed 編輯的內容
在上面的所有操作中,我們發現雖然 sed 操作確實完成了,輸出到標準輸出上的文本內容確實發生了變化,但是如果我們再次打開所編輯的文件,會發現文件內容並沒有發生更改。如果需要保存 sed 編輯的內容,可以使用 -i 選項:
[tongye@localhost Shell_Program]$ sed -i 's/tongye/ttyezi/' test.txt [tongye@localhost Shell_Program]$ cat test.txt hello,my name is ttyezi I want to write a program named HelloWorld.c now,let`s begin #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple writed by ttyezi endView Code
也可以使用輸出重定向的方式,將編輯後的內容保存到一個新的文本文件中而不是輸出到標準輸出:
[tongye@localhost Shell_Program]$ sed 's/hello/HELLO/' test.txt > test1.txt [tongye@localhost Shell_Program]$ cat test1.txt HELLO,my name is ttyezi I want to write a program named HelloWorld.c now,let`s begin #include "stdio.h" main(){ printf("Hello world"); } oh,it`s symple writed by ttyezi endView Code
三、awk 命令
3.1 awk 的基本模式與操作
shell 中提供 awk 命令來重新編排欄位。實際上, awk 本身所提供的功能十分完備,已經是一種很好用的程式語言了,這裡暫且只討論它在 shell 腳本中的一些長處:文本處理功能。awk 命令的基本模式如下:
awk 'program' [ file ...]
awk 讀取命令行上所指定的各個文件(若沒有文件,則為標準輸入),一次讀取一條記錄(行),再針對每一行,應用 program 所指定的命令。 awk程式(program)基本架構為:
pattern {action}
pattern {action}
...
pattern 部分幾乎可以是任何表達式,但是在單命令行程式里,它通常是由斜杠括起來的 ERE。 action 為任意的 awk 語句,但是在單命令行程式里,通常是一個直接明瞭的 print 語句。pattern 或 action 都可以省略(不要全部省略)。省略 pattern,則會對每一條輸入記錄執行 action;省略 action 則預設 action 為{ print } ,將列印顯示整條記錄。
[tongye@localhost etc]$ awk '/root/ {print}' /etc/passwd root:x:0:0:root:/root:/bin/bash operator:x:11:0:operator:/root:/sbin/nologin
上述指令將列印文本中所有包含 root 的行。
3.2 欄位
awk 設計的重點就在欄位與記錄上:awk 讀取輸入記錄(通常是一些行),然後自動將各個記錄切分為欄位。awk 將每條記錄內的欄位數目,存儲到內建變數 NF 中。awk 預設以空白分隔欄位(如空格符、製表符),不過,也可以設置成其他的,通過設置 FS 變數(列數據分隔符)。如果需要使用欄位值,可以使用 $ 字元來引用,$1表示第一個欄位值、$2表示第二個欄位值、... 另外,$0表示整條記錄。舉個例子驗證一下:
[tongye@localhost Shell_Program]$ awk '{print $1}' test.txt hello,my I now,let`s #include main(){ printf("Hello } oh,it`s writed end
這句指令將會按照預設的分隔符(空白符)來對欄位進行劃分,並列印第一個欄位的值到標準輸出。
3.3 設置欄位分隔符
可以使用 -F 選項來修改欄位分隔符。-F 選項會自動的設置 FS 變數,只需將 FS 變數放到 -F 選項後面即可。FS 變數可以被設置為單個字元(此時,只要該字元出現一次,就分隔出一個欄位),也可以被設置為一個完整的 ERE(此時,每一個匹配該 ERE 的文本都將被視為欄位分隔符):
awk -F: '{print $1,$2}' /etc/passwd
這段指令將使用冒號 : 作為欄位分隔符去處理 /etc/passwd 文件,然後輸出文件的第1、第2個欄位到標準輸出。
-F 選項設置的欄位分隔符是相對於輸入而言的。而 awk 的輸入、輸出分隔符用法是分開的,因此即使使用 -F 選項設置了 FS 變數,輸出的欄位分隔符還是預設的空白符,這樣可能會影響結果判斷,使用 -v 選項可以設置輸出欄位分隔符,通過改變 OFS 變數(列數據輸出分隔符)的值,使用形式也 -F 選項有所區別:
awk -v 'OFS=/' '{print $1,$2}' /etc/passwd
-v 選項的用法如上,該指令的將把 awk 的輸出與分隔符設置為斜杠符 / 。舉一個例子:
[tongye@localhost Shell_Program]$ awk -F '/..' -v 'OFS=/' '{ print $1,$2,$3 }' /etc/passwd root:x:0:0:root:/ot:/n bin:x:1:1:bin:/n:/in daemon:x:2:2:daemon:/in:/in adm:x:3:4:adm:/r/m: lp:x:4:7:lp:/r/ool sync:x:5:0:sync:/in:/n shutdown:x:6:0:shutdown:/in:/in halt:x:7:0:halt:/in:/in mail:x:8:12:mail:/r/ool operator:x:11:0:operator:/ot:/in games:x:12:100:games:/r/mes: ftp:x:14:50:FTP User:/r/p: nobody:x:99:99:Nobody:/sbin/login systemd-network:x:192:192:systemd Network Management:/sbin/login dbus:x:81:81:System message bus:/sbin/login polkitd:x:999:998:User for polkitd:/sbin/login tss:x:59:59:Account used by the trousers package to sandbox the tcsd daemon:/v/ll: abrt:x:173:173::/c/rt: sshd:x:74:74:Privilege-separated SSH:/r/pty postfix:x:89:89::/r/ool chrony:x:998:996::/r/b tongye:x:1000:1000:tongye:/me/ngye:
這段指令使用正則表達式 /.. 作為段分隔符去處理 /etc/passwd 文件,文件中每個匹配該正則表達式的文本都被視為一個欄位分隔符。然後使用斜杠符 / 作為輸出欄位分隔符,並將第1、2、3個欄位輸出到標準輸出。
3.4 列印行 print 與 printf
print 上面已經用到過,這是 awk 裡面最常使用的一條語句,可以用來進行簡單的列印工作。print 的參數可以是欄位列表、變數或者字元串:
[tongye@localhost Shell_Program]$ awk -F: -v 'OFS=:' '{print "username is",$1}' /etc/passwd
print 命令將後面的參數一個一個列印到標準輸出,如果沒有後接參數,則預設參數為 $0,將列印整條記錄。註意,print 的參數之間需要用逗號隔開,否則輸出結果將會連到一起沒有間隔。
對於上面的語句,print 後面的參數混合了字元串和變數,當參數較多時,寫起來會比較不方便。此時,可以使用 printf 語句來替代 print 語句。printf 語句可以將所有參數放到一對雙引號中去,與 C 中的 printf 用法類似:
[tongye@localhost Shell_Program]$ awk -F: '{printf "username is %s\n",$1}' /etc/passwd username is root username is bin username is daemon username is adm username is lp username is sync username is shutdown username is halt username is mail username is operator username is games username is ftp username is nobody username is systemd-network username is dbus username is polkitd username is tss username is abrt username is sshd username is postfix username is chrony username is tongye
需要註意的是,awk 的 print 語句會自動提供換行符,而 printf 語句不能,需要自己提供 \n 來進行換行。
參考資料:
《Linux 程式設計 第四版》
《Shell 腳本學習指南》
《UNIX/Linux/OS X 中的 Shell 編程 第四版》