sed修鍊系列(四):sed中的疑難雜症,sed疑難雜症
本文目錄:
1 sed中使用變數和變數替換的問題
2 反向引用失效問題
3 "-i"選項的檔案儲存問題
4 貪婪匹配問題
5 sed命令"a"和"N"的糾葛
1.sed中使用變數和變數替換的問題
在指令碼中使用sed的時候,很可能需要在sed中引用shell變數,甚至想在sed命令列中使用變數替換。也許很多人都遇到過這個問題,但引號卻死活調試不出正確的位置。其實這不是sed的問題,而是shell的特性。搞懂sed如何解決引號的問題,對理解shell引號問題有很大協助,觸類旁通,以後在使用awk、mysql等等內建文法解析的工具時就不會再疑惑。
例如下面想輸出a.txt的倒數5行的語句。可能順手就寫出了下面的命令列:
total=`wc -l <a.txt`sed -n '$((total-4)),$p' a.txt
但很不幸,這會報錯。一方面,"$"在sed中是特殊符號,放在定址運算式中時,它表示的是輸入資料流的最後一行的標記。而$(())中也出現了"$"符號,這會讓sed去解析該符號。另一方面,$(())這部分是使用shell計算而不是使用sed計算的,因此必須要將其暴露給shell,以便能讓shell能解析它。
再說說shell中單引號、雙引號和不加引號的情況。
- 單引號:單引號內的所有字元變為字面符號。但注意:單引號內不能再使用單引號,即使使用了反斜線轉義也不允許。
- 雙引號:雙引號內的所有字元變為字面符號,但"\"、"$"、"`"(反引號)除外,如果開啟了"!"引用曆史命令時,則驚嘆號也除外。
- 不使用引號:等同於使用了雙引號。
上面關於雙引號的情況,描述的並不是真正的完整,但已足夠。這些只是它們的字面意義,引號真正的意義在於:決定命令列中哪些"單詞"需要被shell解析,也決定哪些是字面意義不用被shell解析。詳細內容見:shell解析命令列的過程以及eval命令。
顯然,單引號內所有字元都成為了字面符號,shell不會解析其內任何單詞,例如單引號內變數不再被解析、命令替換和算術運算不再執行、不會進行路徑擴充等等。總之,單引號內的字元全是一般字元,如果某些字元需要交給內建解析功能的命令解析,必須使用單引號。例如,"$"、"!"和"{}"在sed中均有特殊意義,要想讓sed能解析它們,必須對它們使用單引號,否則必出錯,或者產生歧義。例如下面3個sed語句中的符號都必須使用單引號才能得到正確結果。
sed '$d' filenamesed '1!d' filenamesed -n '2{p;q}' filename
而想要讓特殊字元被shell解析,必須不能將其包圍在單引號中,可以使用雙引號,也可以不加任何引號,即使不加引號時可能看上去很怪異。例如,上面的算術運算$(())是想被shell解析的,因此必須使用單引號或者不加引號將其暴露給shell。所以正確的語句是:
sed -n $((total-4))',$p' a.txtsed -n "$((total-4))"',$p' a.txtsed -n "$((total-4)),\$p" a.txt
從肉眼看上去,這個語句的引號加的真的很怪異。但shell又不管醜美,它是死的,在劃分命令列的時候它有自己的一套規則,規則怎樣就怎樣劃分。
於是,關於sed如何和shell互動的問題可以得出一套結論:
因此,使用命令替換的方式讓sed輸出倒數5行的語句如下:
sed -n `expr $(wc -l <a.txt) - 4`',$p' a.txt
上面的語句中,`expr $(wc -l <a.txt) - 4` 要被shell解析,因此必須不能使用單引號包圍。而$p部分的"$"要被sed解析成最後一行,必須使用單引號以避免被shell解析。
更複雜一些,在sed的Regex中使用變數替換。例如,輸出a.txt中以變數str字串開頭的行到最後一行。
str="abc"sed -n /^$str/',$p' a.txt
因為沒有使用任何引號,所以$str能如期被shell替換成"abc"。這個命令還有多種寫法:
sed -n '/^'$str'/,$p' a.txtsed -n "/^$str"'/,$p' a.txtsed -n "/^$str/,\$p" a.txtsed -n "/^$str/,"'$'p a.txt
給一個稍難一些的sed符號使用問題。將/etc/shadow中的最後一行的密碼部分替換成"$1$123456$wOSEtcyiP2N/IfIl15W6Z0"。
[root@xuexi ~]# tail -n 1 /etc/shadowuserX:$6$hS4yqJu7WQfGlk0M$Xj/SCS5z4BWSZKN0raNncu6VMuWdUVbDScMYxOgB7mXUj./dXJN0zADAXQUMg0CuWVRyZUu6npPLWoyv8eXPA.::0:99999:7:::
替換語句如下:
old_pass="$(tail -n 1 /etc/shadow | cut -d':' -f2)"new_pass='$1$123456$wOSEtcyiP2N/IfIl15W6Z0'sed -n '$'s%$old_pass%$new_pass% /etc/shadow
由於old_pass和old_pass中包含了"/"和"$"符號,因此"s"命令的分隔字元使用了"%"替代。再仔細觀察new_pass,其內有"."符號,這是Regex的元字元,因此它還可以匹配其他情況。
2.反向引用失效問題
當Regex中使用二者選一的選項"|"時,如果分組括弧()中的內容沒有參與匹配,後向引用將不起作用。例如(a)\1u|b\1將只匹配"aau"的行,不匹配"ba"的行,因為在二者選一的第二個正則中\1代表的分組沒有參與匹配,所以第二個正則中的\1失效,但是第一個正則中的\1有效。
這是正則匹配的問題,不只是sed,其它使用基礎正則和擴充正則引擎的工具也一樣會有這樣的問題。
另外,在s命令中使用反向引用時,將不會引用"s"命令外面的分組。例如:
echo "ab3456cd" | sed -r "/(ab)/s/([0-9]+)/\1/"
得到的結果將是ab3456cd,而不是ababcd,而且如果此時使用\2引用,則會報錯"invalid reference \2 on 's' command's RHS"。
3."-i"選項的檔案儲存問題
sed是通過建立一個臨時檔案,並將輸出寫入到該臨時檔案,然後重新命名該臨時檔案為源檔案來實現檔案儲存的。因此,sed會無視檔案的唯讀性。
是否允許重新命名或移入或刪除檔案,是由檔案所在目錄的許可權控制的。如果目錄為唯讀許可權,則sed無法使用"-i"選項儲存結果,即使該檔案具有可讀許可權。
4.貪婪匹配問題
所謂的貪婪匹配,是指當Regex能匹配多個內容時,取最長的那個。最簡單的例子,給定資料"abcdsbaz",Regex"a.*b"可以匹配該資料中"ab"和"abcdsb",由於貪婪匹配,它會取最長的"abcdsb"。
echo "abcdbaz" | grep -o "a.*b"abcdb
基礎Regex和擴充Regex一直以來的一個不足之處在於無法原生態克服貪婪匹配,像Perl正則或其他程式設計語言的正則實現的比較完整,在""或"+"這種多次重複的匹配後加上一個"?"就可以明確表示採取懶惰匹配的模式,例如"a.?b"。
echo "abcdbaz" | grep -P -o "a.*?b"ab
想要克服基礎正則或擴充正則的貪婪匹配,只能"投機取巧"地採用不包含符號"[^]"來實現。例如上面的:
echo "abcdbaz" | grep -o "a[^b]*b" ab
這種投機取巧的方式,效能比較差,因為基礎或擴充Regex的引擎總是會先匹配出最長的內容,然後往回匹配,這稱為"回溯"。例如"abcdsbaz"在被"a[^b]*b"匹配時,先匹配出"abcdsb",再一個字元一個字元地回退匹配,直到回退到第一個"b"才是最短的結果。
再例如,/etc/passwd檔案中每行資料的格式如下:
rootx:0:0:root:/root:/bin/bash
如何使用sed向/etc/passwd中的每個使用者問聲好,輸出格式大致為:"hello root"、"hello nobody"。
首先,得取出檔案中的第一列,即使用者名稱。但由於該檔案中所有行都採用冒號分隔各欄位,想要使用Regex匹配得到第一段,必須克服貪婪匹配。語句如下:
sed -r 's/^([^:]*):.*/hello \1/' /etc/passwd
注意,sed採用的是基礎正則和擴充正則引擎,在克服貪婪匹配時,它必須先匹配出最長的,再回溯出最短的。
如果想取/etc/passwd中的前兩個欄位呢?只需將克服貪婪的正則當作整體重複一次即可。
sed -r 's/^([^:]*):([^:]*):.*/hello \1 \2/' /etc/passwd
取第三個欄位?
sed -r 's/^([^:]*:){2}([^:]*):.*/hello \2/' /etc/passwd
取第三和第五個欄位?沒辦法,只能將第四個欄位顯式標註出來。
sed -r 's/^([^:]*:){2}([^:]*):([^:]*):([^:]*):/hello \2 \4/' /etc/passwd
取第三道第5欄位?更簡單,重複3次就可以了。
sed -r 's/^([^:]*:){2}(([^:]*:){3}).*/hello \2/' /etc/passwd
但這樣的結果中,第3到第5欄位中必然會包含":"分隔字元,想要去除它?洗洗睡吧!sed本就不擅長處理欄位,克服貪婪匹配本就讓運算式變得很複雜不易讀,而且效率還不高。用它處理欄位,絕對是吃撐了。
5.sed命令"a"和"N"的糾葛
sed的"a"命令作用是將提供的文本資料隊列化在記憶體中,然後在模式空間內容輸出時追加在輸出資料流的尾部一併輸出。
例如,在匹配行"ccc"後插入一行資料"matched successful"。
echo -e "aaa\nbbb\nccc\nddd" | sed '/ccc/a matched successful'aaabbbcccmatched successfulddd
咋一使用"a"命令,很順利,沒毛病。但是結合"N"試試看?
echo -e "aaa\nbbb\nccc\nddd" | sed '/ccc/{a\matched successful;N}'aaabbbmatched successfulcccddd
不是追加在尾部嗎,怎麼跑匹配行的前面去了?即使"N"讀取了下一行,也應該是追加在"ddd"的下一行吧?想要真正弄明白這個問題,對sed模式空間的輸出機制必須了如指掌,可以參考sed修鍊系列(一):花拳繡腿之入門篇。此處簡單描述下"N"命令的輸出機制。
無論是sed自動讀取下一行,還是"n"或"N"命令讀取下一行,只要有讀取動作,在其前面必然會輸出模式空間的內容。當"N"讀取下一行時,首先它會判斷是否還有下一行可供讀取,如果有,則先鎖住模式空間,然後自動輸出並清空模式空間,再解鎖模式空間並向其尾部追加一個分行符號"\n",最後讀取下一行追加到分行符號尾部。由於模式空間被鎖住,使得自動輸出時輸出資料流是空流,也同樣無法清空模式空間。注意,它不是禁止輸出,雖然輸出空流的結果和禁止輸出是一樣的,但輸出空流它有輸出動作,有輸出資料流,會寫入標準輸出,而禁止輸出則沒有輸出動作。如果沒有下一行可供讀取,則自動輸出模式空間、清空模式空間並退出sed程式。過程大致如下所描述:
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
回到"a"命令和"N"命令結合的問題上。之所以"a"命令的隊列化文本會插入在匹配行的前面,問題就出在輸出空流上。"N"在準備讀取下一行時,它有輸出動作,即使輸出結果為空白。而"a"命令是時刻等待sed輸出資料流的,只要一有輸出資料流,立馬就會追上去追加在輸出資料流的屁股後面。因此,"matched successful"會追加在空流的尾部,追加之後"N"才會讀入下一行,最後輸出模式空間中的內容"ccc\nddd",也就得到前面"有悖期待"的結果。
sed系列文章:
sed修鍊系列(一):花拳繡腿之入門篇
sed修鍊系列(二):武功心法(info sed翻譯+註解)
sed修鍊系列(三):sed進階應用程式之實現視窗滑動技術
sed修鍊系列(四):sed中的疑難雜症
回到系列文章大綱:http://www.cnblogs.com/f-ck-need-u/p/7048359.html
轉載請註明出處:http://www.cnblogs.com/f-ck-need-u/p/7499309.html註:若您覺得這篇文章還不錯請點擊下右下角的推薦,有了您的支援才能激發作者更大的寫作熱情,非常感謝!