在本文章系列中,Daniel Robbins 將為您演示如何使用功能十分強大(但常被遺忘)的 UNIX 流編輯器 sed。sed 是用批處理方式編輯文件或以十分有效的方式創建 shell 腳本以修改現有文件的理想工具。
挑選編輯器
在 UNIX 世界中有很多文本編輯器可供我們選擇。思考一下 -- vi、emacs 和 jed 以及很多其它工具都會浮現在腦海中。我們都有自己已逐漸了解并且喜愛的編輯器(以及我們喜愛的組合鍵)。有了可信賴的編輯器,我們可以輕松處理任何數量與 UNIX 有關的管理或編程任務。
雖然交互式編輯器很棒,但卻有其限制。盡管其交互式特性可以成為強項,但也有其不足之處。考慮一下需要對一組文件執行類似更改的情形。您可能會本能地運行自己所喜愛的編輯器,然后手工執行一組煩瑣、重復和耗時的編輯任務。然而,有一種更好的方法。
進入 sed
如果可以使編輯文件的過程自動化,以便用“批處理”方式編輯文件,甚至編寫可以對現有文件進行復雜更改的腳本,那將太好了。幸運的是,對于這種情況,有一種更好的方法 -- 這種更好的方法稱為 "sed"。
sed 是一種幾乎包括在所有 UNIX 平臺(包括 Linux)的輕量級流編輯器。sed 有許多很好的特性。首先,它相當小巧,通常要比您所喜愛的腳本語言小很多倍。其次,因為 sed 是一種流編輯器,所以,它可以對從如管道這樣的標準輸入接收的數據進行編輯。因此,無需將要編輯的數據存儲在磁盤上的文件中。因為可以輕易將數據管道輸出到 sed,所以,將 sed 用作強大的 shell 腳本中長而復雜的管道很容易。試一下用您所喜愛的編輯器去那樣做。
GNU sed
對 Linux 用戶來說幸運的是,最好的 sed 版本之一恰好是 GNU sed,其當前版本是 3.02。每一個 Linux 發行版都有(或至少應該有)GNU sed。GNU sed 之所以流行不僅因為可以自由分發其源代碼,還因為它恰巧有許多對 POSIX sed 標準便利、省時的擴展。另外,GNU 沒有 sed 早期專門版本的很多限制,如行長度限制 -- GNU 可以輕松處理任意長度的行。
最新的 GNU sed
在研究這篇文章之時我注意到:幾個在線 sed 愛好者提到 GNU sed 3.02a。奇怪的是,在ftp.gnu.org(有關這些鏈接,請參閱參考資料)上找不到 sed 3.02a,所以,我只得在別處尋找。我在alpha.gnu.org 的 /pub/sed 中找到了它。于是我高興地將其下載、編譯然后安裝,而幾分鐘后我發現最新的 sed 版本卻是 3.02.80 -- 可在alpha.gnu.org 上 3.02a 源代碼旁邊找到其源代碼。安裝完 GNU sed 3.02.80 之后,我就完全準備好了。
正確的 sed
在本系列中,將使用 GNU sed 3.02.80。在即將出現的本系列后續文章中,某些(但非常少)最高級的示例將不能在 GNU sed 3.02 或 3.02a 中使用。如果您使用的不是 GNU sed,那么結果可能會不同。現在為什么不花些時間安裝 GNU sed 3.02.80 呢?那樣,不僅可以為本系列的余下部分作好準備,而且還可以使用可能是目前最好的 sed。
sed 示例
sed 通過對輸入數據執行任意數量用戶指定的編輯操作(“命令”)來工作。sed 是基于行的,因此按順序對每一行執行命令。然后,sed 將其結果寫入標準輸出 (stdout),它不修改任何輸入文件。
讓我們看一些示例。頭幾個會有些奇怪,因為我要用它們演示 sed 如何工作,而不是執行任何有用的任務。然而,如果您是 sed 新手,那么理解它們是十分重要的。下面是第一個示例:
$ sed -e 'd' /etc/services
如果輸入該命令,將得不到任何輸出。那么,發生了什么?在該例中,用一個編輯命令 'd' 調用 sed。sed 打開 /etc/services 文件,將一行讀入其模式緩沖區,執行編輯命令(“刪除行”),然后打印模式緩沖區(緩沖區已為空)。然后,它對后面的每一行重復這些步驟。這不會產生輸出,因為 "d" 命令除去了模式緩沖區中的每一行!
在該例中,還有幾件事要注意。首先,根本沒有修改 /etc/services。這還是因為 sed 只讀取在命令行指定的文件,將其用作輸入 -- 它不試圖修改該文件。第二件要注意的事是 sed 是面向行的。'd' 命令不是簡單地告訴 sed 一下子刪除所有輸入數據。相反,sed 逐行將 /etc/services 的每一行讀入其稱為模式緩沖區的內部緩沖區。一旦將一行讀入模式緩沖區,它就執行 'd' 命令,然后打印模式緩沖區的內容(在本例中沒有內容)。我將在后面為您演示如何使用地址范圍來控制將命令應用到哪些行 -- 但是,如果不使用地址,命令將應用到所有行。
第三件要注意的事是括起 'd' 命令的單引號的用法。養成使用單引號來括起 sed 命令的習慣是個好注意,這樣可以禁用 shell 擴展。
另一個 sed 示例
下面是使用 sed 從輸出流除去 /etc/services 文件第一行的示例:
$ sed -e '1d' /etc/services | more
如您所見,除了前面有 '1' 之外,該命令與第一個 'd' 命令十分類似。如果您猜到 '1' 指的是第一行,那您就猜對了。與第一個示例中只使用 'd' 不同的是,這一次使用的 'd' 前面有一個可選的數字地址。通過使用地址,可以告訴 sed 只對某一或某些特定行進行編輯。
地址范圍
現在,讓我們看一下如何指定地址范圍。在本例中,sed 將刪除輸出的第 1 到 10 行:
$ sed -e '1,10d' /etc/services | more
當用逗號將兩個地址分開時,sed 將把后面的命令應用到從第一個地址開始、到第二個地址結束的范圍。在本例中,將 'd' 命令應用到第 1 到 10 行(包括這兩行)。所有其它行都被忽略。
帶規則表達式的地址
現在演示一個更有用的示例。假設要查看 /etc/services 文件的內容,但是對查看其中包括的注釋部分不感興趣。如您所知,可以通過以 '#' 字符開頭的行在 /etc/services 文件中放置注釋。為了避免注釋,我們希望 sed 刪除以 '#' 開始的行。以下是具體做法:
$ sed -e '/^#/d' /etc/services | more
試一下該例,看看發生了什么。您將注意到,sed 成功完成了預期任務。現在,讓我們分析發生的情況。
要理解 '/^#/d' 命令,首先需要對其剖析。首先,讓我們除去 'd' -- 這是我們前面所使用的同一個刪除行命令。新增加的是 '/^#/' 部分,它是一種新的規則表達式地址。規則表達式地址總是由斜杠括起。它們指定一種 模式,緊跟在規則表達式地址之后的命令將僅適用于正好與該特定模式匹配的行。
因此,'/^#/' 是一個規則表達式。但是,它做些什么呢?很明顯,現在該復習規則表達式了。
規則表達式復習
可以使用規則表達式來表示可能會在文本中發現的模式。您在 shell 命令行中用過 '*' 字符嗎?這種用法與規則表達式類似,但并不相同。下面是可以在規則表達式中使用的特殊字符:
字符 描述
與行首匹配
與行末尾匹配
與任一個字符匹配
將與前一個字符的零或多個出現匹配
[ ] 與 [ ] 之內的所有字符匹配
感受規則表達式的最好方法可能是看幾個示例。所有這些示例都將被 sed 作為合法地址接受,這些地址出現在命令的左邊。下面是幾個示例:
規則
表達式 描述
/./ 將與包含至少一個字符的任何行匹配
/../ 將與包含至少兩個字符的任何行匹配
/^#/ 將與以 '#' 開始的任何行匹配
/^$/ 將與所有空行匹配
/}^/ 將與以 '}'(無空格)結束的任何行匹配
/} *^/ 將與以 '}' 后面跟有零或多個空格結束的任何行匹配
/[abc]/ 將與包含小寫 'a'、'b' 或 'c' 的任何行匹配
/^[abc]/ 將與以 'a'、'b' 或 'c'開始的任何行匹配
在這些示例中,鼓勵您嘗試幾個。花一些時間熟悉規則表達式,然后嘗試幾個自己創建的規則表達式。可以如下使用 regexp:
$ sed -e '/regexp/d' /path/to/my/test/file | more
這將導致 sed 刪除任何匹配的行。然而,通過告訴 sed打印 regexp 匹配并刪除不匹配的內容,而不是與之相反的方法,會更有利于熟悉規則表達式。可以用以下命令這樣做:
$ sed -n -e '/regexp/p' /path/to/my/test/file | more
請注意新的 '-n' 選項,該選項告訴 sed 除非明確要求打印模式空間,否則不這樣做。您還會注意到,我們用 'p' 命令替換了 'd' 命令,如您所猜想的那樣,這明確要求 sed 打印模式空間。就這樣,將只打印匹配部分。
有關地址的更多內容
目前為止,我們已經看到了行地址、行范圍地址和 regexp 地址。但是,還有更多的可能。我們可以指定兩個用逗號分開的規則表達式,sed 將與所有從匹配第一個規則表達式的第一行開始,到匹配第二個規則表達式的行結束(包括該行)的所有行匹配。例如,以下命令將打印從包含 "BEGIN" 的行開始,并且以包含 "END" 的行結束的文本塊:
$ sed -n -e '/BEGIN/,/END/p' /my/test/file | more
如果沒發現 "BEGIN",那么將不打印數據。如果發現了 "BEGIN",但是在這之后的所有行中都沒發現 "END",那么將打印所有后續行。發生這種情況是因為 sed 面向流的特性 -- 它不知道是否會出現 "END"。
C 源代碼示例
如果只要打印 C 源文件中的 main() 函數,可輸入:
$ sed -n -e '/main[[:space:]]*(/,/^}/p' sourcefile.c | more
該命令有兩個規則表達式 '/main[[:space:]]*(/' 和 '/^}/',以及一個命令 'p'。第一個規則表達式將與后面依次跟有任意數量的空格或制表鍵以及開始圓括號的字符串 "main" 匹配。這應該與一般 ANSI C main() 聲明的開始匹配。
在這個特別的規則表達式中,出現了 '[[:space:]]' 字符類。這只是一個特殊的關鍵字,它告訴 sed 與 TAB 或空格匹配。如果愿意的話,可以不輸入 '[[:space:]]',而輸入 '[',然后是空格字母,然后是 -V,然后再輸入制表鍵字母和 ']' -- Control-V 告訴 bash 要插入“真正”的制表鍵,而不是執行命令擴展。使用 '[[:space:]]' 命令類(特別是在腳本中)會更清楚。
好,現在看一下第二個 regexp。'/^}' 將與任何出現在新行行首的 '}' 字符匹配。如果代碼的格式很好,那么這將與 main() 函數的結束花括號匹配。如果格式不好,則不會正確匹配 -- 這是執行模式匹配任務的一件棘手之事。
因為是處于 '-n' 安靜方式,所以 'p' 命令還是完成其慣有任務,即明確告訴 sed 打印該行。試著對 C 源文件運行該命令 -- 它應該輸出整個 main() { } 塊,包括開始的 "main()" 和結束的 '}'。
下一篇
既然已經觸及了基本知識,我們將在后兩篇文章中加快步伐。如果想看一些更豐富的 sed 資料,請耐心一些 -- 馬上就有!同時,您可能想查看下列 sed 和規則表達式資源。
sed 是十分強大和小巧的文本流編輯器。在本文章系列的第二篇中,Daniel Robbins 為您演示如何使用 sed 來執行字符串替換、創建更大的 sed 腳本以及如何使用 sed 的附加、插入和更改行命令。
sed 是很有用(但常被遺忘)的 UNIX 流編輯器。在以批處理方式編輯文件或以有效方式創建 shell 腳本來修改現有文件方面,它是十分理想的工具。本文是前一篇介紹 sed 文章的續篇。
替換!
讓我們看一下 sed 最有用的命令之一,替換命令。使用該命令,可以將特定字符串或匹配的規則表達式用另一個字符串替換。下面是該命令最基本用法的示例:
$ sed -e 's/foo/bar/' myfile.txt 上面的命令將 myfile.txt 中每行第一次出現的 'foo'(如果有的話)用字符串 'bar' 替換,然后將該文件內容輸出到標準輸出。請注意,我說的是每行第一次出現,盡管這通常不是您想要的。在進行字符串替換時,通常想執行全局替換。也就是說,要替換每行中的所有出現,如下所示:
$ sed -e 's/foo/bar/g' myfile.txt 在最后一個斜杠之后附加的 'g' 選項告訴 sed 執行全局替換。
關于 's///' 替換命令,還有其它幾件要了解的事。首先,它是一個命令,并且只是一個命令,在所有上例中都沒有指定地址。這意味著,'s///' 還可以與地址一起使用來控制要將命令應用到哪些行,如下所示:
$ sed -e '1,10s/enchantment/entrapment/g' myfile2.txt 上例將導致用短語 'entrapment' 替換所有出現的短語 'enchantment',但是只在第一到第十行(包括這兩行)上這樣做。
$ sed -e '/^$/,/^END/s/hills/mountains/g' myfile3.txt 該例將用 'mountains' 替換 'hills',但是,只從空行開始,到以三個字符 'END' 開始的行結束(包括這兩行)的文本塊上這樣做。
關于 's///' 命令的另一個妙處是 '/' 分隔符有許多替換選項。如果正在執行字符串替換,并且規則表達式或替換字符串中有許多斜杠,則可以通過在 's' 之后指定一個不同的字符來更改分隔符。例如,下例將把所有出現的 /usr/local 替換成 /usr:
$ sed -e 's:/usr/local:/usr:g' mylist.txt 在該例中,使用冒號作為分隔符。如果需要在規則表達式中指定分隔符字符,可以在它前面加入反斜杠。
規則表達式混亂
目前為止,我們只執行了簡單的字符串替換。雖然這很方便,但是我們還可以匹配規則表達式。例如,以下 sed 命令將匹配從 '<' 開始、到 '>' 結束、并且在其中包含任意數量字符的短語。下例將刪除該短語(用空字符串替換):
$ sed -e 's/<.*>//g' myfile.html 這是要從文件除去 HTML 標記的第一個很好的 sed 腳本嘗試,但是由于規則表達式的特有規則,它不會很好地工作。原因何在?當 sed 試圖在行中匹配規則表達式時,它要在行中查找最長的匹配。在我的前一篇 sed 文章中,這不成問題,因為我們使用的是 'd' 和 'p' 命令,這些命令總要刪除或打印整行。但是,在使用 's///' 命令時,確實有很大不同,因為規則表達式匹配的整個部分將被目標字符串替換,或者,在本例中,被刪除。這意味著,上例將把下行:
This is what I meant. 變成:
meant. 我們要的不是這個,而是:
This is what I meant. 幸運的是,有一種簡便方法來糾正該問題。我們不輸入“'<' 字符后面跟有一些字符并以 '>' 字符結束”的規則表達式,而只需輸入一個“'<' 字符后面跟有任意數量非 '>' 字符并以 '>' 字符結束”的規則表達式。這將與最短、而不是最長的可能性匹配。新命令如下:
$ sed -e 's/<[^>]*>//g' myfile.html 在上例中,'[^>]' 指定“非 '>'”字符,其后的 '*' 完成該表達式以表示“零或多個非 '>' 字符”。對幾個 html 文件測試該命令,將它們管道輸出到 "more",然后仔細查看其結果。
更多字符匹配
'[ ]' 規則表達式語法還有一些附加選項。要指定字符范圍,只要字符不在第一個或最后一個位置,就可以使用 '-',如下所示:
'[a-x]*' 這將匹配零或多個全部為 'a'、'b'、'c'...'v'、'w'、'x' 的字符。另外,可以使用 '[:space:]' 字符類來匹配空格。以下是可用字符類的相當完整的列表:
字符類 描述
[:alnum:] 字母數字 [a-z A-Z 0-9]
[:alpha:] 字母 [a-z A-Z]
[:blank:] 空格或制表鍵
[:cntrl:] 任何控制字符
[:digit:] 數字 [0-9]
[:graph:] 任何可視字符(無空格)
[:lower:] 小寫 [a-z]
[:print:] 非控制字符
[:punct:] 標點字符
[:space:] 空格
[:upper:] 大寫 [A-Z]
[:xdigit:] 十六進制數字 [0-9 a-f A-F]
盡可能使用字符類是很有利的,因為它們可以更好地適應非英語 locale(包括某些必需的重音字符等等).
高級替換功能
我們已經看到如何執行簡單甚至有些復雜的直接替換,但是 sed 還可以做更多的事。實際上可以引用匹配規則表達式的部分或全部,并使用這些部分來構造替換字符串。作為示例,假設您正在回復一條消息。下例將在每一行前面加上短語 "ralph said: ":
$ sed -e 's/.*/ralph said: &/' origmsg.txt 輸出如下:
ralph said: Hiya Jim, ralph said: ralph said:
I sure like this sed stuff! ralph said: 該例的替換字符串中使用了 '&' 字符,該字符告訴 sed 插入整個匹配的規則表達式。因此,可以將與 '.*' 匹配的任何內容(行中的零或多個字符的最大組或整行)插入到替換字符串中的任何位置,甚至多次插入。這非常好,但 sed 甚至更強大。
那些極好的帶反斜杠的圓括號
's///' 命令甚至比 '&' 更好,它允許我們在規則表達式中定義區域,然后可以在替換字符串中引用這些特定區域。作為示例,假設有一個包含以下文本的文件:
foo bar oni eeny meeny miny larry curly moe jimmy the weasel 現在假設要編寫一個 sed 腳本,該腳本將把 "eeny meeny miny" 替換成 "Victor eeny-meeny Von miny" 等等。要這樣做,首先要編寫一個由空格分隔并與三個字符串匹配的規則表達式。
'.* .* .*' 現在,將在其中每個感興趣的區域兩邊插入帶反斜杠的圓括號來定義區域:
'\(.*\) \(.*\) \(.*\)' 除了要定義三個可在替換字符串中引用的邏輯區域以外,該規則表達式的工作原理將與第一個規則表達式相同。下面是最終腳本:
$ sed -e 's/\(.*\) \(.*\) \(.*\)/Victor \1-\2 Von \3/' myfile.txt 如您所見,通過輸入 '\x'(其中,x 是從 1 開始的區域號)來引用每個由圓括號定界的區域。輸入如下:
Victor foo-bar Von oni Victor eeny-meeny Von miny Victor larry-curly Von moe Victor jimmy-the Von weasel 隨著對 sed 越來越熟悉,您可以花最小力氣來進行相當強大的文本處理。您可能想如何使用熟悉的腳本語言來處理這種問題 -- 能用一行代碼輕易實現這樣的解決方案嗎?
組合使用
在開始創建更復雜的 sed 腳本時,需要有輸入多個命令的能力。有幾種方法這樣做。首先,可以在命令之間使用分號。例如,以下命令系列使用 '=' 命令和 'p' 命令,'=' 命令告訴 sed 打印行號,'p' 命令明確告訴 sed 打印該行(因為處于 '-n' 模式)。
$ sed -n -e '=;p' myfile.txt 無論什么時候指定了兩個或更多命令,都按順序將每個命令應用到文件的每一行。在上例中,首先將 '=' 命令應用到第 1 行,然后應用 'p' 命令。接著,sed 繼續處理第 2 行,并重復該過程。雖然分號很方便,但是在某些場合下,它不能正常工作。另一種替換方法是使用兩個 -e 選項來指定兩個不同的命令:
$ sed -n -e '=' -e 'p' myfile.txt 然而,在使用更為復雜的附加和插入命令時,甚至多個 '-e' 選項也不能幫我們的忙。對于復雜的多行腳本,最好的方法是將命令放入一個單獨的文件中。然后,用 -f 選項引用該腳本文件:
$ sed -n -f mycommands.sed myfile.txt 這種方法雖然可能不太方便,但總是管用。
一個地址的多個命令
有時,可能要指定應用到一個地址的多個命令。這在執行許多 's///' 以變換源文件中的字和語法時特別方便。要對一個地址執行多個命令,可在文件中輸入 sed 命令,然后使用 '{ }' 字符將這些命令分組,如下所示:
1,20{
在這篇 sed 系列的總結性文章中,Daniel Robbins 帶您體驗 sed 的真正力量。在介紹完幾個重要的 sed 腳本之后,他將通過將一個 Quicken .QIF 文件轉換成可讀文本格式來演示一些基本 sed 腳本的編寫。該轉換腳本不僅實用,而且還是展現 sed 腳本編寫能力的極佳示例。
強健的 sed
在第二篇 sed 文章中,我提供了一些示例來演示 sed 的工作原理,但是它們當中很少有示例能實際做特別有用的事。在這篇 sed 系列的最后文章中,我要改變那種方式,并使用 sed 來做實際的事。我將為您顯示幾個示例,它們不僅演示 sed 的能力,而且還做一些真正巧妙(和方便)的事。例如,在本文的后半部,將為您演示如何設計一個 sed 腳本來將 .QIF 文件從 Intuit 的 Quicken 金融程序轉換成具有良好格式的文本文件。在那樣做之前,我們將看一下不怎么復雜但卻很有用的 sed 腳本。
文本轉換
第一個實際腳本將 UNIX 風格的文本轉換成 DOS/Windows 格式。您可能知道,基于 DOS/Windows 的文本文件在每一行末尾有一個 CR(回車)和 LF(換行),而 UNIX 文本只有一個換行。有時可能需要將某些 UNIX 文本移至 Windows 系統,該腳本將為您執行必需的格式轉換。
$ 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 文章中查看如何這樣做的說明。
我已記不清有多少次在下載一些示例腳本或 C 代碼之后,卻發現它是 DOS/Windows 格式。雖然很多程序不在乎 DOS/Windows 格式的 CR/LF 文本文件,但是有幾個程序卻在乎 -- 最著名的是 bash,只要一遇到回車,它就會出問題。以下 sed 調用將把 DOS/Windows 格式的文本轉換成可信賴的 UNIX 格式:
$ 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