sed系列文章:
sed修煉系列(一):花拳綉腿之入門篇
sed修煉系列(二):武功心法(info sed翻譯+注解)
sed修煉系列(三):sed高級應用之實現窗口滑動技術
sed修煉系列(四):sed中的疑難雜症
- 本文中提到GNU擴展時,表示該功能是GNU為sed提供的(即GNU版本的sed才有該功能),一般此時都會說明:如果要寫具有可移植性的腳本,應盡量避免在腳本中使用該選項。
- 本文中的正則表達式幾乎和grep中支持的一樣。但還是有少數幾個是sed自身才能解析的表達式。因此本譯文中只對這些sed自身才支持的正則表達式做翻譯,其余sed支持的通用性正則表達式見grep命令中文手冊。
- 此外,除了正則表達式部分,還有些地方沒有進行翻譯,因為個人覺得幾乎用不上,沒必要翻譯。但為了保持文章的完整性,仍給出了原文內容。
- 個人建議:如只想了解sed的簡單用法,不建議閱讀本譯文;但如果想深入學習或掌握sed,info sed是最佳選擇,很多處理機制在書上(即使是最為流行的《sed & awk》)都沒有深入說明。本文為我第二次翻譯,兩次翻譯過程中收獲都極大。
- 譯文中有些地方加上了
"(注:)"
,為本人自行加入,助於理解和說明,非原文內容。PS:直到翻譯完,才發現加了很多很多我個人注釋,盡管是一篇譯文,但也舍不得刪掉這些感想。如果這些"注:"有礙觀感,還望各位能體諒。 - 學習sed的過程中,推薦使用"sedsed"調試工具,這對於分析sed處理過程以及pattern space、hold space有很大幫助。但極少數情況下,它的結果可能並不如想象中那樣准確。
- 最后,本文是在markdownpad2上寫好才發上來的,由於markdownpad2和博客園markdown編輯器有些語法不同,可能會出現某些符號缺失的問題,雖然檢查了很久,但篇幅太大,難免有遺漏的地方,所以如果發現了哪里有錯誤或有疑問的地方,盼請指出。同時,也期待與各位交流sed的使用。
- 本人譯作集合:http://www.cnblogs.com/f-ck-need-u/p/7048359.html
1 簡介
2 調用方式
3 sed程序
3.1 sed是如何工作的
3.2 sed定址:篩選行的方式
3.3 正則表達式一覽
3.4 sed常用命令
3.5 sed的s命令
3.6 比較少用的sed命令
3.7 大師級的sed命令(sed標簽功能)
3.8 GNU sed特有的命令
3.9 GNU對正則表達式的反斜線擴展
4 一些簡單的示例腳本
5 GNU sed的限制和優點
6 學習sed的其他資源
7 Bugs說明(建議看)
1. Introduction(簡介)
sed是一個流式編輯器。流式編輯器用於對輸入流(文件或管道傳遞的數據)執行基本的文本轉換操作。在某些方面上,sed有點類似於腳本化編輯的編輯器(如ed
),但sed只能通過一次輸入流,因此它的效率要更高。但sed區別於其他類型編輯器的地方在於它能篩選過濾管道傳遞的文本數據。
(注:sed只能通過一次輸入流,意味着每次的輸入流只能處理一次,因此像(sed -n '2{p;q}';sed -n 3{p;q}) <filename
這樣的命令中,第二個sed語句讀取的輸入流是空流。)
2. Invocation(調用方式)
通常,會使用下面的方式來調用sed:
sed SCRIPT INPUTFILE...
完整的調用格式為:
sed OPTIONS... [SCRIPT] [INPUTFILE...]
如果不指定INPUTFILE或者指定的INPUTFILE為"-",sed將從標准輸入中讀取輸入流並進行過濾。SCRIPT
是第一個非選項參數,sed僅在沒有使用"-e"或"-f script_file"選項時才將其當作是script部分而非輸入文件。
可以使用下面的命令行選項來調用sed:
'--version'
輸出sed的版本號並退出。
'--help'
輸出sed命令行的簡單幫助信息並退出。
'-n'
'--quiet'
'--silent'
默認情況下,sed將在每輪script循環結束(How 'sed' works: Execution Cycle)時自動輸出模式空間中的內容。該選項禁止自動輸出動作,這種情況下,只有顯式通過"p"命令來產生對應的輸出。
'-e SCRIPT'
'--expression=SCRIPT'
向SCRIPT中添加命令集,讓sed根據這些命令集來處理輸入流。(注:其實就是指定pattern和對應的command)
'-f SCRIPT-FILE'
'--file=SCRIPT-FILE'
指定包含command集合的script文件,讓sed根據script文件中的命令集處理輸入流。
'-i[SUFFIX]'
'--in-place[=SUFFIX]'
該選項指定要將sed的輸出結果保存(覆蓋的方式)到當前編輯的文件中。GNU sed是通過創建一個臨時文件並將輸出寫入到該臨時文件,然后重命名為源文件來實現的。
該選項隱含了"-s"選項。
當到達了文件的尾部,臨時文件被重命名為源文件的名稱。如果還提供了SUFFIX,則在重命名臨時文件之前,先使用該SUFFIX先修改源文件名,從而生成一個文件備份。
(注:臨時文件總是會被重命名為源文件名稱,也就是說sed處理完全結束后,仍使用源文件名的文件是sed修改后的文件。文件名中包含了SUFFIX的文件則是最原始文件的備份。例如源文件為a.txt,sed -i'.log' SCRIPT a.txt
將生成兩個文件:a.txt和a.txt.log,前者是sed修改后的文件,a.txt.log是源a.txt的備份文件。)
規則如下:如果擴展名不包含符號"*",將SUFFIX被添加到原文件名的后面當作文件后綴;如果SUFFIX中包含了一個或多個字符"*",則每個"*"都替換為原文件名。這使得你可以為備份文件添加一個前綴,而不是后綴,甚至可以將此備份文件放在在其他已存在的目錄下。
如果沒有提供SUFFIX,源文件被覆蓋,且不會生成備份文件。
'-l N'
'--line-length=N'
為"l"命令指定默認的換行長度。N=0意味着完全不換行的長行,如果不指定,則70個字符就換行。
'--posix'
GNU 'sed' includes several extensions to POSIX sed. In order to simplify writing portable scripts, this option disables all the extensions that this manual documents, including additional commands. Most of the extensions accept 'sed' programs that are outside the syntax mandated by POSIX, but some of them (such as the behavior of the 'N' command described in *note Reporting Bugs::) actually violate the standard. If you want to disable only the latter kind of extension, you can set the 'POSIXLY_CORRECT' variable to a non-empty value.
'-b'
'--binary'
This option is available on every platform, but is only effective where the operating system makes a distinction between text files and binary files. When such a distinction is made--as is the case for MS-DOS, Windows, Cygwin--text files are composed of lines separated by a carriage return and a line feed character, and 'sed' does not see the ending CR. When this option is specified, 'sed' will open input files in binary mode, thus not requesting this special processing and considering lines to end at a line feed.
'--follow-symlinks'
該選項只在支持符號連接的操作系統上生效,且只有指定了"-i"選項時才生效。指定該選項后,如果sed命令行中指定的輸入文件是一個符號連接,則sed將對該符號鏈接的目標文件進行處理。默認情況下,禁用該選項,因此不會修改鏈接的源文件。
'-r'
'--regexp-extended'
使用擴展正則表達式,而不是使用默認的基礎正則表達式。sed所支持的擴展正則表達式和egrep
一樣,使用擴展正則表達式顯得更簡潔,因為有些元字符不用再使用反斜線"\",但這是GNU擴展功能,因此應避免在可移植性腳本中使用。
'-s'
'--separate'
默認情況下,sed會將命令行中指定文件的所有行當作一個長輸入流。此選項為GNU sed擴展功能,指定該選項后,sed將認為命令行中給定的每個文件都是獨立的輸入流。因此此時范圍定址(如/abc/,/def/
)無法跨越多個文件,行號也會在處理每個文件時重置,"$"代表的是每個文件的最后一行,"R"命令調用的文件將繞回到每個文件的開頭。
'-u'
'--unbuffered'
使用盡量少的空間緩沖輸入和輸出行。(該選項在某些情況下尤為有用,例如輸入流的來源是"tail -f
時,指定該選項將可以盡快返回輸出結果。)
'-z'
'--null-data'
'--zero-terminated'
以空串符號"\0"而不是換行符"\n"作為輸入流的行分隔符。
如果未給定"-e"、"-f"、"--expression"或"--file"選項,則命令行中的第一個非選項參數將被認為是SCRIPT被執行。
如果命令行中在上述選項后還加了任意參數,則都將被當作輸入文件。其中"-"表示的是標准輸入流。如果沒有給定任何輸入文件,則默認從標准輸入中讀取輸入里流。
3. 'sed' Programs(sed程序)
sed程序由一個或多個sed命令組成(注:請勿將sed這個工具理解為sed命令,而應該看作是一個包含很多命令的程序,所以后文中"sed命令"表示的是sed中的子命令,而非sed本身這個命令),這些命令通過"-e"、"-f file"傳遞,或者當沒有指定這兩個選項時,通過第一個非選項參數傳遞。這些傳遞的命令集合稱為sed的執行腳本(SCRIPT),命令的執行順序按照命令行中給定的順序依次執行。
在SCRIPT或SCRIPT-FILE中的多個命令可以使用分號";"進行分隔,或者換行書寫。但有些命令,由於它們的語法問題,導致不支持使用分號作為命令分隔符,因此只能換行書寫,除非它們是SCRIPT或SCRIPT-FILE中的最后一個命令。允許命令的前面有空白字符。
每個sed命令由地址或范圍地址,隨后緊跟單字符代表的命令名稱組成,其中地址部分可以省略。
3.1 How 'sed' Works(sed是如何工作的)
sed維護兩個數據緩沖空間:一直處於活動狀態的模式空間(pattern space)和輔助性的保持空間(hold space)。這兩個空間初始時都為空。
sed通過執行下面的循環來操作輸入流中的每一行: 首先,sed讀取輸入流中的一行,移除該行的尾隨換行符,並將其放入到pattern space中。然后對pattern space中的內容執行SCRIPT中的sed命令,每個sed命令都可以關聯一個地址:地址是一種條件判斷代碼,只有符合條件的行才會執行相應的命令。當執行到SCRIPT的尾部時,除非指定了"-n"選項,否則pattern space中的內容將寫到標准輸出流中,並添加回尾隨的換行符。然后進入下一個循環開始處理輸入流中的下一行。
除非指定了特殊的命令(例如"-D"),否則pattern space中的內容在SCRIPT循環結束后會被刪除。但是hold space會保留其中的內容(參見sed命令'h', 'H', 'x', 'g', 'G'以獲知兩個buffer空間是如何互相移動數據的)。
(
注:也就是說,sed程序工作時包含內外兩個循環:內循環是SCRIPT循環,循環的過程是對模式空間中的內容執行SCRIPT中的命令集合,還包括一個隱含的自動輸出動作用於輸出模式空間的內容到標准輸出中;外循環是sed循環,讀取下一行到模式空間中,並執行SCRIPT循環,SCRIPT循環結束后再讀取下一行到模式空間中。使用編程結構描述,結構如下:
for ((line=1;line<=last_line_num;++line))
do
read $line to pattern_space;
while pattern_space is not null
do
execute cmd1 in SCRIPT;
execute cmd2 in SCRIPT;
……
auto_print;
remove_pattern_space;
done
done
一般情況下,SCRIPT循環都只執行一輪就退出並進入外層sed循環,因為執行完一次SCRIPT循環, pattern space就清空了,但有些特殊命令(如"D"命令)會進入多行模式,使得SCRIPT循環結束 時將數據鎖在pattern space中不輸出也不清空,甚至在SCRIPT循環還沒結束時就強行進入下一 輪SCRIPT循環,在《sed & awk》一書中,這種行為被稱之為"回到SCRIPT的頂端"。其實就相當 於在上面的while循環結構中加上了"continue"關鍵字。此外還有命令(如"d")可以直接退出 SCRIPT循環進入下一個sed循環,就像是在while循環中加上了"break"一樣。甚至還有直接退出 sed循環的命令(只有2個這樣的命令:"q"和"Q"),就像是加上了"exit"一樣。
auto_print
和remove_pattern_space
動作是隱含動作,分別稱之為自動輸出和清空pattern space。"-n"選項禁用的自動輸出不是禁止隱含動作auto_print,而是讓其輸出空內容(它們是有區別的),並執行remove_pattern_space。只要沒有指定"-n"選項,在SCRIPT循環結束時一定輸出pattern space中的全部內容並清空pattern space,只不過某些特殊的命令可以將pattern space中的內容鎖住使auto_print輸出空內容,也使得remove_pattern_space移除不了pattern space的內容。
之所以要特地指出"輸出空內容"並標明它和禁止輸出動作,是因為某些命令(如"a"、"i"、"c"命令)依賴於輸出流,沒有輸出動作就沒有輸出流,這些命令就無法正確完成。
這幾個循環、幾個動作的細節非常重要,雖不影響后文單個選項或命令的理解,但如果將命令結合,有時候的結果很可能會出人意料,而sed難就難在命令的合理組合。例如下面的命令,"a"命令本來是要將xyz插入到aaa所在行后面的,但結果卻插在了aaa行的前面。
echo -e "aaa\nbbb" | sed '/aaa/{a\ > xyz > ;N}'
xyz
aaa
bbb
)
3.2 Selecting lines with 'sed'(sed定址:篩選行的方式)
(注:sed SCRIPT中的命令由單字符代表的命令和地址組成,地址的作用是匹配當前正在處理的行,如果能匹配成功,就執行與該地址相關聯的命令。地址由定址表達式決定,定址的結果可能是單行,可能是一個范圍,省略定址表達式時表示所有行。)
可以使用下面任意一種方式進行定址:
'NUMBER'
指定一個行號,sed將僅只匹配該行。(需要注意,除非使用了"-s"或"-i"選項,sed將對所有輸入文件的行連續計數。)
'FIRST~STEP'
這是GNU sed的功能,FIRST和STEP都是非負數。該定址表達式表示從第FIRST行開始,每隔STEP行就再取一次。也就是取行號滿足“FIRST+(N*STEP)” (其中N>=0)的行。因此,要選擇所有的奇數行,使用“1~2”;要從第2行開始每隔3行取一次,使用“2~3”;要從第10行開始每隔5行取一次,使用“10~5”;而“50~0”則表示只取第50行。
'$'
該符號匹配的是最后一個文件的最后一行,如果指定了"-i"或"-s",則匹配的是每個文件的最后一行。
(注:總之,"$"匹配的是每個輸入流的最后一行)
'/REGEXP/'
該定址表達式將選擇那些能被正則表達式REGEXP匹配的所有行。如果REGEXP中自身包含了字符"/",則必須使用反斜線進行轉義,即"\/"
。
空的正則表達式"//"表示引用最后一個正則表達式匹配的結果(命令"s"中的正則匹配部分如果是空正則"//",則一樣如此處理)。注意,正則表達式的修飾符(即下文中的"I"和"M",以及s命令中的修飾符"i"、"I"和"M")是在正則表達式編譯完成之后(注:進行數據匹配時)才生效的,因此,這些修飾符和空正則一起使用時無效(注:會報錯,提示"cannot specify modifiers on empty regexp")。
(注:這里的修飾符特指定址時可用的修飾符,即I和M。命令s也有修飾符"i"、"I"和"M"。
如:sed '/hold/Is//gogogo/g'
能成功,因為第一個定址正則表達式的修飾符"I"的對象是非空集"hold",第二個正則模式"//"沒有指定"i"、"I"或"M"修飾符,所以成功。但sed '/hold/Is//gogogo/gi'
會報錯,因為在正則編譯結束后還未開始進行匹配的時候,第二個正則表達式中的修飾符"i"的對象是空集。sed '/hold/I{//Mp}'
和sed '/hold/I{//Ip}'
也都是失敗的,原因都是第二個正則的修飾符對象是空集。
'\%REGEXP%'
('%'可以使用其他任意單個字符替換。)
這和上一個定址表達式的作用是一樣的,只不過是使用符號"%"替換了符號"/"。當REGEXP中包含"/"符號時,使用該定址表達式就無需對"/"使用反斜線"\"轉義。但如果此時REGEXP中包含了"%"符號時,該符號需要使用"\"轉義。
總之,定址表達式中使用的分隔符在REGEXP中出現時都需要使用反斜線轉義。
'/REGEXP/I'
'\%REGEXP%I'
正則表達式的修飾符"I"是GNU的擴展功能,表示REGEXP在匹配時不區分大小寫。
'/REGEXP/M'
'\%REGEXP%M'
正則表達式的修飾符"M"是GNU的擴展功能,這可以讓sed直接匹配多行模式下(multi-line mode)的位置。該修飾符使得正則表達式的元字符"^"和"$"匹配分別匹配每個新行后的空字符和新行前的空字符。還有另外兩個特殊的字符序列(\`和\',分別是反斜線加反引號,反斜線加單引號),它們總是匹配buffer空間中的起始和結束位置。此外,元字符點"."不能匹配多行模式下的換行符。
(注:在單行模式下,使用M和不使用M是沒有區別的,但在多行模式下各符號匹配的位置將和通常的正則表達式元字符匹配的內容不同,各符號的匹配位置如下圖所示)
如果沒有給定地址,則會匹配輸入流中的所有行,如果給定了單地址的定址表達式,則這些行會在模式空間中被匹配條件進行匹配。
可以使用逗號分隔的兩個地址表達式(ADDR1,ADDR2)來描述范圍地址。范圍地址表示從符合第一個地址條件的行開始匹配,直到符合第二個地址的行結束。
(注:需要注意,無論行是否符合地址匹配條件,它們都會被讀入pattern space,只不過讀入的行如果不符合地址匹配條件,將直接執行SCRIPT循環中的隱含動作,並結束SCRIPT循環繼續讀取輸入流的下一行)
如果第二個定址表達式是正則表達式REGEXP,將從符合第一個定址表達式的行開始逐行檢查,以匹配范圍定址的結束行:范圍定址的范圍總是會跨越至少兩行(當然,如果輸入流結束的情形除外)。
如果第二個定址表達式是一個小於或等於第一個表達式代表的行號值,則只會匹配第一個表達式搜索到的那一行。
(注:即如果范圍地址的結束行在起始行的前面,則只會匹配起始行。)
GNU sed還支持以下幾種特殊的兩地址格式,這些都是GNU的擴展功能:
'0,/REGEXP/'
使用行號0作為起始地址也是支持的,就像此處的"0,/REGEXP/",這時sed會嘗試對第一行就匹配REGEXP。換句話說,"0,/REGEXP/"和"1,/REGEXP/"基本是相同的。但以行號0作為起始行時,如果第一行就能被ADDR2匹配,范圍搜索立即就結束,因為第二個地址已經搜索到了;如果以行號1作為起始行,會從第二行開始匹配ADDR2,直到匹配成功。
注意,0地址只有在這一種情況下才是有意義的,實際上並沒有第0行,在其他任何時候使用行號0都會給出錯誤提示。
'ADDR1,+N'
匹配ADDR1和其后的N行。
'ADDR1,~N'
匹配ADDR1和其后的行直到出現N的倍數行,倍數可為隨意整數倍。 (注:可以是任意倍,只要N的倍數是最接近且大於ADDR1的即可。如ADDR1=1,N=3
匹配1到3行,ADDR1=5,N=4
匹配5-8行。而"1,+3"匹配的是第一行和其后的3行即1-4行。)
在地址的后面追加"!"符號指定反轉匹配的含義。意思是,如果"!"跟在范圍定址的后面,則那些匹配的行將不被選擇,而是不匹配的行被選擇。同樣可以適用於單個定址甚至於有悖常理的空定址。
(注:例如,sed -n '3!p' INPUTFILE
,sed -n '3,5!p' INPUTFILE
,甚至是sed -n '!p' INPUTFILE
)
(注:
-
sed采用計數器計算,每讀取一行,計數器加1,直到最后一行。因此在讀到最后一行前,sed是不知道這次輸入流中總共有多上行,也不知道最后一行是第幾行。"$"符號表示最后一行,它只是一個特殊的標識符號。當sed讀取到輸入流的尾部時,sed就會為該行打上該標記。無法使用"$"參與數學運算,例如無法使用
$-1
表示倒數第二行,因為sed在讀到最后一行前,它並不知道這是倒數第二行,此時也還沒打"$"標記,因此$-1
是錯誤的定址表達式。另一方面,在本譯文的最開始就說明了,sed只能通過一次輸入流,這意味着已經讀取過的行無法再次被讀取,所以sed不提供往回取數據的定址表達式,上面的幾個定址表達式也確實證明了這一點。事實上,sed中根本就無法使用"-"做減法或取負數,因為語法不支持。 -
范圍匹配的是pattern space中的內容,對於
regexp1,regexp2
這樣的范圍定址自然容易理解,但如果是num1,num2
這樣的范圍定址,它可能和想象中的匹配方式不一樣。每讀取一行,sed內部的行號計數器都會"+1",所以行號值總是記錄在內存中。當進行行號匹配時,其本質是和內存中當前計數器的值進行匹配,如果當前計數器的值在范圍內,則能被匹配,否則無法匹配。因此,從hold space回到pattern space的行或者被修改的行甚至是被刪除過的行,不管這一行的內容在pattern space中是否存在,只要當前計數器值在范圍內,就能匹配。例如多行模式:
echo -e "abc\nfgh" | sed -n 'N;1p'
該命令不輸出任何內容。雖然N讀取下一行后,pattern space兩行中的第一行一直原封不動的放在那,沒做任何處理,但"1p"根本就無法匹配這一行,因為當前計數器的值為2。
)。
3.3 Overview of Regular Expression Syntax(正則表達式一覽)
(注:sed解析、編譯和匹配正則表達式的引擎和grep的引擎一樣,因此本文基本不做翻譯,可以參考我對info grep的譯文:grep命令中文手冊。)
To know how to use 'sed', people should understand regular expressions ("regexp" for short). A regular expression is a pattern that is matched against a subject string from left to right. Most characters are "ordinary": they stand for themselves in a pattern, and match the corresponding characters in the subject. As a trivial example, the pattern
The quick brown fox
matches a portion of a subject string that is identical to itself. The power of regular expressions comes from the ability to include alternatives and repetitions in the pattern. These are encoded in the pattern by the use of "special characters", which do not stand for themselves but instead are interpreted in some special way. Here is a brief description of regular expression syntax as used in 'sed'.
'CHAR'
A single ordinary character matches itself.
'*'
Matches a sequence of zero or more instances of matches for the preceding regular expression, which must be an ordinary character, a special character preceded by '\', a '.', a grouped regexp (see below), or a bracket expression. As a GNU extension, a postfixed regular expression can also be followed by '*'; for example, 'a**' is equivalent to 'a*'. POSIX 1003.1-2001 says that '*' stands for itself when it appears at the start of a regular expression or subexpression, but many nonGNU implementations do not support this and portable scripts should instead use '\*' in these contexts.
'\+'
As '*', but matches one or more. It is a GNU extension.
'\?'
As '*', but only matches zero or one. It is a GNU extension.
'\{I\}'
As '*', but matches exactly I sequences (I is a decimal integer; for portability, keep it between 0 and 255 inclusive).
'\{I,J\}'
Matches between I and J, inclusive, sequences.
'\{I,\}'
Matches more than or equal to I sequences.
'\(REGEXP\)'
Groups the inner REGEXP as a whole, this is used to:
-
Apply postfix operators, like
'\(abcd\)*'
: this will search for zero or more whole sequences of 'abcd', while'abcd*'
would search for 'abc' followed by zero or more occurrences of 'd'. Note that support for'\(abcd\)*'
is required by POSIX 1003.1-2001, but many non-GNU implementations do not support it and hence it is not universally portable. -
Use back references (see below).
'.'
Matches any character, including newline.
'^'
Matches the null string at beginning of the pattern space, i.e. what appears after the circumflex must appear at the beginning of the pattern space.
In most scripts, pattern space is initialized to the content of each line (*note How 'sed' works: Execution Cycle.). So, it is a useful simplification to think of '^#include' as matching only lines where '#include' is the first thing on line--if there are spaces before, for example, the match fails. This simplification is valid as long as the original content of pattern space is not modified, for example with an 's' command.
'^' acts as a special character only at the beginning of the regular expression or subexpression (that is, after '(' or '|'). Portable scripts should avoid '^' at the beginning of a subexpression, though, as POSIX allows implementations that treat '^' as an ordinary character in that context.
'$'
It is the same as '^', but refers to end of pattern space. '$' also acts as a special character only at the end of the regular expression or subexpression (that is, before ')' or '|'), and its use at the end of a subexpression is not portable.
'[LIST]'
'[^LIST]'
Matches any single character in LIST: for example, '[aeiou]' matches all vowels. A list may include sequences like 'CHAR1-CHAR2', which matches any character between (inclusive) CHAR1 and CHAR2.
A leading '^' reverses the meaning of LIST, so that it matches any single character not in LIST. To include ']' in the list, make it the first character (after the '^' if needed), to include '-' in the list, make it the first or last; to include '^' put it after the first character.
The characters '$', '*', '.', '[', and '\'
are normally not special within LIST. For example, '[\*]'
matches either '\' or '*'
, because the '\' is not special here. However, strings like '[.ch.]', '[=a=]', and '[:space:]' are special within LIST and represent collating symbols, equivalence classes, and character classes, respectively, and '[' is therefore special within LIST when it is followed by '.', '=', or ':'. Also, when not in 'POSIXLY_CORRECT' mode, special escapes like '\n' and '\t' are recognized within LIST. *Note Escapes::.
'REGEXP1\|REGEXP2'
Matches either REGEXP1 or REGEXP2. Use parentheses to use complex alternative regular expressions. The matching process tries each alternative in turn, from left to right, and the first one that succeeds is used. It is a GNU extension.
'REGEXP1REGEXP2'
Matches the concatenation of REGEXP1 and REGEXP2. Concatenation binds more tightly than '|', '^', and '$', but less tightly than the other regular expression operators.
'\DIGIT'
Matches the DIGIT-th '(...)' parenthesized subexpression in the regular expression. This is called a "back reference". Subexpressions are implicity numbered by counting occurrences of '(' left-to-right.
'\n'
匹配換行符。
'\CHAR'
Matches CHAR, where CHAR is one of '$', '*', '.', '[', '\', or '^'
. Note that the only C-like backslash sequences that you can portably assume to be interpreted are '\n' and '\\'
; in particular '\t' is not portable, and matches a 't' under most implementations of 'sed', rather than a tab character.
注意,sed支持的正則表達式是貪婪匹配的。例如,從行左向行右匹配時,如果滿足匹配的匹配字符有多個,則會取最長的。(注:例如字符串'abbbc',給定正則表達式"ab*",滿足該正則的字符串序列有"a","ab","abb"以及"abbb",由於是貪婪匹配,它會取最長的,即"abbb")
示例:
'abcdef'
匹配字符串"abcdef"。
'a*b'
匹配0或多個a,但后面需要跟上字符b,例如'b'或'aaaaab'。
'a\?b'
匹配"b"或"ab"。
'a\+b\+'
匹配一個或多個a,且后面需要有一個或多個b,所以"ab"是最短的匹配序列,其他的如'aaaab'、'abbbbb'或'aaaaaabbbbbbb'都能被匹配。
'.*'
'.\+'
都匹配所有字符;但是,第一個匹配任何字串(包含空字串),而第二個只匹配包含至少一個字符的字串(空格也是字符)。
(注:即第一種會匹配所有,包括null字符,第二種不能匹配null字串。例如:sed -n '/.*/p'
會匹配空行。sed -n '/.\+/p'
不會匹配空行,這是匹配非空行的一種簡便方法。)
'^main.*(.*)'
匹配以"main"開頭的行,且該行還包括括號。
'^#'
匹配以"#"開頭的行。
'\\$'
匹配以反斜線結尾的行。
'\$'
匹配包含美元符號的行。
'[a-zA-Z0-9]'
在C字符集環境下,這匹配任意ASCII的字母和數字。
'[^ tab]\+'
(此處tab代表一個制表符)匹配不包含空格和制表符的行。通常用於匹配整個單詞。
'^\(.*\)\n\1$'
匹配相鄰兩行完全相同的情況。
'.\{9\}A$'
匹配9個任意字符,后面還帶一個字母A。
'^.\{15\}A'
匹配以任意15個字符開頭但第16個字符是A的行。
3.4 Often-Used Commands(sed常用命令)
如果要掌握sed,下面幾個命令幾乎是必須掌握的。
'#'
不能跟在定址表達式后。
以"#"開頭的行表示注釋行。
如果要考慮可移植性,需要注意有些sed可能僅支持一條單行注釋,且此時"#"必須是SCRIPT中的第一個字符。
注意:如果SCRIPT中的前兩個字符是"#n",則表示強制開啟選項"-n",而非注釋。如果想要開啟一個注釋行來注釋以字母"n"開頭的字符串,那么需要使用大寫字母N代替,或者"#"和"n"之間加上一個或多個空白字符。
'q [EXIT-CODE]'
該命令只能在單定址表達式下使用。
表示立即退出sed,而不再處理后續的命令和輸入流。注意,退出sed前,當前pattern space中的內容會被輸出,除非使用了"-n"選項。返回一個退出狀態碼是GNU sed的擴展功能。(注:后文還有一個"Q"命令,和"q"命令作用相同,但退出sed前不輸出當前pattern space中的內容。)
'd'
刪除模式空間中的內容,並立即進入下一個sed循環(注:即以"break"的方式直接退出當前SCRIPT循環,並讀取輸入流中的下一行,再執行SCRIPT循環)。
'p'
輸出當前模式空間的內容(到標准輸出中)。該命令一般只和"-n"選項一起使用。
'n'
輸出模式空間的內容(除非自動輸出功能被禁止),然后讀取下一行到模式空間中替換其中的內容。如果輸入流中沒有下一行供"n"讀取(注:即此前已經讀取了輸入流的最后一行),sed將直接退出,不會執行SCRIPT中后續的命令。
(
注:以下面兩個命令為例:其中"i"命令為插入字符串並輸出。
[root@xuexi ~]# echo -e "line1\nline2\nline3\nline4" | sed -n 'n;p;i aaa'
line2
aaa
line4
aaa
[root@xuexi ~]# echo -e "line1\nline2\nline3" | sed -n 'n;p;i aaa'
line2
aaa
第二個命令讀取了第三行到pattern space后執行n命令,但此時輸入流中已經沒有下一行供讀取,於是直接結束,鏈后續的"p"命令都沒有執行。
)
'{ COMMANDS }'
使用大括號將一系列命令組合成一個命令組。在某些時候特別有用,例如相對某一匹配行或某一范圍中的行做多次處理時。
3.5 The 's' Command(sed的s命令)
sed的"s"命令(該命令用於字符串替換)的語法格式為"s/REGEXP/REPLACEMENT/FLAGS"
。"s"命令中的字符"/"可以統一被替換成任意其他單個字符(注:在定址正則表達式中斜杠也可以被替換成其他字符,但需要在第一個被替換字符前加上反斜線轉義,例如\%REGEXP%
,而s命令中替換時無需加反斜線)。如果斜線字符"/"(或被替換后的其他字符)需要出現在REGEXP或REPLACEMENT中,必須使用反斜線進行轉義。
sed的"s"命令算的上是sed程序中最重要的命令,它有很多不同的選項。但它的基本概念很簡單:"s"命令使用REGEXP匹配pattern space中的內容,如果匹配成功,則匹配成功的那部分字符串被替換為REPLACEMENT。
REPLACEMENT中可以使用"\N"
(N是從1到9的整數)進行后向引用,所代表的是REGEXP第N個括號\(...\)
中匹配的內容。另外,REPLACEMENT中可以包含未轉義的"&"符號,這表示引用pattern space中被匹配的整個內容(注:是pattern space中的所有匹配,不僅僅只是括號的分組匹配)。最后,GNU sed為REPLACEMENT還提供了一些"反斜線加單字母"的特殊字符序列,如下:
'\L'
將REPLACEMENTE轉換成小寫,直到遇到了"\U"或"\E"。
'\l'
將REPLACEMENTE中下一個字符轉換成小寫。
'\U'
將REPLACEMENTE轉換成大寫,直到遇到了"\L"或"\E"。
'\u'
將REPLACEMENT中下一個字符轉換成大寫。
'\E'
停止"\L"和"\E"開啟的大小寫轉換。
(
注:使用方法如下示例
shell> echo hello | sed 's/e/ \Uyour\Lname /'
h YOURname llo
)
當使用了"g"修飾符時,大小寫轉換不會從一個正則匹配事件擴展到另一個正則匹配事件。例如,如果pattern space中的內容為"a-b-",執行下面的命令:
s/\(b\?\)-/x\u\1/g
得到的輸出為"axxB"。當替換第一個"-"時,"\u"
只影響"\1"
代表的空值。當替換"b-"時,由於"\1"
代表的是"b",所以字符"b"會被替換為"B",但添加到pattern space中的"x"仍然為小寫。
另一方面,"\l"
和"\u"
會影響REPLACEMENT中的空引用的后一個字符。例如:模式空間內容為"a-b-"
,執行下面的命令:
s/\(b\?\)-/\u\1x/g
將使用"X"(大寫)替換"-",使用"Bx"替換"b-"。如果這樣的結果不是你想要的結果,可以在此例中的"\1"
后加上"\E"
防止"x"被轉換。
如果想要在最后的REPLACEMENT中包含字面符號"\"
、"&"
或換行符,需要在REPLACEMENT中這些字符之前加上反斜線。
sed的"s"命令可以接0或多個如下列出的修飾符(FLAGS):
'g'
使用REPLACEMENT替換所有被REGEXP匹配的內容,而非第一次被匹配到的。
(注:sed任何動作都是在pattern space進行的,字符串替換也如此。不加"g"修飾符時,將只pattern space中第一個被匹配到的內容,加上"g",將替換pattern space中所有被匹配到的內容,因此如果是多行工作模式,第二行或更多行只要能被匹配都會被替換。)
'NUMBER'
為一個整數值N。表示只替換第N個被匹配到的內容。
注意:POSIX標准中沒有說明既指定"g"又指定"NUMBER"修飾符時會如何處理,並且當前各種sed的實現也沒有標准說法。對於GNU sed而言,定義如下:忽略第NUMBER個匹配前的所有匹配,然后從第NUMBER個匹配開始向后重新匹配並替換所有匹配成功的內容。
'p'
替換動作完成后打印新的模式空間中的內容。
注意:當既指定"p"又指定"e"命令時,它們的前后順序不同,會得到兩種不同結果。一般來說,"ep"
這種順序得到的結果可能是所期望的結果,但另一種順序"pe"
對調試很有用。出於這個原因,當前版本的GNU sed特地解釋了"p"命令在"e"命令前(或后)時,將在"e"命令生效前(或后)輸出內容,因為"s"命令的每個修飾符一般都只展示一次結果。雖然這種行為已經寫入文檔,但未來可能會改變。
'w FILE-NAME'
該子命令表示將模式空間的內容寫入到指定的文件FILE-NAME中。GNU sed支持兩個特殊的FILE-NAME:"/dev/stderr"
和"/dev/stdout"
,分別表示寫入到標准錯誤和標准輸出中。
'e'
該命令允許通過管道將shell命令的執行結果直接傳遞到pattern space中。當替換動作完成后,將搜索pattern space,發現的命令會被執行,並且執行結果覆蓋到當前pattern space中。它會禁用尾隨換行符,並且如果待執行命令中包含了NULL字符,該命令將不被定義為命令,即表示它是普通字符而不是命令所以不執行。這是GNU sed的功能。
(
注:"s"命令的"e"修飾符似乎只有搜索pattern space並找出其中的命令並執行的功能,沒有選項描述中第一句話所述的功能。但"e"命令有該功能,例如:
echo -e "good\nbad" | sed 'e echo haha'
haha
good
haha
bad
不討論e命令的用法,關於"s"命令的"e"修飾符的用法如下:
文件ttt.txt的內容如下:
[root@xuexi tmp]# cat ttt.txt
ls /tmp
haha
excute a command
將第二行的"haha"替換成一個命令后使用"e"修飾符,那么在替換完成后會查找模式空間,如果找到了可以執行的命令就執行。
[root@xuexi tmp]# sed 's/haha/du -sh \/tmp/ge' ttt.txt
ls /tmp # 注意這一行沒有執行
18M /tmp # 說明執行了du -sh /tmp的命令
excute a command
注意到第一行雖然也是可以執行的命令,但是卻沒有執行,因為"e"是"s"命令的修飾符,需要成功匹配"haha"的行才能符合"e"修飾符。
注意模式空間中的內容是一行一行的,命令將把整行內容作為命令行。例如,如果只將excute替換成du -sh /tmp
,那么模式空間中的內容將是"du -sh /tmp a command",它會對/tmp和當前目錄下的"a和"command"目錄進行"du -sh"統計,但很可能"a"或"command"目錄根本不存在,這時就會報錯。
[root@xuexi tmp]# sed 's%excute%du -sh /tmp%ge' ttt.txt
ls /tmp
haha
du: cannot access 'command': No such file or directory # 不存在command目錄所以錯誤
18M /tmp
4.0K a # 當前目錄下正好存在a目錄,所以統計了信息
並且如果替換后找到的命令不在行首(有前導空白沒有影響),將出現錯誤,因為是將整行作為命令來執行的。
[root@xuexi tmp]# sed 's%command%du -sh /tmp%ge' ttt.txt
ls /tmp
haha
sh: excute: command not found
所以更保險的方法是對整行進行替換。
[root@xuexi tmp]# sed 's%^excute.*%du -sh /tmp%ge' ttt.txt
ls /tmp
haha
18M /tmp
當然,如果想要執行第一行的"ls /tmp"命令也很簡單,只需匹配這一行。也可以在此命令上進行命令擴展。如下面將/tmp替換后,模式空間的內容是"ls -ld /tmp;du -sh /tmp",所以會執行這兩條命令。
[root@xuexi tmp]# sed 's!/tmp!-ld /tmp;du -sh /tmp!ge' ttt.txt
dr-xr-xr-x. 17 root root 8736768 Oct 25 14:11 /tmp
18M /tmp
haha
excute a command
sed '${p;s/.*/:>filename/e;d} filename'
)
'I'
'i'
這兩個修飾符作用相同,表示在REGEXP匹配的時候忽略大小寫。這是GNU擴展功能。
'M'
'm'
和前文定址表達式中所述的M修飾符作用一致。見M修飾符。
(注:除了上述明確的修飾符外,還有一個特殊的不算修飾符的符號"\",當它寫在"s"命令的REPLACEMENT的每個行尾時,表示新轉義當前命令行的行尾,也就是開啟一個新行。例如:
echo 'abcdef' | sed 's/^.*$/\ &\ /'
這表示在abcdef這一行內容前后分別加一個空行,也就是將abcdef這一行嵌入到空行中間。所以可將其理解為換行符"\n",但必須注意,如果轉義新行后有內容,則此新行必須再次使用反斜線終止該行,因為它的行尾已經被轉義了。例如:
echo 'abcdef' | sed 's/^.*$/\ &\ xyz\ /'
echo 'abcdef' | sed 's/^.*$/&\ /'
其實個人覺得使用"\n"方便多了,即容易理解又容易控制。例如上面最后一個命令改為:echo 'abcdef' | sed 's/^.*$/&\n/'
。在后文的好幾個例子中都是用了轉義行尾的技巧,所以此處提上一提。
)
3.6 Less Frequently-Used Commands(比較少用的sed命令)
雖然可能用的不如前面所述的命令頻繁,但這些小命令有時候非常有用。
'y/SOURCE-CHARS/DEST-CHARS/'
(y命令中的斜線"/"可以統一被替換成其他單個字符。)
轉換pattern space中能被SOURCE-CHARS匹配的字符為DEST-CHARS,且是一一對應地轉換。
斜線"/"(或被替換為的其他字符)、反斜線"\"和換行符可以出現在SOURCE-CHARS或DEST-CHARS中,但需要使用反斜線進行轉義。(轉義后的)SOURCE-CHARS和DEST-CHARS中字符的數量必須完全相同。
'a\'
'TEXT'
是GNU sed的擴展功能,該命令可以在兩個地址格式的定址表達式后使用。
隊列化該命令后的文本內容(最后一個反斜線"\"是文本結束符,在輸出時該符號會被移除),並在當前SCRIPT循環結束時輸出,或從輸入流中讀取下一行時輸出。(注:本質是:只要"有讀取下一行"的動作就會觸發隊列化內容的輸出,不止是"a"、還有"i"和"c"以及"r"等,另外,除了sed循環的第一個動作可以讀取下一行,命令"N"和"n"都會讀取下一行,因此它們也會觸發這些隊列化內容的輸出)
輸入的文本內容中如果要出現反斜線字符,需要進行轉義,即"\\"
。
作為一個GNU擴展功能,如果命令"a"和換行符之間存在非空白字符"\"序列,則此反斜線會開啟新行,並且反斜線后的第一個非空白字符作為下一行的第一個字符。這同樣適用於下面的"i"和"c"命令。
(注:命令"a"為尾部追加插入,其動作是將輸入文本隊列化在內存中,它不會進入模式空間,而是隱含動作自動輸出模式空間內容時,在半路追上stdout並將隊列化的文本追加到其尾部,並同時輸出。下面的"i"和"c"命令所操作都是stdout,都不會進入pattern space。也就是說,這3個命令操作的文本內容不受任何sed其他選項和命令控制。如"-n"選項無法禁止該輸入,因為"-n"禁止的是pattern space的自動輸出,同時,如果SCRIPT中這3個命令后還有后續的命令序列,則這些命令也無法匹配和操作這些文本內容,例如"p"命令不會輸出這些隊列化文本,因為它不是pattern space中的內容。
這是sed程序中少見的幾個在pattern space外處理數據的動作。具體用法如下兩個示例:
[root@xuexi tmp]# cat set2.txt
carrot
cookiee
gold
[root@xuexi tmp]# sed '/carrot/a\iron\nsteel' set2.txt # 換行插入
carrot
iron
steel
cookiee
gold
[root@xuexi tmp]# sed '/carrot/a\iron\ # iron作為第一行,其后跟\繼續輸入下一行
> steel' set2.txt # 直到遇到結束引號,隊列化文本才結束 carrot iron steel cookiee gold
)
'i\'
'TEXT'
是GNU的擴展功能,該命令可以在兩個地址格式的定址表達式后使用。
立即輸出該命令后的文本(除了最后一行,每一行都以反斜線"\"結尾,但在輸出時會自動移除)。
(注:"i"命令同"a"命令一樣,只不過是在stdout流的頭部加上隊列化的文本,並同時輸出。它同樣不進入pattern space,操作的也是stdout流)
'c\'
'TEXT'
刪除從模式空間輸出的內容,並在最后一行處(如果沒有指定定址選項則每一行)輸出"c"命令后隊列化文本(除了最后一行,每行都以"\"結尾,輸出時會移除)。該命令處理完后會開始新的sed循環,因為模式空間的內容將會被刪除。
(注:"c"即change,表示使用隊列化文本修改輸出流,雖說是修改,但實際上是替換模式空間的輸出流並輸出。它和"i"、"a"兩個命令不一樣,它"冒充"了輸出流輸出后就立即進入下一個sed循環,使得SCRIPT中后續的命令不會執行,而"i"、"a"則不會退出循環,而是會繼續回到SCRIPT循環中執行后續的命令)
(注:雖然這3個命令用的遠不如"s"命令頻繁,但我個人認為,理解了這3個命令,就理解了大半sed的工作機制。)
'='
是GNU擴展功能,可以在兩個地址格式的定址表達式后使用。
該命令用於輸出當前正在處理的輸入流中的行號(行號后會加上尾隨換行符)。
(注:這是將sed內部保存的行號計數器的值輸出,是內存中的內容,因此不進入pattern space,不受"-n"選項影響。)
'l N'
將模式空間中的內容以一種明確的形式打印:非打印字符(和"\"字符)被打印成C語言風格的轉義形式;長行被分割后打印,使用"\"字符來表示分割的位置;每一行的結尾被標記上"$"符。
N指定了行分割時期望的行長度(即多少字符換行),長度指定為0表示不換行。如果忽略N,則使用默認長度(默認70)。N參數是GNU sed的擴展功能。
'r FILENAME'
GNU擴展功能,該命令接受兩個地址的定址表達式。
讀取filename中的內容並按行隊列化,然后在當前SCRIPT循環的最后或者讀取下一行前插入到output流中。注意,如果filename無法被讀取,它將被當成一個空文件而不會產生任何錯誤提示。
作為GNU sed的擴展功能,支持特殊的filename值:/dev/stdin,表示從標准輸入中讀取內容。
(注:
-
"r"命令的功能和"a"命令的作用是完全一樣的,連工作機制都完全相同,除了"r"命令是從文件中讀取隊列化文本,而"a"讀取的是命令行中提供的。
-
從"r"命令之后到換行符或sed的引號之間的所有內容都作為"r"的文件參數。因此r命令之后如果還有命令,應當分行寫。
-
"r"命令是一次隊列化filename中的所有行,后文還有一個"R"命令是每次隊列化filename中的一行,它們都受輸出流的影響,只要有輸出流就追加到輸出流中,並開始下一輪的隊列化過程。並非sed每從輸入流中讀取一行就隊列化一次)
)
'w FILENAME'
將模式空間的內容寫入到filename中,作為一個GNU sed擴展,它支持兩種特殊的filename值:/dev/stderr和/dev/stdout,分別表示將模式空間的內容寫到標准錯誤和標准輸出中。
在輸入流的第一行被讀入前,filename文件將被創建(或截斷,即清空);所有引用相同filename的"w"命令(包括替換命令"s"后的"w"修飾符)不會先關閉filename文件再打開該文件來寫入。
(注:
- 即sed腳本中使用了"w"命令后該filename文件對sed而言一直是處於打開狀態的,直到sed退出才關閉。多次使用引用相同文件的"w"命令會向該打開文件中寫入,因此多個"w"寫入filename時是追加寫入的方式,但如果"w"打開的文件已存在,則會先截斷該文件,即后續追加式的"w"輸出會覆蓋原文件。
- "w"命令不會影響pattern space的輸出,它就像tee命令一樣,一份輸出到屏幕,一份重定向到filename中。
)
'D'
如果pattern space中未包含換行符,則像"d"命令中提到的一樣進入下一個sed循環。否則刪除pattern space中第一個換行符之前的所有內容,並重新回到SCRIPT的頂端重新對pattern space中剩下的內容進行處理,而不會讀取輸入流中的下一行。
(注:換句話說,D命令總是刪除pattern space中第一個換行符前的內容,如果刪除后pattern space中還有內容剩下,則直接回到SCRIPT頂端重新對剩下的這部分內容執行命令,即以"continue"的方式強制進入下一個SCRIPT循環,如果pattern space中沒有剩下內容,則直接退出當前SCRIPT循環,並進入下一個sed循環。)
'N'
在當前pattern space中的尾部加上一個換行符,並讀取輸入流的下一行追加到換行符后面。如果輸入流中沒有下一行供"N"讀取,則直接退出sed循環。
(注:無論是sed循環的第一步、還是n命令或是N命令,它們讀取下一行到pattern space時總會移除行尾換行符。但N命令在讀取下一行前在當前pattern space的尾部加上了一個"\n",這使得pattern space中有了兩行,如果一個SCRIPT中有多次N命令還可能有更多行。因此N命令是sed進入多行工作模式的方法。但要注意,雖然稱為多行模式,但在pattern space中加入換行符並非真的換行,在pattern space中仍是一行顯示的,因為它能被匹配到,且在計算字符數量時會被計入,它僅僅只是一個特殊符號,只不過在輸出時會換行顯示)
'P'
輸出pattern space中第一個換行符"\n"前面的內容。
(注:即輸出pattern space中的第一行)
'h'
使用pattern space中的內容替換hold space中的內容。 (注:替換后,pattern space內容仍然保留,不會被清空)
'H'
在hold space的尾部追加一個換行符,並將pattern space中的內容追加到hold space的換行符尾部。
(注:追加后,pattern space內容仍然保留,不會被清空)
'g'
使用hold space中的內容替換pattern space中的內容。
'G'
在當前pattern space的尾部追加一個換行符,並將hold space中的內容追加到pattern space的換行符尾部。
(注:這是另一個進入多行模式空間的方法。這就有了一個特殊的用法,sed 'G' filename
可以在filename的每一行后添加一個空行,這也是很多人產生誤解的地方,認為hold space的初始內容為空行,追加到pattern space就成了空行。其實並非如此,因為無論是pattern space還是hold space在初始時都是空的,G命令在pattern space的尾部加上了換行符,並將hold space中的空內容追加到換行符后,使得這成了一個空行。)
'x'
交換pattern space和hold space的內容。
3.7 Commands for 'sed' gurus(大師級的sed命令)
在大多數情況下,使用這些命令意味着你可能可以使用像"awk"或"perl"等的工具更好地達到目的。但是偶爾有人會堅持使用'sed',這些命令可能會使得腳本變得非常復雜。
(注:雖說標簽功能很少使用,但有時候確實非常有用,它在sed腳本內部實現了簡單的編程結構體)
':LABEL'
不可接在定址表達式后。
設置一個分支標簽LABEL供"b"、"t"或"T"(注:"T"命令在后文的GNU sed擴展中說明)命令跳轉。除此功能,無任何作用。
(注:冒號和LABEL之間不能有空格,雖然原文中有空格,但這是錯誤的)
'b LABEL'
無條件跳轉到標簽LABEL上。如果LABEL參數省略,則跳轉到SCRIPT的尾部准備進入下一個sed循環,即跳轉到隱含動作auto_print
的前面。
't LABEL'
如果對最近讀入的行有"s"命令的替換成功了,則跳轉到LABEL標簽處。如果LABEL參數省略,則跳轉到SCRIPT的尾部准備進入下一個sed循環,即跳轉到隱含動作auto_print
的前面。(注:該命令和"T LABEL"相反,"T"是在"s"替換不成功時跳轉到指定標簽。)
(注:另外,"t"的跳轉條件是有"s"命令替換成功,不是最近一個"s"命令替換成功,所以如果有多個"s"命令,即使"t"命令最近的"s"命令不成功,則仍會跳轉,直到一個跳轉循環內都沒有替換成功的才終止跳轉。例如:
`echo 'abcdef' | sed ':x;s/haha/yyy/;s/def/haha/;s/yyy/zzz/;tx'`
abczzz
第一輪,被讀取的輸入行在第一個"s"和第3個"s"命令失敗,但第二個成功,所以仍會執行跳轉,於是進入第二輪。在第二輪,第一個"s"和第三個"s"替換成功,所以繼續跳轉,於是進入第三輪。在第三輪中,所有"s"都失敗,所以不再跳轉。
)
(注:篇幅原因,使用一個示例簡單解釋一下標簽和跳轉命令的使用。
- 使用":LABEL"設置好標簽后,就可以使用"b LABEL"、"t LABEL"或"T LABEL"命令跳轉到該標簽。
- 跳轉到某標簽的意思是開始執行該標簽后的命令。
- 如果"b"、"t"或"T"命令省略了參數LABEL,則跳轉到隱藏標簽,即自動輸出pattern space動作
auto_print
的前面。
假設有如下test.txt文件,該文件中空白處都是一個個的空格。目的是多個連續的空格替換成圓圈"○",有幾個空格就替換成幾個圓圈,但初始時只有一個空格的不進行替換(即第一行的第一個空格和最后一行的最后一個空格不替換)。
[root@xuexi ~]# cat test.txt
sleep sleep
sleep sleep
sleep sleep
sleep sleep
sleep sleep
sleep sleep
sleep sleep
可以使用下面簡單的命令來實現:
[root@xuexi ~]# sed 's/ /○○/g;s/○ /○○/g' test.txt
sleep○○○○○○○sleep
○○sleep○○○○○○sleep
○○○sleep○○○○○sleep
○○○○sleep○○○○sleep
○○○○○sleep○○○sleep
○○○○○○sleep○○sleep
○○○○○○○sleep sleep
如果使用標簽功能,就可以變相地實現sed命令組的循環和條件判斷功能。例如使用"b"標簽跳轉功能來實現,語句如下:
[root@xuexi ~]# sed '
:replace;
s/ /○○/;
/ /b replace;
s/○ /○○/g' test.txt
該語句首先定義了一個標簽replace
,在其后是第一個要執行的命令,作用是將兩個空格替換成兩個圓圈的"s"命令,隨后是"b"標簽跳轉語句,表示如果行中有兩個連續的空格,就使用"b"命令跳轉到replace
標簽處,於是再次執行"s"命令,替換結束后再次回到/ /b replace
命令,於是這樣就可以對每一輸入行實現循環替換動作,直到最后行中沒有連續空格。但此時可能還會有一個多余的空格跟在圓圈后面,於是就能滿足s/○ /○○/g
命令的替換條件。
同樣,還可以使用"t"標簽完成上述要求。
[root@xuexi ~]# sed '
:replace;
s/ /○○/;
t replace;
s/○ /○○/g' test.txt sleep○○○○○○○sleep ○○sleep○○○○○○sleep ○○○sleep○○○○○sleep ○○○○sleep○○○○sleep ○○○○○sleep○○○sleep ○○○○○○sleep○○sleep ○○○○○○○sleep sleep
其實"t"標簽更可能用於實現像shell中case的條件判斷功能。例如:
sed '{ s/abc/ABC/; t; s/opq/OPQ/; t; s/xyz/XYZ/}' filename
這表示如果能替換什么成功,就跳轉到尾部結束,是多選一的情況。
)
3.8 Commands Specific to GNU 'sed'(GNU sed特有的命令)
這些命令是GNU sed特有的命令,因此必須謹慎使用它們,並且sed腳本沒有可移植性的要求。
'e [COMMAND]'
該命令允許將shell命令行的輸出結果插入到pattern space中。不給定參數COMMAND時,"e"命令將搜索pattern space並執行發現的命令,並將命令的執行結果替換到pattern space中(注:類似於shell下的命令替換功能)。
如果指定了"COMMAND"參數,"e"命令將解析它並認為它是一個shell命令,並在(子)shell環境下執行后將結果發送到輸出流中。
無論是否指定了COMMAND參數,如果待執行命令中包含了空字符,則都不被認為是一個命令。
注意,不像"r"命令,"e"命令的結果是立即輸出的;而"r"命令需要延遲到當前SCRIPT循環結束時才會輸出。
'F'
輸出當前處理的輸入流的文件名(輸入完后會換行)。
'L N'
這是一個失敗的GNU擴展功能,無視。
'Q [EXIT-CODE]'
該命令只能在單個定址表達式下使用。
該命令和命令"q"相同,除了它不會輸出pattern space中的內容(注:"q"命令在退出前會輸出當前pattern space中的內容)。同樣,像"q"那樣可以提供一個退出狀態碼。
該命令非常有用,因為要完成這個顯而易見的簡單功能的唯一替代方法是使用'-n'選項(這可能會使腳本不必要地復雜化)或使用以下代碼片段,但這會浪費時間讀取整個文件且沒有任何效果可見:
(注:即使使用了"-n"選項,性能也不如"q"和"Q",因為它仍會讀完整個輸入流,而"q | Q"是立即退出sed程序。)
:eat
$d Quit silently on the last line
N Read another line, silently
g Overwrite pattern space each time to save memory
b eat
'R FILENAME'
每次讀取FILENAME中的一行並隊列化在內存中,其余的像"r"選項一樣,在每個SCRIPT循環結束時追上stdout流並追加在其尾部。如果FILENAME無可讀取或到達了文件尾部,將不會給出任何報錯信息。
和"r"命令一樣,它也支持特殊的FILENAME值:/dev/stdin,表示從標准輸入中讀取數據。
(注:"R"命令是每次隊列化filename中的一行,而"r"命令是一次隊列化filename中的所有行。它們都受輸出流的影響,有一次輸出就追加到輸出流中,並開始下一輪的隊列化過程。並非sed每從輸入流中讀取一行就隊列化一次)
'T LABEL'
和"t LABEL"相反,該命令是僅當"s"命令未完成替換時跳轉到標簽LABEL。如果LABEL參數省略,則跳轉到SCRIPT的尾部准備進入下一個sed循環,即跳轉到隱含動作auto_print
的前面。
'v VERSION'
該命令除了讓sed程序在不支持GNU sed功能的時候失敗不做任何事。此外,可以指定一個版本號,表示你的sed腳本必須要高於此版本的sed程序才能運行,例如指定"4.0.5"。默認指定的版本號為"4.0",因為這是第一個支持該命令的版本。
'W FILENAME'
將pattern space中第一個換行符之前的內容寫入到filename中。除此之外,和"w"命令完全相同。
'z'
該命令用於清空pattern space。可以認為它等價於"s/.*//"
,但卻效率更高,且可以在輸入流中存在無效多字節序列(例如UTF-8時,一個字符占用多個字節)的情況下工作。POSIX要求這樣的序列無法使用"."進行匹配,因此沒有任何方法既可以清空sed的buffer空間,又能保證sed腳本的可移植性。
3.9 GNU Extensions for Escapes in Regular Expressions(GNU對正則表達式的反斜線擴展)
直到目前位置,我們只遇到了"\^"
格式的轉義,它告訴sed不要將脫字符"^"解釋為特殊元字符,而是解釋為一個普通的字面符號。再例如,"\*"
匹配的是單個*
字符,而不是匹配0或多個反斜線字符。
本小節介紹另一種類型的轉義。如下:
'\a'
匹配一個BEL字符,即一個"蜂鳴警告"(ASCII 7)。
'\f'
匹配一個分頁符(ASCII 12)。
'\n'
匹配一個換行符(ASCII 10)。
'\r'
匹配一個回車符(ASCII 13)。
'\t'
匹配一個水平制表符(ASCII 9)。
'\v'
匹配一個垂直制表符(ASCII 11)。
'\cX'
Produces or matches 'CONTROL-X', where X is any character. The precise effect of '\cX' is as follows: if X is a lower case letter, it is converted to upper case. Then bit 6 of the character (hex 40) is inverted. Thus '\cz' becomes hex 1A, but '\c{' becomes hex 3B, while '\c;' becomes hex 7B.
'\dXXX'
匹配十進制ASCII值為XXX的字符。
'\oXXX'
匹配八進制ASCII值為XXX的字符。
'\xXX'
匹配十六進制ASCII值為XX的字符。
'\b'
(回退刪除鍵)無法實現,因為"\b"
用於匹配單詞的邊界。
以下轉義序列匹配特殊的字符類,且只在正則表達式中有效:
'\w'
匹配任意單詞組成字符。單詞的組成成分包括:字母、數字和下划線。其他任意字符都不是單詞的一部分。
'\W'
匹配人非單詞字符。即匹配除了字母、數字和下划線的任意字符。
'\b'
匹配單詞的邊界位置。
'\B'
匹配非單詞的邊界位置。
"\`"
此為sed獨有正則表達式。匹配pattern space中的開頭。在多行模式下,這和"^"
是不同的。
"\'"
此為sed獨有正則表達式。匹配pattern space中的尾部。在多行模式時,這和"$"
是不同的。
(注:關於最后兩個正則表達式,它們是sed才可以使用的。在多行模式下,它們和"^"、"$"的區別,見M修飾符。)
4. Some Sample Scripts(一些簡單的示例腳本)
這里有一些sed示例腳本,可以引導你成為大師級的sed使用者。
(注:說實話,下面的17個示例看的我懷疑人生,真的沒想到sed功能強大如斯,但也難的想死,這絕對是骨灰級玩家才能玩的)
- 示例菜單:
一些吸引人的示例:
Centering lines
Increment a number
Rename files to lower case
Print bash environment
Reverse chars of lines
模仿一些標准的工具:
tac : Reverse lines of files
cat -n : Numbering lines
cat -b : Numbering non-blank lines
wc -c : Counting chars
wc -w : Counting words
wc -l : Counting lines
head : Printing the first lines
tail : Printing the last lines
uniq : Make duplicate lines unique
uniq -d: Print duplicated lines of input
uniq -u: Remove all duplicated lines
cat -s : Squeezing blank lines
4.1 Centering Lines(文本中心線)
該腳本將所有行都划分中心線,每行最多80個字符,中心線的位置指定為第40個字符處。如果要改變行寬度,則下面的'\{...\}'
中的數字必須更改,同時需要在腳本的第一段中添加或刪除適當個數的空格。
注意此處是如何使用buffer空間的命令來分隔兩部分的,這是一個通用技巧。
(注:如果行的內容原本就超過81個字符,則這些行的行尾會被截斷)
#!/usr/bin/sed -f
# Put 80 spaces in the buffer
1 {
x
s/^$/ /
s/^.*$/&&&&&&&&/
x
}
# del leading and trailing spaces
y/tab/ / s/^ *//
s/ *$//
# add a newline and 80 spaces to end of line
G
# keep first 81 chars (80 + a newline)
s/^\(.\{81\}\).*$/\1/
# \2 matches half of the spaces, which are moved to the beginning
s/^\(.*\)\n\(.*\)\2/\2\1/
(注:從此腳本可知,雖在pattern space中加上了換行符,但其實不是真的換行,它在pattern space中仍是一行,只不過在輸出時換行符使結果換了行。因此,N命令進入多行模式也是一樣的。同時,換行符是一個字符,能被匹配,也能被替換掉)
4.2 Increment a Number(數值增長)
該腳本是sed中少數幾個演示了如何做算術計算的示例。這確實很有可能,但只能手動實現。
為了增長一個數值,只需要為該數的末尾加上1,並替換原末尾即可。但有一種例外:當末尾數位9時,其前面一位也必須增長,如果這還是9,則還需增長,直到無需進位時才結束。
Bruno Haible提供的解決方案非常聰明,因為它只使用了一個buffer空間。它是這樣工作的:將末尾9替換成下划線,然后使用多個"s"命令增長最后一個數字(下划線前面的數字也屬於最后一個數字),最后將所有的下划線替換為0。
(注:下面的td
和tn
是t d
和t n
的簡寫。)
#!/usr/bin/sed -f
/[^0-9]/ d
# replace all trailing 9s by _ (any other character except digits, could
# be used)
:d
s/9\(_*\)$/_\1/
td
# incr last digit only. The first line adds a most-significant
# digit of 1 if we have to add a digit.
s/^\(_*\)$/1\1/; tn
s/8\(_*\)$/9\1/; tn
s/7\(_*\)$/8\1/; tn
s/6\(_*\)$/7\1/; tn
s/5\(_*\)$/6\1/; tn
s/4\(_*\)$/5\1/; tn
s/3\(_*\)$/4\1/; tn
s/2\(_*\)$/3\1/; tn
s/1\(_*\)$/2\1/; tn
s/0\(_*\)$/1\1/; tn
:n
y/_/0/
(注:例如為該sed腳本傳遞一個個位數8,由於該數據是以8結尾的,因此將一直執行到s/8\(_*\)$/9\1/;tn
將8增長為9,並跳轉到標簽n,標簽n后的命令不用對其處理,所以直接輸出9。假如傳遞的數字是3999,則:d;s/9\(_*\)$/_\1/;td
將一直循環,首先替換成"399_"
,再循環一次替換成"39__"
,最后一次循環替換成"3___"
,由於此時結尾沒9了,且是以3結尾,於是繼續向下直到s/3\(_*\)$/4\1/; tn
,它會將"3___"
替換成"4___"
,並跳轉到標簽n,y/_/0/
將把下划線替換成0得到4000,最后輸出結果為4000。
4.3 Rename Files to Lower Case(重命名文件名為小寫)
這是一個非常奇怪的sed應用。轉換文件名文本,並轉換其為shell命令,然后將它們提供給shell。
該應用的主體是其中的sed腳本部分,它會重新將名稱的小寫字母映射為大寫,甚至還檢查重新映射后的名稱是否和原始名稱相同。注意腳本是如何使用shell變量和正確的引號進行參數化的。
#! /bin/sh
# rename files to lower/upper case...
#
# usage:
# move-to-lower *
# move-to-upper *
# or
# move-to-lower -R .
# move-to-upper -R .
#
help()
{
cat << eof
Usage: $0 [-n] [-R] [-h] files...
-n do nothing, only see what would be done
-R recursive (use find)
-h this message
files files to remap to lower case
Examples:
$0 -n * (see if everything is ok, then...)
$0 *
$0 -R .
eof
}
apply_cmd='sh'
finder='echo "$@" | tr " " "\n"'
files_only=
while :
do
case "$1" in
-n) apply_cmd='cat' ;;
-R) finder='find "$@" -type f';;
-h) help ; exit 1 ;;
*) break ;;
esac
shift
done
if [ -z "$1" ]; then
echo Usage: $0 [-h] [-n] [-r] files...
exit 1
fi
LOWER='abcdefghijklmnopqrstuvwxyz'
UPPER='ABCDEFGHIJKLMNOPQRSTUVWXYZ'
case `basename $0` in
*upper*) TO=$UPPER; FROM=$LOWER ;;
*) FROM=$UPPER; TO=$LOWER ;;
esac
eval $finder | sed -n ' # remove all trailing slashes s/\/*$// # add ./ if there is no path, only a filename /\//! s/^/.\// # save path+filename h # remove path s/.*\/// # do conversion only on filename
# (注:注意這里是如何引用變量的。本質是將其還原為shell的token,所以遇到變量,就在其前和后使用引號將其暴露給shell) y/'$FROM'/'$TO'/ # now line contains original path+file, while # hold space contains the new filename x # add converted file name to line, which now contains # path/file-name\nconverted-file-name G # check if converted file name is equal to original file name, # if it is, do not print anything /^.*\/\(.*\)\n\1/b # escape special characters for the shell s/["$'\\]/\\&/g
# now, transform path/fromfile\n, into
# mv path/fromfile path/tofile and print it
s/^\(.*\/\)\(.*\)\n\(.*\)$/mv "\1\2" "\1\3"/p
' | $apply_cmd
4.4 Print 'bash' Environment(打印"bash"環境)
該腳本去除了'set'命令中的shell function的定義。
#!/bin/sh
set | sed -n '
:x
# if no occurrence of "=()" print and load next line
/=()/! { p; b; }
/ () $/! { p; b; }
# possible start of functions section
# save the line in case this is a var like FOO="() "
h
# if the next line has a brace, we quit because
# nothing comes after functions
n
/^{/ q
# print the old line
x; p
# work on the new line now
x; bx
'
4.5 Reverse Characters of Lines(反轉行中的字符)
該腳本可用反轉行中字符的位置,它使用了一次性移動兩個字符的技術,因此它的速度比直觀看上去的更快。
注意標簽定義語句前面的"tx"命令,它用來重置"t"標簽跳轉的條件,免得被第一個"s"命令影響而跳轉。
#!/usr/bin/sed -f
/../! b
# Reverse a line. Begin embedding the line between two newlines
# (注:下面的"s"命令使用了轉義行尾的技巧,其實和"sed /^.*$/\n&\n/"是等價的)
s/^.*$/\
&\
/
# Move first character at the end. The regexp matches until
# there are zero or one characters between the markers
tx
:x
s/\(\n.\)\(.*\)\(.\n\)/\3\2\1/
tx
# Remove the newline markers
s/\n//g
(注:例如echo abcde | sed -f file.sed
,傳入的是"abcde",首先判斷它不是單字符,否則將跳轉到尾部,然后將"abcde"嵌入到兩個相同的特殊字符中,此處采用的是嵌入到兩空行中,為了解釋方便,假設這里的特殊字符為"#"符號,於是得到"#abcde#",隨后一個"t x"命令用於重置跳轉條件,避免第一個s命令的成功狀態影響跳轉條件。再執行第二個s命令,該命令將兩個"#"前后的字符反轉,第一輪循環得到"e#dcb#a",隨后跳轉再次替換,得到"ed#c#ba",再次跳轉,但這一次替換不成功。最后將所有的"#"刪除,即得到最終結果"edcba")
4.6 Reverse Lines of Files(反轉文件中的行)
這是一個沒什么用但卻很有意思的腳本,是模仿unix命令。就像tac
一樣工作。
注意,非GNU sed執行該腳本時很容易內存溢出。
#!/usr/bin/sed -nf
# reverse all lines of input, i.e. first line became last, ...
# from the second line, the buffer (which contains all previous lines)
# is *appended* to current line, so, the order will be reversed
1! G
# on the last line we're done -- print everything
$ p
# store everything on the buffer again
h
4.7 Numbering Lines(打印行號)
該腳本和cat -n
的功能一樣。當然,這個腳本沒什么用,兩個原因:第一,有人使用C語言實現了該功能,第二,下面這個shell腳本可以實現相同的目的卻更快。
#! /bin/sh
sed -e "=" $@ | sed -e ' s/^/ / N s/^ *\(......\)\n/\1 / '
它首先使用sed來輸出行號,然后通過"N"兩行一分組並進行調整。當然,這個腳本並沒有下面這個腳本的引導意義多。
這個算法同時使用了兩個buffer空間,使得每一行可以盡快被打印出來然后丟棄。而行號被分離,使得數字改變后可以放入一個buffer空間,而沒有改變的數字在另一個buffer空間;改變的數字使用一個"y"命令來修改。於是行號被存儲在hold space,在下一個迭代中被使用上。
#!/usr/bin/sed -nf
# Prime the pump on the first line
x
/^$/ s/^.*$/1/
# Add the correct line number before the pattern
G
h
# Format it and print it
s/^/ /
s/^ *\(......\)\n/\1 /p
# Get the line number from hold space; add a zero
# if we're going to add a digit on the next line
g
s/\n.*$//
/^9*$/ s/^/0/
# separate changing/unchanged digits with an x
s/.9*$/x&/
# keep changing digits in hold space
h
s/^.*x//
y/0123456789/1234567890/
x
# keep unchanged digits in pattern space
s/x.*$//
# compose the new number, remove the newline implicitly added by G
G
s/\n//
h
4.8 Numbering Non-blank Lines(打印非空白行行號)
模范的是"cat -b",它不會計算空行的行號。
在此腳本中和上一個腳本相同的部分沒有給出注釋,於此處可知,對於sed腳本來說,給注釋是多么重要的事。
#!/usr/bin/sed -nf
/^$/ {
p
b
}
# Same as cat -n from now
x
/^$/ s/^.*$/1/
G
h
s/^/ /
s/^ *\(......\)\n/\1 /p
x
s/\n.*$//
/^9*$/ s/^/0/
s/.9*$/x&/
h
s/^.*x//
y/0123456789/1234567890/
x
s/x.*$//
G
s/\n//
h
4.9 Counting Characters(計算字符個數)
該腳本展示了sed另一種做數學計算的方式。此處我們必須可以增大到一個極大的數值,因此要成功實現這一點可能不那么容易。
解決的方法是將數字映射為字母,這是sed的一種算盤功能實現。"a"表示個位數,"b"表示十位數,以此類推。如前所見,我們仍然在hold space中保存總數。
在最后一行上,我們將算盤上的值轉換為十進制數字。為了能適應多種情況,這里使用了循環而不是一大堆的"s"命令:首先轉換個位數,然后從數值中移除字母"a",然后滾動字母使得各個字母都轉換成對應的數值。
#!/usr/bin/sed -nf
# Add n+1 a's to hold space (+1 is for the newline)
s/./a/g
H
x
s/\n/a/
# Do the carry. The t's and b's are not necessary,
# but they do speed up the thing
t a
: a; s/aaaaaaaaaa/b/g; t b; b done
: b; s/bbbbbbbbbb/c/g; t c; b done
: c; s/cccccccccc/d/g; t d; b done
: d; s/dddddddddd/e/g; t e; b done
: e; s/eeeeeeeeee/f/g; t f; b done
: f; s/ffffffffff/g/g; t g; b done
: g; s/gggggggggg/h/g; t h; b done
: h; s/hhhhhhhhhh//g
: done
$! {
h
b
}
# On the last line, convert back to decimal
: loop
/a/! s/[b-h]*/&0/
s/aaaaaaaaa/9/
s/aaaaaaaa/8/
s/aaaaaaa/7/
s/aaaaaa/6/
s/aaaaa/5/
s/aaaa/4/
s/aaa/3/
s/aa/2/
s/a/1/
: next
y/bcdefgh/abcdefg/
/[a-h]/ b loop
p
4.10 Counting Words(計算單詞個數)
該腳本和前一個差不多,每個單詞都轉換成一個單獨的字母"a"(上一個腳本中是每一個字母轉換成一個字母"a")。
有趣的是,"wc"程序對"wc -c"計算字符時做了優化,使得計算單詞的速度要比計算字符的速度慢很多。與之相反,這個腳本的瓶頸在於算術,因此單詞計算的速度要快的多(因為只需維護少量的數值計算)
此腳本中和上一個腳本的共同部分沒有給注釋,這再次說明了sed腳本中注釋的重要性。
#!/usr/bin/sed -nf
# Convert words to a's
s/[ tab][ tab]*/ /g
s/^/ /
s/ [^ ][^ ]*/a /g
s/ //g
# Append them to hold space
H
x
s/\n//
# From here on it is the same as in wc -c.
/aaaaaaaaaa/! bx; s/aaaaaaaaaa/b/g
/bbbbbbbbbb/! bx; s/bbbbbbbbbb/c/g
/cccccccccc/! bx; s/cccccccccc/d/g
/dddddddddd/! bx; s/dddddddddd/e/g
/eeeeeeeeee/! bx; s/eeeeeeeeee/f/g
/ffffffffff/! bx; s/ffffffffff/g/g
/gggggggggg/! bx; s/gggggggggg/h/g
s/hhhhhhhhhh//g
:x
$! { h; b; }
:y
/a/! s/[b-h]*/&0/
s/aaaaaaaaa/9/
s/aaaaaaaa/8/
s/aaaaaaa/7/
s/aaaaaa/6/
s/aaaaa/5/
s/aaaa/4/
s/aaa/3/
s/aa/2/
s/a/1/
y/bcdefgh/abcdefg/
/[a-h]/ by
p
4.11 Counting Lines(統計行數)
就像wc -l
一樣,統計行數。
#!/usr/bin/sed -nf
$=
4.12 Printing the First Lines(打印順數前幾行)
該腳本是sed腳本中最有用又最簡單的。它顯示了輸入流的前10行。使用"q"的作用是立即退出sed,不再讀取額外的行浪費時間和資源。
#!/usr/bin/sed -f
10q
4.13 Printing the Last Lines(打印倒數幾行)
輸出倒數N行比輸出順數前N行要復雜的多,但確實很有用。N值是下面腳本中1,10 !s/[^\n]*\n//
的數值10對應值,此處表示輸出倒數10行。
該腳本類似於tac
腳本,都將每次的結果保留在hold space中最后輸出。
#!/usr/bin/sed -nf
1! {; H; g; }
1,10 !s/[^\n]*\n//
$p
h
關鍵點,該腳本維持了一個10行的窗口,在向其中添加一行時滑動該窗口並刪除最舊的一行(第二個s命令有點類似於"D"命令,但卻不用重新進入SCRIPT循環)
在寫高級或復雜sed腳本時,"窗口滑動"的技術作用非常大,因為像"P"這樣的命令要實現相同的目的需要手動做很多額外的工作。
為了介紹這種技術,在剩下的幾個示例中,均使用了"N"、"P"和"D"命令充分演示了該技術。此處是一個"窗口滑動"技術對"tail"命令的實現。
這個腳本看上去更復雜,但實際上工作方式和上一個腳本是一樣的:當踢掉了合理的行后,不再使用hold space來保存內部行的狀態,而是使用"N"和"D"命令來滑動pattern space:
#!/usr/bin/sed -f
1h
2,10 {; H; g; }
$q
1,9d
N
D
注意在讀取了輸入流的前10行后,其中的第1、2、4行的命令就失效了。之后,所有的工作是:最后一行時推出,追加下一行到pattern space中並移除其內第一行。
4.14 Make Duplicate Lines Unique(移除重復行)
這個示例充分展現了"N"、"D"和"P"命令的藝術所在,這可能是成為大師路上最難的幾個命令。
#!/usr/bin/sed -f
h
:b
# On the last line, print and exit
$b
N
/^\(.*\)\n\1$/ {
# The two lines are identical. Undo the effect of the N command.
g
bb
}
# If the 'N' command had added the last line, print and exit
$b
# The lines are different; print the first and go
# back working on the second.
P
D
正如所見,我們使用"P"和"D"維護了一個2行的窗口空間。這種窗口(滑動)技術在高級sed腳本中經常會使用。
(注:"N"、"P"和"D"是sed中絕佳組合,通常為這3個命令關聯不同的定址表達式以及感嘆號"!"時,得到的結果千變萬化。一方面"N"和"D"可以"滑動窗口",另一方面,"P"和"D"可以輸出窗口的第一行並滑動,再配合"N"或其它進入多行模式的方法(如"G",或"s"命令添加了換行符"\n"),使得窗口的大小可以一直維持下去。通常這3個命令同時使用時,它們的相對前后順序是"NPD"。另外,"D"比較特殊,它會重新進入SCRIPT循環,使得窗口中的內容只保留符合條件的行,因此滑動窗口變得更加"動態",要使用s命令實現這種動態窗口滑動,只能借助"t"標簽跳轉來實現循環)
4.15 Print Duplicated Lines of Input(只打印重復行)
該腳本只打印重復行,就像"uniq -d"一樣。
#!/usr/bin/sed -nf
$b
N
/^\(.*\)\n\1$/ {
# Print the first of the duplicated lines
s/.*\n//
p
# Loop until we get a different line
:b
$b
N
/^\(.*\)\n\1$/ {
s/.*\n//
bb
}
}
# The last line cannot be followed by duplicates
$b
# Found a different one. Leave it alone in the pattern space
# and go back to the top, hunting its duplicates
D
4.16 Remove All Duplicated Lines(移除所有重復行)
該腳本移除所有重復行,就像"uniq -u"一樣。
#!/usr/bin/sed -f
# Search for a duplicate line --- until that, print what you find.
$b
N
/^\(.*\)\n\1$/ ! {
P
D
}
:c
# Got two equal lines in pattern space. At the
# end of the file we simply exit
$d
# Else, we keep reading lines with 'N' until we
# find a different one
s/.*\n//
N
/^\(.*\)\n\1$/ {
bc
}
# Remove the last instance of the duplicate line
# and go back to the top
D
4.17 Squeezing Blank Lines(壓縮連續的空白行)
作為最后一個示例,這里給出了3個腳本,用於增加復雜度和速度,這可以使用"cat -s"來實現同樣的功能:壓縮空白行。
第一個腳本的實現方式是保留第一個空行,如果發現后續還有連續空行,則直接跳過。
#!/usr/bin/sed -f
# on empty lines, join with next
# Note there is a star in the regexp
:x
/^\n*$/ {
N
bx
}
# now, squeeze all '\n', this can be also done by:
# s/^\(\n\)*/\1/
s/\n*/\ /
下面這個腳本要更復雜一些,它會移除所有的第一個空行,並保留最后一個空行。
#!/usr/bin/sed -f
# delete all leading empty lines
1,/^./{
/./!d
}
# on an empty line we remove it and all the following
# empty lines, but one
:x
/./!{
N
s/^\n$//
tx
}
下面這個腳本會移除前導和尾隨空行,速度最快。注意,"n"和"b"會徹底完成整個循環,而不會依賴於sed自動讀取下一行。
#!/usr/bin/sed -nf
# delete all (leading) blanks
/./!d
# get here: so there is a non empty
:x
# print it
p
# get next
n
# got chars? print it again, etc...
/./bx
# no, don't have chars: got an empty line
:z
# get next, if last line we finish here so no trailing
# empty lines are written
n
# also empty? then ignore it, and get next... this will
# remove ALL empty lines
/./!bz
# all empty lines were deleted/ignored, but we have a non empty. As
# what we want to do is to squeeze, insert a blank line artificially
i\
bx
5. GNU sed's Limitations and Non-limitations(GNU sed的限制和優點)
對於那些想寫具有可移植性的sed腳本,需要注意有些sed程序有眾所周知的buffer大小限制,要求不能超過4000字節,在POSIX標准中明確說明了要不小於8192字節。在GNU sed則沒有這些限制。
然而,在匹配的時候是使用遞歸處理子模式和無限重復。這意味着可用的棧空間可能會限制那些可以通過某些pattern處理的緩沖區的大小。
6. Other Resources for Learning About 'sed'(學習sed的其他資源)
除了某些關於sed的書籍(專門研究或作為某個章節討論的shell編程)之外,可以從"sed-users"郵件列表的FAQ中(包含一些sed書籍的推薦和建議)獲取更多關於sed的信息:
http://sed.sourceforge.net/sedfaq.html
此外,還有sed的新手教程和其他一些sed相關資源:
http://www.student.northpark.edu/pemente/sed/index.htm
"sed-user"郵件列表由Sven Guckes負責維護,可以訪問http://groups.yahoo.com來訂閱。
7. Reporting Bugs(Bugs說明)
如要報告bug,郵送至bug-sed@gnu.org,請同時包含在郵件的body中包含sed的版本號,即"sed --version"的輸出結果。
請不要像下面一樣報告bug:
while building frobme-1.3.4
$ configure
error--> sed: file sedscr line 1: Unknown option to 's'
以下是一些容易被報告的bug,但實際上卻不是bug:(注:也就是容易讓人疑惑的點,對於深入理解sed幫助很大)
- 'N'命令在最后一行上的處理方式
大多數版本的sed在"N"命令處理輸入流的最后一行時會直接退出而不打印任何東西。GNU sed會輸出pattern space的內容,除非使用了"-n"選項。這是GNU sed特意的。
例如,sed N foo bar
將依賴於"foo"是偶數行還是奇數行。或者,當想在某個匹配行后讀取后續的幾行時,傳統的sed可能只能寫成這樣:
/foo/{ $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N; $!N }
來替代:
/foo/{ N;N;N;N;N;N;N;N;N; }
無論何時,最簡單的工作方式是使用$d;N
,或者設置"POSIXLY_CORRECT"變量為一個非空值,來解決上述傳統的依賴行為。
- 正則表達式崩潰(問題出在反斜線上)
sed使用的是POSIX的基礎正則表達式語法,根據POSIX標准,有些轉義序列沒有預定義,在sed中需要注意的包括:
\|、\+、\?、\<、\>、\b、\B、\w、\W、\'和\`
由於所有的GNU程序都是用POSIX的基礎正則表達式,sed會將這些轉義符解析為特殊的元字符,因此"x+"會匹配一個或多個"x","abc|def"會匹配"abc"或"def"。
這些語法在用其他版本的sed運行時可能會出現問題,特別是某些版本的sed程序會將"|"和"+"解析為字面符號的"|"和"+"。因此在某些版本的sed上運行需要參考其所支持的正則表達式語法做相應修改。
再者說,某些腳本中"s|abc|def||g"來移除"abc"或"def"字符串,但這只在sed 4.0.x版之前能正常工作,在新版的sed中會將其解析為移除"abc|def"字符串。這在POSIX中同樣沒有定義對應的標准。
- '-i'選項會破壞只讀文件
簡單地說,"sed -i"將刪除只讀文件中的內容。通俗地說,"-i"選項會破壞受保護的只讀文件。這不是bug,而是Unix文件系統工作方式引起的結果。
普通文件的權限表示的是可以對該文件中的數據做什么樣的操作,但是目錄的權限表示的是可以對目錄中的文件做什么樣的操作。"sed -i"絕不會為了寫入而再次打開該文件。取而代之的是,它會先寫入一個臨時文件,最后將其重命名為原文件名:刪除或重命名文件的權限是目錄權限控制的,因此該操作依賴的是目錄的權限,而非文件本身的權限。同樣的原因,"sed -i"不允許修改一個可讀但其目錄只讀的普通文件。
- '0a'無法工作,報錯
根本就沒有0行。0行的概念只有一種情況下使用0,/REGEXP/
,它和1,/REGEXP/
只有一點不同:如果REGEXP能匹配上第一行,則前者的結果是只有第一行,而后者會繼續向下匹配直到能匹配REGEXP,因為范圍地址必須要跨越至少兩行,除非直到最后一行都沒有匹配上。
- '[a-z]'會忽略大小寫
這是字符集環境設置的問題。POSIX強制'[a-z]'這樣的字符列表采用當前系統當前字符集的排序規則進行排序,C字符集環境下,它代表的是小寫字母序列,其他字符集環境下,可能代表的是小寫和大寫字母序列,這依賴於字符集。
為了解決這個問題,可以設置LC_COLLATE
和LC_CTYPE
環境變量為C。
- 's/.*//'不會清空pattern space
會發生這種情況,可能是因為你的輸入流中包含了多字節序列(如UTF-8).POSIX強制這樣的序列字符無法被"."匹配,因此s/.*//
將不會如你所願那樣清空pattern space。事實上,在絕大多數多字節序列環境下,沒有任何辦法腳本的中途清空pattern space。出於這個原因,GNU sed提供了一個"z"命令,它將"\0"作為輸入流的行分隔符。
為了解決這些問題,可以設置LC_COLLATE
和LC_CTYPE
環境變量為C。