---簡介 bash
在這篇 sed 系列的總結性文章中,Daniel Robbins 帶您體驗 sed 的真正力量。在介紹完幾個重要的 sed 腳本以後,他將經過將一個 Quicken .QIF 文件轉換成可讀文本格式來演示一些基本 sed 腳本的編寫。該轉換腳本不只實用,並且仍是展示 sed 腳本編寫能力的極佳示例。 ui
---強健的 sed spa
在 第二篇 sed 文章中,我提供了一些示例來演示 sed 的工做原理,可是它們當中不多有示例能實際作特別 有用的事。在這篇 sed 系列的最後文章中,我要改變那種方式,並使用 sed 來作實際的事。我將爲您顯示幾個示例,它們不只演示 sed 的能力,並且還作一些真正巧妙(和方便)的事。例如,在本文的後半部,將爲您演示如何設計一個 sed 腳原本將 .QIF 文件從 Intuit 的 Quicken 金融程序轉換成具備良好格式的文本文件。在那樣作以前,咱們將看一下不怎麼複雜但卻頗有用的 sed 腳本。 設計
---文本轉換 unix
第一個實際腳本將 UNIX 風格的文本轉換成 DOS/Windows 格式。您可能知道,基於 DOS/Windows 的文本文件在每一行末尾有一個 CR(回車)和 LF(換行),而 UNIX 文本只有一個換行。有時可能須要將某些 UNIX 文本移至 Windows 系統,該腳本將爲您執行必需的格式轉換。 code
$ sed -e 's/$/\r/' myunix.txt > mydos.txt |
在該腳本中,'$' 規則表達式將與行的末尾匹配,而 '\r' 告訴 sed 在其以前插入一個回車。在換行以前插入回車,當即,每一行就以 CR/LF 結束。請注意,僅當使用 GNU sed 3.02.80 或之後的版本時,纔會用 CR 替換 '\r'。若是尚未安裝 GNU sed 3.02.80,請在個人 第一篇 sed 文章中查看如何這樣作的說明。 regexp
我已記不清有多少次在下載一些示例腳本或 C 代碼以後,卻發現它是 DOS/Windows 格式。雖然不少程序不在意 DOS/Windows 格式的 CR/LF 文本文件,可是有幾個程序卻在意 -- 最著名的是 bash,只要一遇到回車,它就會出問題。如下 sed 調用將把 DOS/Windows 格式的文本轉換成可信賴的 UNIX 格式: ci
$ sed -e 's/.$//' mydos.txt > myunix.txt |
該腳本的工做原理很簡單:替代規則表達式與一行的最末字符匹配,而該字符剛好就是回車。咱們用空字符替換它,從而將其從輸出中完全刪除。若是使用該腳本並注意到已經刪除了輸出中每行的最末字符,那麼,您就指定了已是 UNIX 格式的文本文件。也就不必那樣作了! 開發
---反轉行 字符串
下面是另外一個方便的小腳本。與大多數 Linux 發行版中包括的 "tac" 命令同樣,該腳本將反轉文件中行的次序。"tac" 這個名稱可能會給人以誤導,由於 "tac" 不反轉行中字符的位置(左和右),而是反轉文件中行的位置(上和下)。用 "tac" 處理如下文件:
foo bar oni |
....將產生如下輸出:
oni bar foo |
能夠用如下 sed 腳本達到相同目的:
$ sed -e '1!G;h;$!d' forward.txt > backward.txt |
若是登陸到恰巧沒有 "tac" 命令的 FreeBSD 系統,將發現該 sed 腳本頗有用。雖然方便,但最好仍是知道該腳本爲何那樣作。讓咱們對它進行討論。
---反轉解釋
首先,該腳本包含三個由分號隔開的單獨 sed 命令:'1!G'、'h' 和 '$!d'。如今,須要好好理解用於第一個和第三個命令的地址。若是第一個命令是 '1G',則 'G' 命令將只應用第一行。然而,還有一個 '!' 字符 -- 該 '!' 字符 忽略該地址,即,'G' 命令將應用到除第一行以外的 全部行。'$!d' 命令與之相似。若是命令是 '$d',則將只把 'd' 命令應用到文件中的最後一行('$' 地址是指定最後一行的簡單方式)。然而,有了 '!' 以後,'$!d' 將把 'd' 命令應用到除最後一行以外的 全部行。如今,咱們所要理解的是這些命令自己作什麼。
當對上面的文本文件執行反轉腳本時,首先執行的命令是 'h'。該命令告訴 sed 將模式空間(保存正在處理的當前行的緩衝區)的內容複製到保留空間(臨時緩衝區)。而後,執行 'd' 命令,該命令從模式空間中刪除 "foo",以便在對這一行執行完全部命令以後不打印它。
如今,第二行。在將 "bar" 讀入模式空間以後,執行 'G' 命令,該命令將保留空間的內容 ("foo\n") 附加到模式空間 ("bar\n"),使模式空間的內容爲 "bar\n\foo\n"。'h' 命令將該內容放回保留空間保護起來,而後,'d' 從模式空間刪除該行,以便不打印它。
對於最後的 "oni" 行,除了不刪除模式空間的內容(因爲 'd' 以前的 '$!')以及將模式空間的內容(三行)打印到標準輸出以外,重複一樣的步驟。
如今,要用 sed 執行一些強大的數據轉換。
---sed QIF 魔法
過去幾個星期,我一直想買一份 Quicken來結算個人銀行賬戶。Quicken 是一個很是好的金融程序,固然會成功地完成這項工做。可是,通過考慮以後,我以爲本身能夠輕易編寫某個軟件來結算個人支票簿。我想,畢竟,我是個軟件開發人員!
我開發了一個很好的小型支票簿結算程序(使用 awk),它經過分析包含個人全部交易的文本文件的語法來計算餘額。略微調整以後,我將其改進,以即可以象 Quicken 那樣跟蹤不一樣的貸款和借款類別。可是,我還要添加一個特性。最近,我將賬戶轉移到一家有聯機 Web 賬戶界面的銀行。有一天,我注意到,這家銀行的 Web 站點容許以 Quicken 的 .QIF 格式下載個人賬戶信息。我立刻以爲,若是能夠將該信息轉換成文本格式,那就太棒了。
---兩種格式的故事
在查看 QIF 格式以前,先看一下個人 checkbook.txt 格式:
28 Aug 2000 food - - Y Supermarket 30.94 25 Aug 2000 watr - 103 Y Check 103 52.86 |
在個人文件中,全部字段都由一個或多個製表符分開,每一個交易佔據一行。日期以後的下一個字段列出支出類型(若是是收入項,則爲 "-")。第三個字段列出收入類型(若是是支出項,則爲 "-")。而後,是一個支票號字段(若是爲空,則仍是 "-"),一個交易完成字段("Y" 或 "N"),一個註釋和一個美圓金額字段。如今,讓咱們看一下 QIF 格式。當用文本查看器查看下載的 QIF 文件時,它看起來以下:
!Type:Bank D08/28/2000 T-8.15 N PCHECKCARD SUPERMARKET ^ D08/28/2000 T-8.25 N PCHECKCARD PUNJAB RESTAURANT ^ D08/28/2000 T-17.17 N PCHECKCARD SUPERMARKET |
瀏覽過文件以後,不難猜出其格式 -- 忽略第一行,其他的格式以下:
D<數據> T<交易量> N<支票號> P<描述> ^ (這是字段分隔符) |
---開始處理
在處理象這樣重要的 sed 項目時,不要氣餒 -- sed 容許您將數據逐漸修改爲最終形式。在進行當中,能夠繼續細化 sed 腳本,直到輸出與預期的徹底同樣爲止。無需在試第一次時就保證其徹底正確。
要開始,首先建立一個名爲 "qiftrans.sed" 的文件,而後開始修改數據:
1d /^^/d s/[[:cntrl:]]//g |
第一個 '1d' 命令刪除第一行,第二個命令從輸出除去那些討厭的 '^' 字符。最後一行除去文件中可能存在的任何控制字符。既然在處理外來文件格式,我想消除在中途遇到任何控制字符的風險。到目前爲止,一切順利。如今,要向該基本腳本中添加一些處理功能:
1d /^^/d s/[[:cntrl:]]//g /^D/ { s/^D\(.*\)/\1\tOUTY\tINNY\t/ s/^01/Jan/ s/^02/Feb/ s/^03/Mar/ s/^04/Apr/ s/^05/May/ s/^06/Jun/ s/^07/Jul/ s/^08/Aug/ s/^09/Sep/ s/^10/Oct/ s/^11/Nov/ s/^12/Dec/ s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3: } |
首先,添加一個 '/^D/' 地址,以便 sed 只在遇到 QIF 數據字段的第一個字符 'D' 時纔開始處理。當 sed 將這樣一行讀入其模式空間時,將按順序執行花括號中的全部命令。
花括號中的第一個命令將把以下行:
D08/28/2000 |
變換成:
08/28/2000 OUTY INNY |
固然,如今的格式還不完美,但不要緊。咱們將在進行過程當中逐漸細化模式空間的內容。後面 12 行的最後效果是將數據變換成三個字母的格式,最後一行從數據中除去三個斜槓。最後獲得這一行:
Aug 28 2000 OUTY INNY |
OUTY 和 INNY 字段是佔位符,之後將被替換。如今還不能肯定它們,由於若是美圓金額爲負,將把 OUTY 和 INNY 設置成 "misc" 和 "-",可是,若是美圓金額爲正,將分別把它們更改爲 "-" 和 "inco"。既然尚未讀入美圓金額,因此,須要暫時使用佔位符。
---細化
如今進一步細化:
1d /^^/d s/[[:cntrl:]]//g /^D/ { s/^D\(.*\)/\1\tOUTY\tINNY\t/ s/^01/Jan/ s/^02/Feb/ s/^03/Mar/ s/^04/Apr/ s/^05/May/ s/^06/Jun/ s/^07/Jul/ s/^08/Aug/ s/^09/Sep/ s/^10/Oct/ s/^11/Nov/ s/^12/Dec/ s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3: N N N s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/ s/NUMNUM/-/ s/NUM\([0-9]*\)NUM/\1/ s/\([0-9]\),/\1/ } |
後七行有些複雜,因此將詳細討論它們。首先,連續使用三個 'N' 命令。'N' 命令告訴 sed 將 下一行讀入輸入中,而後將其附加到當前模式空間。這三個 'N' 命令致使將下三行附加到當前模式空間緩衝區,如今這一行看起來以下:
28 Aug 2000 OUTY INNY \nT-8.15\nN\nPCHECKCARD SUPERMARKET |
sed 的模式空間變得很難看 -- 須要除去額外的新行,並執行某些附加的格式化。要這樣作,將使用替代命令。要匹配的模式爲:
'\nT.*\nN.*\nP.*' |
這將與後面依次跟有 'T'、零或多個字符、新行、'N'、任何數量的字符、新行、'P'、以及任何數量字符的新行匹配。呀!這個規則表達式將與剛剛附加到模式空間的三行的全 部內容匹配。但咱們要從新格式化該區域,而不是整個替換它。美圓金額、支票號(若是有的話)和描述須要出如今替換字符串中。要這樣作,咱們用帶有反斜槓的 圓括號括起那些「感興趣部分」,以即可以在替換字符串中引用它們(使用 '\1'、'\2\ 和 '\3' 來告訴 sed 將它們插入到何處)。如下是最後的命令:
s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/ |
該命令將咱們的行變換成:
28 Aug 2000 OUTY INNY NUMNUM Y CHECKCARD SUPERMARKET AMT-8.15AMT |
雖然該行正變得好一些,可是,有幾件事一看就有點...啊...有趣。首先是那個愚蠢的 "NUMNUM" 字符串 -- 其目的何在?若是查看 sed 腳本的後兩行,就會發現其目的,後兩行將把 "NUMNUM" 替換成 "-",而把 "NUM"<number>"NUM" 替換成 <number>。如您所見,用愚蠢的標記括起支票號容許咱們在該字段爲空時方便地插入一個 "-"。
---結束嘗試
最後一行除去數字後的逗號。它把如 "3,231.00" 這樣的美圓金額轉換成我使用的格式 "3231.00"。如今,讓咱們看一下最終腳本:
最終的「QIF 到文本」腳本
1d /^^/d s/[[:cntrl:]]//g /^D/ { s/^D\(.*\)/\1\tOUTY\tINNY\t/ s/^01/Jan/ s/^02/Feb/ s/^03/Mar/ s/^04/Apr/ s/^05/May/ s/^06/Jun/ s/^07/Jul/ s/^08/Aug/ s/^09/Sep/ s/^10/Oct/ s/^11/Nov/ s/^12/Dec/ s:^\(.*\)/\(.*\)/\(.*\):\2 \1 \3: N N N s/\nT\(.*\)\nN\(.*\)\nP\(.*\)/NUM\2NUM\t\tY\t\t\3\tAMT\1AMT/ s/NUMNUM/-/ s/NUM\([0-9]*\)NUM/\1/ s/\([0-9]\),/\1/ /AMT-[0-9]*.[0-9]*AMT/b fixnegs s/AMT\(.*\)AMT/\1/ s/OUTY/-/ s/INNY/inco/ b done :fixnegs s/AMT-\(.*\)AMT/\1/ s/OUTY/misc/ s/INNY/-/ :done } |
附加的十一行使用替代和一些分支功能來美化輸出。首先看一下這行:
/AMT-[0-9]*.[0-9]*AMT/b fixnegs |
該行包含一個格式爲 "/regexp/b label" 的分支命令。若是模式空間與規則表達式匹配,sed 將分支到 fixnegs 標號。您應該能夠輕易找到該標號,它在代碼中爲 ":fixnegs"。若是規則表達式不匹配,則以常規方式繼續處理下一個命令。
既然您理解該命令自己的工做原理,讓咱們看一下分支。若是看一下分支規則表達式,將看到它與後面依次跟有 '-'、任意數量的數字、一個 '.'、任意數量的數字和 'AMT' 的字符串 'AMT' 匹配。就象我確信您已猜到同樣,該規則表達式專門處理負的美圓金額。在這以前,用 'ATM' 括起美圓金額,以便之後能夠輕易找到它。由於規則表達式只與以 '-' 開始的美圓金額匹配,因此,該分支只在恰巧處理借款時才發生。若是正處理貸款,應該將 OUTY 設置成 'misc',將 INNY 設置成 '-',而且應該除去貸款數量前面的負號。若是跟蹤代碼的流程,將看到實際狀況正是這樣。若是不執行分支,則用 '-' 替換 OUTY,用 'inco' 替換 INNY。完成了!如今輸出行是完美的:
28 Aug 2000 misc - - Y CHECKCARD SUPERMARKET -8.15 |
---別犯糊塗
如您所見,只要按部就班地解決問題,使用 sed 轉換數據就沒有那麼難。不要試圖使用一個 sed 命令或一會兒解決全部問題。相反,要朝着目標逐步進行,並不斷改進 sed 腳本,直到其輸出正如您但願那樣爲止。sed 有許多功能,但願您已很是熟悉其內部工做原理並繼續努力以進一步掌握它!