2021-05-12 14:32:11
Linux sed 命令詳解系列教學之入門篇
本文目錄:
1 基本概念
2 sed選項
3 定址表示式
4 sed常用命令
5 總結
學習sed的過程中,推薦使用"sedsed"偵錯工具,這對於分析sed處理過程以及pattern space、hold space有很大幫助。
1.基本概念
sed是一個流式編輯器程式,它讀取輸入流(可以是檔案、標準輸入)的每一行放進模式空間(pattern space),同時將此行行號通過sed行號計數器記錄在記憶體中,然後對模式空間中的行進行模式匹配,如果能匹配上則使用sed程式內部的命令進行處理,處理結束後,從模式空間中輸出(預設)出去,並清空模式空間,隨後再從輸入流中讀取下一行到模式空間中進行相同的操作,直到輸入流中的所有行都處理完成。由此可見,sed是一個迴圈一個迴圈處理內容的。
這是sed的一個迴圈的過程:
- 讀取輸入流的一行到模式空間。
- 對模式空間中的內容進行匹配和處理。
- 自動輸出模式空間內容。
- 清空模式空間內容。
- 讀取輸入流的下一行到模式空間。
上述整個迴圈過程中,第2步是我們寫sed命令所修改的地方,其餘的幾個步驟,通過命令列無法改變。但是,sed有幾個命令和選項能改變第3、4步的行為,使其輸出總是輸出空內容或無法清空模式空間。
sed程式的語法格式為:
sed OPTIONS SCRIPT INPUT_STREAM
其中SCRIPT部分就是所謂的sed指令碼,它是sed內部命令的集合,sed中的命令有些奇特,它包含行匹配以及要執行的命令。格式為ADDR1[,ADDR2]cmd_list
。例如,要對第2行執行刪除命令,其命令為sed 2d filename
,只輸出第4行到6行,其命令為sed -n 4,6p
。
sed的內部命令非常多,但既然"花拳繡腿篇",當然只介紹些入門的東西。具體的行匹配方法、有哪些命令以及哪些選項稍後解釋。現在的重點是sed中的迴圈過程。既然SCRIPT是命令的集合,於是上面的迴圈過程可以修改為如下:
- 讀取輸入流的一行到模式空間。
- 對模式空間中內容執行SCRIPT。(包括上面範例中的"2d"和"4,6p")
- 讀取輸入流的下一行到模式空間。
- 對模式空間中內容執行SCRIPT。
其中SCRIPT部分包含了sed命令列中的內部命令,還包括兩個特殊動作:自動輸出和清空模式空間內容。這兩個動作是一定會執行的,只不過有些時候通過某些命令可以使其輸出空內容、使其清空不了模式空間。
如果使用程式設計結構來描述,則大致過程如下:
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;
execute cmd3 in SCRIPT;
……
auto_print;
remove_pattern_space;
done
done
其中while迴圈執行的正是SCRIPT中的所有命令,只不過一般情況下,while迴圈只執行一輪就退出並進入外層的for迴圈。於是,外層的for迴圈稱之為"sed迴圈",內層的while迴圈稱之為"SCRIPT"迴圈。所以,for迴圈只包含了兩個動作:讀取下一行和執行SCRIPT迴圈。
其實while迴圈中是有continue、break甚至是exit的,分別表示回到SCRIPT的頂端(即進入下一個SCRIPT迴圈)、退出當前SCRIPT回圈回到外層sed迴圈以及退出整個sed迴圈。顯然,這不是"花拳繡腿"的內容。
最後,說明下sed命令列如何書寫,其實就是寫SCRIPT部分,這部分的寫法比較靈活,大致有以下幾種:
# 一行式。多個命令使用分號分隔
sed Address{cmd1;cmd2;cmd3...}
# 多個表示式時,可以使用"-e"選項,也可以不用,但使用分號分隔
sed Address1{cmd1;cmd2;cmd3};Address2{cmd1;cmd2;cmd3}...
sed -e 'Address1{cmd1;cmd2;cmd3}' -e 'Address2{cmd1;cmd2;cmd3}' ...
# 分行寫時
sed Address1{
cmd1
cmd2
cmd3
}
Address2{
cmd1
cmd2
cmd3
}
如果是寫在檔案中,即sed指令碼,以檔名為a.sed為例。
#!/usr/bin/sed -f
#注釋行
Address1{cmd1;cmd2...}
Address2{cmd1;cmd2...}
......
有了以上基本的大綱性知識,理解和深入sed機制就簡單多了。
2.sed選項
sed選項不算多,能用到的更沒幾個。
sed OPTIONS SCRIPT INPUT_STREAM
可能用到的幾個選項:
'-n'
預設情況下,sed將在每輪script迴圈結束時自動輸出模式空間中的內容。使用該選項後可以使得這次自動輸出動作輸出空內容,而不是當前模式空間中的內容。注意,"-n"是輸出空內容而不是禁用輸出動作,雖然兩者的結果都是不輸出任何內容,但在有些依賴於輸出動作和輸出流的地方,它們的區別是很大的,前者有輸出流,只是輸出空流,後者則沒有輸出流。
'-e SCRIPT'
前文說了,SCRIPT中包含的是命令的集合,"-e"選項就是向SCRIPT中新增命令的。可以省略"-e"選項,但如果命令列容易產生歧義,則使用"-e"選項可明確說明這部分是SCRIPT中的命令。另外,如果一個"-e"選項不方便描述所需命令集合時,可以指定多個"-e"選項。
'-f SCRIPT-FILE'
指定包含命令集合的SCRIPT檔案,讓sed根據SCRIPT檔案中的命令集處理輸入流。
'-i[SUFFIX]'
該選項指定要將sed的輸出結果儲存(覆蓋的方式)到當前編輯的檔案中。GNU sed是通過建立一個臨時檔案並將輸入寫入到該臨時檔案,然後重新命名為原始檔來實現的。
當當前輸入流處理結束後,臨時檔案被重新命名為原始檔的名稱。如果還提供了SUFFIX,則在重新命名臨時檔案之前,先使用該SUFFIX修改原始檔名,從而生成一個原始檔的備份檔案。
臨時檔案總是會被重新命名為原始檔名稱,也就是說輸入流處理結束後,仍使用原始檔名的檔案是sed修改後的檔案。檔名中包含了SUFFIX的檔案則是最原始檔案的備份。例如原始檔為a.txt,sed -i'.log' SCRIPT a.txt
將生成兩個檔案:a.txt和a.txt.log,前者是sed修改後的檔案,a.txt.log是源a.txt的備份檔案。
重新命名的規則如下:如果擴充套件名不包含符號"*",將SUFFIX新增到原檔名的後面當作檔案字尾;如果SUFFIX中包含了一個或多個字元"*",則每個"*"都替換為原檔名。這使得你可以為備份檔案新增一個字首,而不是字尾。如果沒有提供SUFFIX,原始檔被覆蓋,且不會生成備份檔案。
該選項隱含了"-s"選項。
'-r'
使用擴充套件正規表示式,而不是使用預設的基礎正規表示式。sed所支援的擴充套件正規表示式和egrep
一樣。使用擴充套件正規表示式顯得更簡潔,因為有些元字元不用再使用反斜線""。正規表示式見grep命令中文手冊。
'-s'
預設情況下,如果為sed指定了多個輸入檔案,如sed OPTIONS SCRIPT file1 file2 file3
,則多個檔案會被sed當作一個長的輸入流,也就是說所有檔案被當成一個大檔案。指定該選項後,sed將認為命令列中給定的每個檔案都是獨立的輸入流。
既然是獨立的輸入流,範圍定址(如/abc/,/def/
)就無法跨越多個檔案進行匹配,行號也會在處理每個檔案時重置,"$"代表的也將是每個檔案的最後一行。這也意味著,如果不使用該選項,則這幾個行為都是可以完成的。
範例:以sed命令"p"和"="為例,其中"p"命令用於強制輸出當前模式空間中的內容,"="命令用於輸出sed行號計數器當前的值,即剛被讀入到模式空間中的行是輸入流中的第幾行。
(1).只輸出a.txt中的第5行。
sed -n 5p a.txt
這裡使用了"-n"選項,使得讀取到模式空間的每一行都無法被輸出,只有明確使用了"p"選項才能被"p"動作輸出。由於只有讀入的第5行內容能匹配"5",才能被"p"輸出。
其實上面的命令和sed -n -e '5p' a.txt
是完全一樣的,因為"5p"在sed解析命令列時不會產生歧義,所以可以省略"-e"選項。
(2).輸出a.txt,並輸出每行的行號。
sed '=' a.txt
由於要輸出a.txt的內容,所以不使用"-n"選項,同時"="命令會輸出每行行號。
(3).分別輸出a.txt和b.txt的第5行,並分別儲存到".bak"字尾的檔案中。
sed -i'*.bak' -n '5p' a.txt b.txt
此處必須使用"-s"選項,否則將只會輸出"a.txt+b.txt"結合後的第5行。但"-i"隱含了"-s"選項。這會生成4個檔案:a.txt、b.txt和a.txt.bak、b.txt.bak。前兩個是第5行內容,後兩個是原始檔的備份檔案。
(4).使用擴充套件正規表示式,輸出a.txt和b.txt中能包含3個以上字母"a"的行。
sed -r -n '/aaa+/p' a.txt b.txt
3.定址表示式
當sed將輸入流中的行讀取到模式空間後,就需要對模式空間中的內容進行匹配,如果能匹配就能執行對應的命令,如果不能匹配就直接輸出、清空模式空間並進入下一個sed迴圈讀取下一行。
匹配的過程稱為定址。定址表示式有多種,但總的來說,其格式為[ADDR1][,ADDR2]
。這可以分為3種方式:
- ADDR1和ADDR2都省略時,表示所有行都能被匹配上。
- 省略ADDR2時,表示只有被ADDR1表示式匹配上的行才符合條件。
- 不省略ADDR2時,是範圍地址。表示從ADDR1匹配成功的行開始,到ADDR2匹配成功的行結束。
無論是ADDR1還是ADDR2,都可以使用兩種方式進行匹配:行號和正規表示式。如下:
'N'
指定一個行號,sed將只匹配該行。(需要注意,除非使用了"-s"或"-i"選項,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",則匹配的是每個檔案的最後一行。總之,"$"匹配的是每個輸入流的最後一行。
需要注意的是,sed採用行號計數器來臨時記錄當前行的行號,因此sed在讀取到最後一行前即使是倒數第二行的時候,完全不知道最後一行是第幾行,所以代表最後一行的"$"無法進行任何數學運算,例如倒數第二行使用"$-1"表示是錯誤的。而且,"$"只是一個額外的標記符號,當sed讀取到輸入流的最後一行時,發現這就是最後一行,於是為此行打上"$"記號,並讀取到模式空間中。
'/REGEXP/'
將選擇能被正規表示式REGEXP匹配的所有行。如果REGEXP中自身包含了字元"/",則必須使用反斜線跳脫,即"/"
。
'/REGEXP/I'
和"/REGEXP/"是一樣的,只不過匹配的時候不區分大小寫。
'%REGEXP%'
('%'可以使用其他任意單個字元替換。) 這和上一個定址表示式的作用是一樣的,只不過是使用符號"%"替換了符號"/"。當REGEXP中包含"/"符號時,使用該定址表示式就無需對"/"使用反斜線""跳脫。但如果此時REGEXP中包含了"%"符號時,該符號需要使用""跳脫。
總之,定址表示式中使用的分隔符在REGEXP中出現時,都需要使用反斜線跳脫。
'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 '3p' INPUTFILE
sed -n '3,5!p' INPUTFILE
sed -n '3,/^# .*/! p' INPUTFILE
sed -n '/abc/,/xyz/p' INPUTFILE
sed -n '!p' INPUTFILE # 這個有悖常理,但確實是允許的
4.sed常用命令
sed命令很多,本文的只簡單介紹幾個最常見的。
(1).強制輸出命令"p"。
該命令能強制輸出當前模式空間的內容。即使使用了"-n"選項。
事實上,它們本就不衝突,因為迴圈過程如下:
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;
ADDR1,ADDR2{print}; # "p" command
……
auto_print;
remove_pattern_space;
done
done
在sed處理的過程中,"p"和"auto_print"是兩個輸出動作,都是輸出當前模式空間的內容,只不過auto_print是隱含動作。使用了"-n"選項,其所影響的動作僅是"auto_print",使其輸出空內容。也因此,當沒有使用"-n"選項時,模式空間的內容會被輸出兩次。
例如,僅輸出標准輸入的第2行內容。
[root@linuxidc ~]# echo -e 'abcnxyz' | sed -n 2p
xyz
不加"-n"選項,在"p"輸出之後,SCRIPT迴圈的結尾處還會被auto_print輸出一次。
[root@linuxidc ~]# echo -e 'abcnxyz' | sed 2p
abc
xyz # 這是p命令輸出的結果
xyz # 這是自動輸出的結果
(2).刪除命令"d"。
命令"d"用於刪除整個模式空間中的內容,並立即退出當前SCRIPT迴圈,進入下一個sed迴圈,即讀取下一行。
迴圈大致格式如下:
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;
ADDR1,ADDR2{delete;break}; # "d" command
……
auto_print;
remove_pattern_space;
done
done
唯一需要注意的一點是立即退出當前SCRIPT迴圈,這意味著如果"d"命令後面還有其他的命令,則這些命令都不會執行。
例如:刪除a.txt中的第5行,並儲存到原檔案中。
sed -i '5d' a.txt
這裡不能使用重定向的方式儲存,因為重定向是在sed命令執行前被shell執行的,所以會截斷a.txt,使得sed讀取的輸入流為空,或者結果出乎意料之外。而"-i"選項則不會操作原檔案,而是生成臨時檔案並在結束時重新命名為原檔名。
刪除a.sh中包含"#"開頭的註釋行,但第一行的#!/bin/bash
不刪除。
sed '/^#/{1!d}' a.sh
如果"d"後面還有命令,在刪除模式空間後,這些命令不會執行,因為會理解退出當前SCRIPT迴圈。例如:
echo -e 'abcnxyz' | sed '{/abc/d;=}'
2
xyz
其中"="這個命令用於輸出行號,但是結果並沒有輸出被"abc"匹配的行的行號。
(3).退出sed程式命令"q"和"Q"。
使用"q"和"Q"命令的作用是立即退出當前sed程式,使其不再執行後面的命令,也不再讀取後面的行。因此,在處理大檔案或大量檔案時,使用"q"或"Q"命令能提高很大效率。它們之間的不同之處在於"q"命令被執行後還會使用自動輸出動作輸出模式空間的內容,除非使用了"-n"選項。而"Q"命令則會立即退出,不會輸出模式空間內容。另外,可以為它們指定退出狀態碼,例如"q 1"。
使用了"q"和"Q"的sed迴圈結構大致如下:
# "q"命令
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;
ADDR1,ADDR2{auto_print;exit}; # "q" command
……
auto_print;
remove_pattern_space;
done
done
# "Q"命令
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;
ADDR1,ADDR2{exit}; # "Q" command
……
auto_print;
remove_pattern_space;
done
done
例如,搜尋指令碼a.sh,當搜尋到使用了"."或"source"命令載入環境設定指令碼時就輸出並立即退出。
sed -n -r '/^[ t]*(.|source) /{p;q}' a.sh
(4).輸出行號命令"="。
"="命令用於輸出最近被讀取行的行號。在sed內部,使用行號計數器進行行號計數,每讀取一行,行號計數器加1。計數器的值儲存在記憶體中,在要求輸出行號時,直接插入在輸出流中的指定位置。由於值是存在於記憶體中,而非模式空間中,因此不受"-n"選項的影響。
這是一個依賴於輸出流的命令,只要有輸出動作就會追加在該輸出流的尾部。
例如,搜尋出httpd.conf中"DocumentRoot"開頭的行的行號,允許有前導空白字元。
sed -n '/^[ t]*DocumentRoot/{p;=}' httpd.conf
DocumentRoot "/var/www/html"
119
如果"="命令前沒有"p"輸出命令,且沒有使用"-n"選項,則是輸出在Document所在行的前一行,因為SCRIPT最後的自動輸出動作也有輸出流。
(5).字元一一對應替換命令"y"。
該命令和"tr"命令的對映功能一樣,都是將字元進行一一替換。
例如,將a.txt中包含大寫字母的YES、Yes等替換成小寫的yes。
sed 'y/YES/yes/' a.txt
(6).手動讀取下一行命令"n"。
在sed的迴圈過程中,每個sed迴圈的第一步都是讀取輸入流的下一行到模式空間中,這是我們無法控制的動作。但sed有讀取下一行的命令"n"。
由於是讀取下一行,所以它會觸發自動輸出的動作,於是就有了輸出流。不僅如此,還應該記住的是:只要有讀取下一行的行為,在其真正開始讀取之前一定有隱式自動輸出的行為。
但需注意,當沒有下一行可供"n"讀取時(例如檔案的最後一行已經被讀取過了),將輸出模式空間內容後直接退出sed程式,使得"n"命令後的所有命令都不會執行,即使是那兩個隱含動作。
相應的迴圈結構如下:
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;
ADDR1,ADDR2{ # "n" command
if [ "$line" -ne "$last_line_num" ];then
auto_print;
remove_pattern_space;
read next_line to pattern_space;
else
auto_print;
remove_pattern_space;
exit;
fi
};
……
auto_print;
remove_pattern_space;
done
done
注意,是先判斷是否有下一行可讀取,再輸出和清空pattern space中的內容,所以then和else語句中都有這兩個動作。 也許感覺上似乎更應該像下面這樣的優化形式:
ADDR1,ADDR2{ # "n" command
auto_print;
remove_pattern_space;
[ "$line" -ne "$last_line_num" ] && read next_line to pattern_space || exit;
};
但事實證明並非如此,證明過程在本文結尾。此處暫不討論這些複雜的東西,先看看"n"命令的範例。
例如,搜尋a.txt中包含"redirect"字串的行以及其下一行,並輸出。
sed -n '/redirect/{p;n;p}' a.txt
再例如下面的命令。
echo -e "abcndefnxyz" | sed '/abc/{n;=;p}'
abc
2
def
def
xyz
從結果中可以分析出,"n"讀取下一行前輸出了"abc",然後立即讀入了下一行,所以輸出的行號是2而不是1,因為這時候行號計數器已經讀取了下一行,隨後命令"p"輸出了該模式空間的內容,輸出後還有一次自動輸出的隱含動作,所以"def"被輸出了兩次。
(7).替換命令"s"。
這是sed用的最多的命令。兩個字就能概括其功能:替換。將匹配到的內容替換成指定的內容。
"s"命令的語法格式為:其中"/"可以替換成任意其他單個字元。
s/REGEXP/REPLACEMENT/FLAGS
它使用REGEXP去匹配行,將匹配到的那部分字元替換成REPLACEMENT。FLAGS是"s"命令的修飾符,常見的有"g"、"p"和"i"或"I"。
- "g":表示替換行中所有能被REGEXP匹配的部分。不使用g時,預設只替換行中的第一個匹配內容。此外,"g"還可以替換成一個數值N,表示只替換行中第N個被匹配的內容。
- "p":輸出替換後模式空間中的內容。
- "i"或"I":REGEXP匹配時不區分大小寫。
REPLACEMENT中可以使用"N"(N是從1到9的整數)進行後向參照,所代表的是REGEXP第N個括號(...)中匹配的內容。另外,REPLACEMENT中可以包含未跳脫的"&"符號,這表示參照pattern space中被匹配的整個內容。需要注意,"&"是參照pattern space中的所有匹配,不僅僅只是括號的分組匹配。
例如,刪除a.sh中所有"#"開頭(可以包括前導空白)的註釋符號"#",但第一行"#!/bin/bash"不處理。
sed -i '2,$s/^[ t]*#//' a.sh
為a.sh檔案中的第5行到最後一行的行首加上註釋符號"#"。
sed '5,$s/^/#/' a.sh
將a.sh中所有的"int"單詞替換成"SIGINT"。
sed 's/bintb/SIGINT/g' a.sh
將a.sh中"cmd1 && cmd2 || cmd3"的cmd2和cmd3命令對調個位置。
sed 's%&&(.*) ||(.*)%&&2 ||1%' a.sh
這裡使用了"%"代替"/",且在REPLACEMENT部分對"&"進行了跳脫,因為該符號在REPLACEMENT中時表示的是參照REGEXP所匹配的所有內容。
(8).追加、插入和修改命令"a"、"i"、"c"。
這3個命令的格式是"[a|i|c] TEXT",表示將TEXT內容佇列化到記憶體中,當有輸出流或者說有輸出動作的時候,半路追上輸出流,分別追加、插入和替換到該輸出流然後輸出。追加是指追加在輸出流的尾部,插入是指插入在輸出流的首部,替換是指將整個輸出流替換掉。"c"命令和"a"、"i"命令有一絲不同,它替換結束後立即退出當前SCRIPT迴圈,並進入下一個sed迴圈,因此"c"命令後的命令都不會被執行。
例如:
echo -e "abcndef" | sed '/abc/a xyz'
abc
xyz
def
其實"a"、"i"和"c"命令的TEXT部分寫法是比較複雜的,如果TEXT只是幾個簡單字元,如上即可。但如果要TEXT是分行文字,或者包含了引號,或者這幾個命令是寫在"{}"中的,則上面的寫法就無法實現。需要使用符號""來跳脫行??符號,這表示開啟一個新行,此後輸入的內容都是TEXT,直到遇到引號或者";"開頭的行時。
例如,在a.sh的#!/bin/bash
行後新增一個注釋行"# Script filename: a.sh"以及一個空行。由於是追加在尾部,所以使用"a"命令。
sed '%#!/bin/bash%a# Script filename: a.shn' a.sh
"a"命令後的第一個反斜線用於標記TEXT的開始,"n"用於新增空白行。如果分行寫,或者"a"命令寫在大括號"{}"中,則格式如下:
sed '%#!/bin/bash%a
# Script filename: a.shn
' a.sh
sed '%#!/bin/bash%{p;a
# Script filename: a.shn
;p}' a.sh
最後需要說的是,這3個命令的TEXT是存放在記憶體中的,不會進入模式空間,因此不受"-n"選項或某些命令的影響。此外,這3個命令依賴於輸出流,只要有輸出動作,不管是空輸出流還是非空的輸出流,只要有輸出,這幾個命令就會半路"劫殺"。如果不理解這兩句話,這3個命令的結果有時可能會比較疑惑。
例如,"a"命令是追加在當前匹配行行尾的,但為什麼下面的"haha"卻插入到匹配行"def"的前面去了呢?
echo -e "abcndefnxyz" | sed '/def/{a
haha
;N}'
abc
haha
def
xyz
閱讀了下面的"N"命令之後,再回頭看這個範例,應該能知道為什麼。
(9).多行模式命令"N"、"D"、"P"簡單說明。
在前面已經解釋了"n"、"d"和"p"命令,sed還支援它們的大寫命令"N"、"D"和"P"。
- "N"命令:讀取下一行內容追加到模式空間的尾部。其和"n"命令不同之處在於:"n"命令會輸出模式空間的內容(除非使用了"-n"選項)並清空模式空間,然後才讀取下一行到模式空間,也就是說"n"命令雖然讀取了下一行到模式空間,但模式空間仍然是單行資料。而"N"命令在讀取下一行前,雖然也有自動輸出和清空模式空間的動作,但該命令會把當前模式空間的內容鎖住,使得自動輸出的內容為空,也無法清空模式空間,然後讀取下一行追加到當前模式空間中的尾部。追加時,原有內容和新讀取內容使用換行符"n"分隔,這樣在模式空間中就實現了多行資料。即所謂的"多行模式"。 另外,當無法讀取到下一行時(到了檔案尾部),將直接退出sed程式,使得"N"命令後的命令不會再執行,這和"n"命令是一樣的。
- "D"命令:刪除模式空間中第一個換行符"n"之前的內容,然後立即回到SCRIPT迴圈的頂端,即進入下一個SCRIPT迴圈。如果"D"刪除後,模式空間中已經沒有內容了,則SCRIPT迴圈自動退出進入下一個sed迴圈;如果模式空間還有剩餘內容,則繼續從頭執行SCRIPT迴圈。也就是說,"D"命令後的命令不會被執行。
- "P"命令:輸出模式空間中第一個換行符"n"之前的內容。
"N"、"D"和"P"命令作用非常大,它們是絕佳的組合命令,因為借助它們能實現"視窗滑動"技術,這對於複雜的文字行操作來說大有裨益。但顯然,這不是本文的內容。
此處按照慣例,還是給出它們的大致迴圈結構:其中"N"命令的if判斷和前文的"n"一樣,在本文結尾證明。
# "N"命令的大致迴圈結構
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;
ADDR1,ADDR2{ # "N" command
if [ "$line" -ne "$last_line_num" ];then
lock pattern_space;
auto_print;
remove_pattern_space;
unlock pattern_space;
append "n" to pattern_space;
read next_line to pattern_space;
else
auto_print;
remove_pattern_space;
exit;
fi
};
……
auto_print;
remove_pattern_space;
done
done
# "D"命令的大致迴圈結構
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;
ADDR1,ADDR2{ # "D" command
delete first line in pattern_space;
continue;
};
……
auto_print;
remove_pattern_space;
done
done
# "P"命令的大致迴圈結構
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;
ADDR1,ADDR2{ # "P" command
print first line in pattern_space;
};
……
auto_print;
remove_pattern_space;
done
done
(10).buffer空間資料交換命令"h"、"H"、"g"、"G"、"x"簡單說明。
sed除了維護模式空間(pattern space),還維護另一個buffer空間:保持空間(hold space)。這兩個空間初始狀態都是空的。
絕大多數時候,sed僅依靠模式空間就能達到目的,但有些複雜的資料操作則只能借助保持空間來實現。之所以稱之為保持空間,是因為它是暫存資料用的,除了僅有的這幾個命令外,沒有任何其他命令可以操作該空間,因此借助它能實現資料的永續性。
保持空間的作用很大,它和模式空間之間的資料交換能實現很多看上去不能實現的功能,是實現sed高階功能所必須的,例如"視窗滑動"。同樣,這不是本文的內容。所以只簡單解釋這幾個命令的作用:
- "h"命令:將當前模式空間中的內容覆蓋到保持空間。
- "H"命令:在保持空間的尾部加上一個換行符"n",並將當前模式空間的內容追加到保持空間的尾部。
- "g"命令:將保持空間的內容覆蓋到當前模式空間。
- "G"命令:在模式空間的尾部加上一個換行符"n",並將當前保持空間的內容追加到模式空間的尾部。
- "x"命令:交換模式空間和保持空間的內容。
注意,無論是交換、追加還是覆蓋,原空間的內容都不會被刪除。
總結
看到這裡,對sed已經有了一些概念,也許已經發現了sed的重點在於各選項和各命令是如何影響sed迴圈以及SCRIPT迴圈的。確實如此,在info sed文件中,雖然沒有將這些工作機制詳細描述,但各選項各命令說明中,在需要的時候都提到了這些細節,而我所做的只不過是將其系統性地描述出來、做一些深入,再給幾個範例解釋,並使用通俗易懂的迴圈結構來展示這些機制。
最後,驗證前文"n"和"N"命令留下的疑問:"n"和"N"命令是先判斷是否還有下一行,再自動輸出的。也就是證明下面兩個判斷語句採用前者還是後者的問題。
ADDR1,ADDR2{ # "n" command
if [ "$line" -ne "$last_line_num" ];then
auto_print;
remove_pattern_space;
read next_line to pattern_space;
else
auto_print;
remove_pattern_space;
exit;
fi
};
ADDR1,ADDR2{ # "n" command
auto_print;
remove_pattern_space;
[ "$line" -ne "$last_line_num" ] && read next_line to pattern_space || exit;
};
雖然後者看上去程式碼更優化,但事實上採用的是前者。要證明這一點不太容易,好在我想出了下面的方法來證明。下面的範例中使用的是"N",它和"n"在判斷邏輯上的行為是一致的。
[root@linuxidc ~]# echo -e "abcndefnxyz" | sed '/def/{a
haha
;N}'
abc
haha
def
xyz
[root@linuxidc ~]# echo -e "abcndef" | sed '/def/{a
haha
;N}'
abc
def
haha
在以上兩個命令中,第一個命令"haha"是插入在匹配行"def"的前面,而第二個命令則是插入在"def"的後面。似乎根據"a"命令的作用來說,第二個命令才是意料之中的結果。
首先,解釋第一個命令為何"haha"會出現在匹配行"def"的前面。當sed讀取的行能匹配"def"時,將佇列化"haha"到記憶體中,並在有輸出流的時候追加到輸出流尾部。由於這裡的輸出流來自於"a"命令後的"N"命令,該命令將模式空間鎖住,使得隱含動作自動輸出的內容為空,但佇列化的內容還是發現了這個空輸出流,於是追加在這個空流的尾部。再之後,"N"將下一行讀取到模式空間中,到了SCRIPT迴圈的結尾,再次自動輸出,此時模式空間有兩行:"def" 和 "xyz",這兩行同時被輸出。顯然,在"def"被輸出之前,佇列化的內容已經隨著空輸出流而輸出了。
再解釋為何第二個命令的結果中"haha"在"def"之後,這也是待證明的疑問。第二個命令中,由於"def"已經是輸入流的最後一行,"N"已經無法再讀取下一行,於是輸出當前模式空間內容並退出sed程式。假設,"n"或"N"命令是先自動輸出、清空模式空間內容,再判斷是否有下一行可讀取的,那麼在判斷之前自動輸出時,"N"不知道是否還有下一行,於是佇列化的內容應該同第一個命令一樣,插入在"def"之前。但結果卻並非如此。如果先判斷是否有下一行可供讀取,再輸出、清空模式空間,則佇列化內容是跟隨著"N"退出sed程式前輸出的,這正符合第二個命令的結果。
本文永久更新連結地址:https://www.linuxidc.com/Linux/2018-03/151192.htm
相關文章