天天看點

【系統工程師的自我修養】sed篇

注:本文除特殊注明外均針對傳統UNIX中的sed,而非GNU的版本,以保證通用性,GNU的新特性本文暫不進行介紹,請參看手冊。另外,本文對sed的講述和總結不為求全面,隻求實用性和适用性強。有需要還是參閱man手冊和sed相關資料。

1.原理篇

掌握這個東西首先需要掌握的就是原理,否則一切技巧都是白扯。sed以行為處理機關,預設輸入輸出均為系統标準輸入輸出(是以除非重定向,否則它并不真正修改檔案),它首先判斷要處理的行是否在要處理的範圍之内(下一章中稱之為SELECTION),如果是則讀入pattern space中,這是sed進行字元串處理工作的一個區域。腳本中的sed指令逐條執行來編輯pattern space裡面的字元串,執行完畢後将該pattern space中處理過的字元串進行輸出,随之pattern space被清空;接着,再重複執行剛才的動作,檔案中的新的一行被讀入,判斷是否在SELECTION中,編輯、輸出,直到檔案處理完畢,整個過程如下圖所示。除了 pattern space 外,sed還有一個 hold space,用處是暫存文字字元串的地方,hold space中的字元串隻是用于臨時處理的中間結果,是不會被輸出的(在本文第四章會有介紹,此時不了解不影響閱讀此文)。

【系統工程師的自我修養】sed篇

2.用途篇

學習一個腳本語言,了解了基本原理後就要調研一下這個東東是不是滿足完成你任務的需求。一提到sed,肯定就會牽扯到awk,它具體的功能會在後邊的awk篇進行叙述。對于同一個任務,sed和awk都有可能解決,是以對于sed、awk的用途每個人都有不同的習慣和自己擅長的用法,在實踐中,我個人習慣于用sed進行行處理,也就是根據sed的原理,需要進行一行行的處理操作時優先使用sed。而用awk更多的進行列處理。而在具體任務上,由于sed強大的替換能力和編輯能力,我常常用sed作為編輯器,而awk 作為資訊擷取和格式處理輸出的能力。

3.初級文法篇

作為一個腳本類工具,确定就要用它完成任務後,要開始真正使用它,掌握其語言特性是必不可少的。形式上,使用sed采用如下指令格式:

sed [options] 'SELECTION edit-instructions'  file(s)

從指令格式可以看出sed可以一次處理多個文本。此處先不介紹options,因為options往往要和後邊的command進行配合而才能展現出價值。下邊,我們先從command開始了解。

根據是否使用hold space(不懂這個概念也可先往下看,讀完本文就明白)的差別,對于一個初級使用者,了解如下sed中不使用hlod space 的command是實踐的第一步。本文在此基礎上以解決問題的思路首先來介紹不使用hold space 的指令,另外需要說明的是正規表達式是另一個配合使用的利器,在本文中為了不引入更大的麻煩,是以用例中盡量使用最簡答的正規表達式。

1)範圍選取

在sed中如果不指定範圍,則處理指令是針對整個标準輸入的。如需要在某個範圍内進行處理,則需要進行範圍選取。也就是指令格式中的SELSECTION。sed根據SELECTION取得相應的文本行,在這些行中根據edit-instructions進行編輯。注意,SELECTION和edit-instructions中的空格不是必須的。

SELECTION 可以如下表達:

單個行号:如1為取第一行,5為取第五行,$為取最後一行             

行範圍:如5,$   為取從第五行到最後一行之間的文本行

單個正則比對:如/string/ 為取包含string的行

一個正則比對範圍:如/^on/,/off$/ 為取從on開頭的行到off結尾的行之間(含這兩個比對行)所包含的文本行。

行範圍與正則比對範圍集合:如10,/man/表示從第10行到包含有man的行之間的文本

除去所比對行外的範圍:如/Llew/! 表示除了比對Llew的行外其餘的文本行

在進入處理指令前,先介紹一下本文中的幾個示例檔案

phonelist:

JCHJCL01:/tmp/gnuhpc#cat phonelist Terrell, Terry 617-7989 Franklin, Francis 704-3876 Patterson, Pat 614-6122 Robinson, Robin 411-3745 Christopher, Chris 305-5981 Martin, Marty 814-5587 Llewellyn, Lynn 316-6221 Jansen, Jan 903-3333 Llewellyn, Lee 817-8823

paths:

JCHJCL01:/tmp/gnuhpc#cat paths /opt/virtprovider/lib /var/adm/syslog /usr/bin/ /usr/local/bin

config.ini:

JCHJCL01:/tmp/gnuhpc#cat config.ini [WQMInfo] ProxyMode=0 Proxy= RunMode=1 LastStatisticTime=2012-06-17 RecordMode=0 RecordKeyBoard=1 RecordMouseClick=1 RecordMouseMove=0 QMPath= [BROWSE_MODE] ShowToolBar=0 ShowStatusBar=0 AutoCloseDialog=0 MinimizeToHide=0 [DEVELOP_MODE] ShowToolBar=1 ShowStatusBar=1 MinimizeToHide=1 [RUN_MODE] AutoCloseDialog=1 JCHJCL01:/tmp/gnuhpc#

2)列印指令

明确如何界定需要處理文本後,首先學習一個簡單的指令:列印這部分文本。

基本文法:SELSECTIONp 列印pattern space中的内容

Task1:列印包含Franklin的行

JCHJCL01:/tmp/gnuhpc#sed -n '/Franklin/p' phonelist

注意,選項-n 表示所有都不列印,而僅僅列印出比對的行,可以試一試沒有這個選項的情況。回顧sed機制,它會将文本一行行放在pattern space,不管你做什麼樣的後續操作、甚至不做任何編輯動作,它都會在command執行完後把pattern space打出來,這你就了解了為什麼要用這個選項。

3)處理指令

a)  增改操作:

基本文法:

SELECTIONx\

text

其中斜杠後有回車,而x則為:

i 表示插入選中行前

a 表示追加在選中行之後

c 表示将選中行修改為text

Task2:在第二行前插入一個聯系人Jonney, Wang 923-3322

JCHJCL01:/tmp/gnuhpc#sed '2i\                    > Jonney, Wang 923-3322' phonelist Jonney, Wang 923-3322

Task3:在Martin, Marty後加入聯系人Jonney, Wang 923-3322

JCHJCL01:/tmp/gnuhpc#sed '/Martin, Marty/a\

Task3:将名字為Llewellyn的記錄都記為“BANNED”

JCHJCL01:/tmp/gnuhpc#sed '/Llewellyn/c\            BANNED' phonelist BANNED

b)删除操作

SELECTIONd ,清除pattern space中的所有内容

Task4 删除最後一行:

JCHJCL01:/tmp/gnuhpc#sed '$d' phonelist

c)替換操作:

'SELECTION s/old string/new string/’ 替換所選區域中第一次出現的old string

'SELECTION s/old string/new string/g’ 替換所選區域中所有的old string

'SELECTION y/string1/string2/’ 對所選區域中的string1所含字元對應替換為string2中同位置的字元,與tr指令相同。

Task5:将第一個Robin替換為Robbins

JCHJCL01:/tmp/gnuhpc#sed 's/Robin/Robbins/' phonelist Robbinsson, Robin 411-3745

Task6:将所有Rob替換為John

JCHJCL01:/tmp/gnuhpc#sed 's/Rob/John/g' phonelist Johninson, Johnin 411-3745

Task7:将/usr/bin/中的/bin/替換為/bin

JCHJCL01:/tmp/gnuhpc#sed 's/\/bin\//\/bin/' paths /usr/bin

在這種出現很多/的檔案時需要\來進行轉義,稍微一多就容易出錯,那麼采用如下的方式把替換分隔符的方式進行就好,其中感歎号隻是一個其他類字元,換做另外一個字元(例如@)也是沒有關系的:

JCHJCL01:/tmp/gnuhpc#sed 's!/bin/!/bin!' paths

Task8:加密所有的1234,規則為将檔案中1、2、3、4對應改為A、B、C、D:

JCHJCL01:/tmp/gnuhpc#sed 'y/1234/ABCD/' phonelist Terrell, Terry 6A7-7989 Franklin, Francis 70D-C876 Patterson, Pat 6AD-6ABB Robinson, Robin DAA-C7D5 Christopher, Chris C05-598A Martin, Marty 8AD-5587 Llewellyn, Lynn CA6-6BBA Jansen, Jan 90C-CCCC Llewellyn, Lee 8A7-88BC

d)寫檔案操作:

基本文法:'SELECTION command/w filename’

Task9:将所有Rob 改為Robbin,并将結果寫到一個叫做result 的檔案中

JCHJCL01:/tmp/gnuhpc#sed 's/Rob/Robbin/gw result' phonelist Robbininson, Robbinin 411-3745 JCHJCL01:/tmp/gnuhpc#cat result

e)讀檔案操作:

基本文法:'SELECTION command/r filename’

Task10:如果phonelist中存在Patterson,則将檔案paths的内容加入到Patterson後的那一行

JCHJCL01:/tmp/gnuhpc#sed '/Patterson/r paths' phonelist

f)批處理操作:

如果要處理的很多,我們也可以将sed指令寫入一個腳本,然後運作時采用-f選項指定運作該腳本就行。請注意sed會将第一條指令執行的結果發給第二條執行,是以指令的順序尤為重要。

基本文法:sed -f scriptfile  filename

Task11:把617替換為817,把704替換為522,把411替換為235

JCHJCL01:/tmp/gnuhpc#cat subscript s/617/817/ s/704/522/ s/411/235/ JCHJCL01:/tmp/gnuhpc#sed -f subscript phonelist Terrell, Terry 817-7989 Franklin, Francis 522-3876 Robinson, Robin 235-3745

也可以使用多行操作模式,基本文法為:

'SELECTION1 operation1

SELECTIONn operationn'

其實就是把多個指令用回車連起來

Task12:将Martin替換Mary,将Tearrey替換為Tearrey

JCHJCL01:/tmp/gnuhpc#sed 's/Martin/Mary/ s/Terrell/Tearrey/' phonelist Tearrey, Terry 617-7989 Mary, Marty 814-5587

另一種方式是使用-e選項,基本文法為:

-e ‘command1’ –e ‘command2’

同樣的任務:

JCHJCL01:/tmp/gnuhpc#sed -e 's/Martin/Mary/' -e 's/Terrell/Tearrey/' phonelist

還有另外一種方法,有點類似于C語言中分号的使用:

JCHJCL01:/tmp/gnuhpc#sed 's/Martin/Mary/;s/Terrell/Tearrey/' phonelist  

我一般習慣于采用分号,簡單也夠明了。另外,對于同一個區域,還可以使用{}進行處理:

Task13:将含有QM的行中QM改為PM,=号改為“:”

JCHJCL01:/tmp/gnuhpc#sed '/QMPath/{s/QM/PM/ > s/=/:/ > }' config.ini PMPath:

4.進階文法篇之hold space的使用

開篇提到了這個hold space,再複習一遍:Hold space 是 sed 用來暫存 pattern space 内容的一個臨時空間。在進行中,有時我們希望保留pattern space的内容在下一次進行處理,是以sed的開發者設計實作了hold space,并且提供了很多指令在pattern space和hold space之間進行複制。記憶上,g和G都是get 的意思,表示從hold space取出放回pattern space,而h和H都是hold的意思,也就是從pattern space到hlod space。

g :将hold space中的内容拷貝到pattern space中,原來pattern space裡的内容清除

G:pattern space末尾加上換行符後将hold space中的内容append到pattern space中

h:将pattern space中的内容拷貝到hold space中,原來的hold space裡的内容被清除

H:hold space末尾加上換行符後将pattern space中的内容append到hold space中

x :交換 hold space 與 pattern space 内容

Task13:倒置phonelist

我們可以拿一個簡單的檔案來理清思路:

A

B

C

D

如下圖:

【系統工程師的自我修養】sed篇

除了第一行和最後一行處理不一樣以外(第一行隻執行h,而最後一行隻執行G),其餘行都是用G、d和h(使用d 的原因是不把中間結果輸出)。在sed中有個操作是!,也就是程式設計語言中的“非”,即不執行,是以,我們可以寫出sed指令來倒置一個檔案:

JCHJCL01:/tmp/gnuhpc#sed '1!G;h;$!d' phonelist

5.進階文法篇之元字元的使用

sed有幾個很NB的元字元,這部分往往與正規表達式一起使用能夠得到事半功倍的效果。

& : 代表SELECTION中比對的部分,常用于某個子字元串前後添加字元的操作

\num : num代表比對子字元串的序号,從1開始,\num表示比對的子字元串(正規表達式中稱為分組),其中子字元串的比對模式是由圓括号及其轉義字元構成。

Task 15:将每個電話号碼前加上Tel:

JCHJCL01:/tmp/gnuhpc#sed '/[0-9]\{3\}-[0-9]\{4\}/s//Tel: &/g' phonelist Terrell, Terry Tel: 617-7989 Franklin, Francis Tel: 704-3876 Patterson, Pat Tel: 614-6122 Robinson, Robin Tel: 411-3745 Christopher, Chris Tel: 305-5981 Martin, Marty Tel: 814-5587 Llewellyn, Lynn Tel: 316-6221 Jansen, Jan Tel: 903-3333 Llewellyn, Lee Tel: 817-8823

可以看到前邊SELECTION是一個正規表達式來比對電話号碼,也就是0-9三位數-0-9四位數這樣一個比對邏輯,關鍵是元字元的使用,&代表了比對的這串電話号碼,在前邊加上Tel:就是件很随意的事情了。

Task16:電話号碼更新,從原來的四位數統一更新為五位數,6開頭。

JCHJCL01:/tmp/gnuhpc#sed 's/\([0-9]\{3\}\)-\([0-9]\{4\}\)/\1-6\2/g' phonelist Terrell, Terry 617-67989 Franklin, Francis 704-63876 Patterson, Pat 614-66122 Robinson, Robin 411-63745 Christopher, Chris 305-65981 Martin, Marty 814-65587 Llewellyn, Lynn 316-66221 Jansen, Jan 903-63333 Llewellyn, Lee 817-68823

Task17:将paths檔案中的路徑用逗号連起來。首先,将每一行都放入hold space(以\n連接配接起來)而這個中間過程不顯示(也就是$!d所表示的除非處理到最後一行,否則都把pattern space删掉),随後在到最後一行時将hold space中的内容放回pattern space(此處用了x,其實g也是可以的),并且把開頭的\n去掉後将剩餘的\n替換為,最後列印。

JCHJCL01:/tmp/gnuhpc#sed 'H;$!d;${ > x > s/^\n// > s/\n/,/g > }' paths /opt/virtprovider/lib,/var/adm/syslog,/usr/bin/,/usr/local/bin

6.進階文法篇之改變處理流程操作

有時我們希望對比對的下一行或多行進行操作,有時我們又希望在處理完畢後馬上退出(因為sed會對讀入文本的每一行進行操作,即使肉眼看起來明顯不比對也是需要sed先把字元串load進來與SELECTION對照判斷的),此時就需要改變處理流程。

n:将之前讀入的行(也就是在pattern space中的行)輸出到螢幕,然後為将下一行的内容提前讀入pattern space(替換上邊已經列印的行),後續的指令會應用到新讀入的行上。

q:  使用時前邊加行号n,表示取前n行,這個在讀取大檔案的前幾行時有很大的作用。

N:将下一行的内容讀取并追加到目前模式空間中(用換行符作為連接配接),并沒有輸出目前模式空間中的行。注意pattern space含有多行時,正規表達式符号^和$含義分别為^比對模式空間的最開始,而$是比對模式空間的最後位置。

D:該指令删除模式空間中從第一個字元到第一個換行符的内容,并且跳轉到指令開頭重新執行。注意,當模式空間仍有内容時,不讀入新的輸入行,類似形成一個循環。

P:僅列印模式空間中從第一個字元到第一個換行符的内容,重新在模式空間的内容上執行編輯指令,類似形成一個循環。

:label和b label  : 标注一個标簽并跳轉。例如,下面的例子中模拟了一個if操作存在符合pattern則跳過command2直接執行command3

command1

/pattern/b goto

command2

:goto

command3

而下邊的例子則模拟了一個if else操作,符合pattern時執行command3,不符合時執行command3

/pattern/b dosomething

b

:dosomething

Task18:将QMPath後邊的空行删掉

JCHJCL01:/tmp/gnuhpc#sed '/QMPath/{n > /^$/d

Task19: 取一個大檔案的前兩行,可以看到同樣都是取前兩行,由于file這個檔案較大,最終導緻效率的差别是幾十倍。

JCHJCL01:/tmp/gnuhpc#ls -l file             -rw-r--r--    1 root     system     78888888 Jan 25 23:20 file JCHJCL01:/tmp/gnuhpc#time sed -n '1,2p' file 1 2 real    0m0.78s user    0m0.35s sys     0m0.13s JCHJCL01:/tmp/gnuhpc#time sed '2q' file     real    0m0.01s user    0m0.00s sys     0m0.00s

Task20:将多個連續空行縮減為一個空行,非連續空行保留。$q表示最後一行不進行處理,因為由于前邊的處理,到了最後一行已經不會出現連續空行了。其餘的處理邏輯為:比對空行,讀入下一行,發現下一行還是空行後删除第一個空行然後繼續讀、處理直到下一行不是空行為止。

sed '/^$/{$q N /^\n$/D }' config.ini

6.sed技巧拾零篇

本來想介紹一下正則表達,無奈太博大精深,幾個例子也說明不了太多,是以正則表達部分可以參考sed手冊,裡面有比較詳盡的講解。此處舉一些常用和手冊上沒有提及的用例。

[:alnum:]:表示所有的字母和數字

[:digit:]: 表示所有數字

[:upper:]: 表示所有的大寫字母

[:lower:] :表示所有的小寫字母

Task21:去掉config.ini中的數字,使得config.ini變為一個配置檔案模闆

JCHJCL01:/tmp/gnuhpc#sed 's/[[:digit:]]//g' config.ini ProxyMode= RunMode= LastStatisticTime=-- RecordMode= RecordKeyBoard= RecordMouseClick= RecordMouseMove= ShowToolBar= ShowStatusBar= AutoCloseDialog= MinimizeToHide=

利用SELECTION進行取符合比對條件的連續多行:

Task22:取得config.ini中WQMInfo段

JCHJCL01:/tmp/gnuhpc#sed -n '/WQMInfo/,/^$/p' config.ini

取比對條件的上N行和下N行:

Task23:取得通訊錄中Martin上邊的一個人的記錄,讀入下一行到模式空間,并且判斷是否包含Lynn,如果比對,則列印模式空間中的第一行,如果不比對,則删除模式空間的第一行,循環處理。

JCHJCL01:/tmp/gnuhpc#sed -n '$!N;/Lynn/!D;/Lynn/P' phonelist 

Task24:取得Martin上邊包含Martin的所有人記錄:

JCHJCL01:/tmp/gnuhpc#sed -n '1,/Martin/p' phonelist  

Task25:取得Martin上邊包含Martin的三條記錄:

JCHJCL01:/tmp/gnuhpc#sed -n '1,/Martin/p' phonelist | tail -3

Task26:取得通訊錄中Martin下邊的一個人的記錄:

JCHJCL01:/tmp/gnuhpc#sed -n '/Martin/{n > p > }' phonelist

Task27:取得Martin下邊包含Martin的所有人記錄:

JCHJCL01:/tmp/gnuhpc#sed -n '/Martin/,$p' phonelist

Task28:取得Martin下邊包含Martin的三條記錄:

JCHJCL01:/tmp/gnuhpc#sed -n '/Martin/,$p' phonelist | head -3

注釋掉某些行:

Task29:假設ini檔案行注釋為前後兩個感歎号,請注釋掉RecordMouseMove

JCHJCL01:/tmp/gnuhpc#sed 's/^RecordMouseMove.*/!!&!!/' config.ini !!RecordMouseMove=0!!

Task30:假設paths檔案用#進行注釋,則注釋掉含有usr的行

JCHJCL01:/tmp/gnuhpc#sed 's/.*usr.*/#&/' paths #/usr/bin/ #/usr/local/bin

一個更簡單的方法是:

JCHJCL01:/tmp/gnuhpc#sed '/usr/s/^/#/' paths

Task31:在paths中每行加一個行号和冒号

JCHJCL01:/tmp/gnuhpc#sed = paths  | sed 'N;s/\n/:/' 1:/opt/virtprovider/lib 2:/var/adm/syslog 3:/usr/bin/ 4:/usr/local/bin

Task32: 處理XML

JCHJCL01:/tmp/gnuhpc#echo "<Amount>10kg</Amount>" | sed 's#\(<Amount>\)[0-9,a-z]*\(</Amount>\)#\1'100kg'\2#g' <Amount>100kg</Amount>

Task33:替代一行中多個比對模式中的其中一個。對于下邊的解釋:前者替換倒數第二個比對;後者替換最後一個比對 。

sed 's/(.*)foo(.*foo)/1bar2/ test.txt sed 's/(.*)foo/1bar/' test.txt

Task34:删除paths的最後2行,在讀入第一行後,使用N讀入一行,并且如果新讀入的下一行不是最後一行,則列印模式空間中的第一行,并且删除,然後接着執行N;如果新讀入的一行是檔案的最後一行,則删除模式空間中的所有内容(此時pattern space中即為倒數兩行)。

JCHJCL01:/tmp/gnuhpc#sed 'N;$!P;$!D;$d' paths

小結篇

本文已幾個文本檔案為例子,說明了sed的基本用法指南,由于我本人傾向于腳本要具有高度的可移植性,另外同一個任務不一定都要交給一個工具完成,多個工具配合使用,在不太考慮性能的前提下,simpler better,是以諸多進階用法和GNU sed的用法均在此文沒有涉及。