1. 程式人生 > >sed修煉系列(四):sed中的疑難雜癥

sed修煉系列(四):sed中的疑難雜癥

chan 滑動 特殊符號 container 源文件 重復 target 情況 即使

本文目錄:
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 filename
sed ‘1!d‘ filename
sed -n ‘2{p;q}‘ filename

而想要讓特殊字符被shell解析,必須不能將其包圍在單引號中,可以使用雙引號,也可以不加任何引號,即使不加引號時可能看上去很怪異。例如,上面的算術運算$(())是想被shell解析的,因此必須使用單引號或者不加引號將其暴露給shell。所以正確的語句是:

sed -n $((total-4))‘,$p a.txt
sed -n "$((total-4))"‘,$p a.txt
sed -n "$((total-4)),\$p" a.txt

從肉眼看上去,這個語句的引號加的真的很怪異。但shell又不管醜美,它是死的,在劃分命令行的時候它有自己的一套規則,規則怎樣就怎樣劃分。

於是,關於sed如何和shell交互的問題可以得出一套結論:

  1. 遇到需要被shell解析的都不加引號,或者加雙引號;
  2. 遇到shell和所執行命令共有的特殊字符時,要想被sed解析,必須加單引號,或者在雙引號在加反斜線轉義;
  3. 那些無關緊要的字符,無論加什麽引號。

因此,使用命令替換的方式讓sed輸出倒數5行的語句如下:

sed -n `expr $(wc -l <a.txt) - 4`‘,$p a.txt

上面的語句中,`expr $(wc -l <a.txt) - 4` 要被shell解析,因此必須不能使用單引號包圍。而$p部分的"$"要被sed解析成最後一行,必須使用單引號以避免被shell解析。

更復雜一些,在sed的正則表達式中使用變量替換。例如,輸出a.txt中以變量str字符串開頭的行到最後一行。

str="abc"
sed -n /^$str/‘,$p‘ a.txt

因為沒有使用任何引號,所以$str能如期被shell替換成"abc"。這個命令還有多種寫法:

sed -n ‘/^‘$str‘/,$p‘ a.txt
sed -n "/^$str"‘/,$p‘ a.txt
sed -n "/^$str/,\$p" a.txt
sed -n "/^$str/,"‘$‘p a.txt

給一個稍難一些的sed符號使用問題。將/etc/shadow中的最後一行的密碼部分替換成"$1$123456$wOSEtcyiP2N/IfIl15W6Z0"。

[root@xuexi ~]# tail -n 1 /etc/shadow
userX:$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_passold_pass中包含了"/"和"$"符號,因此"s"命令的分隔符使用了"%"替代。再仔細觀察new_pass,其內有"."符號,這是正則表達式的元字符,因此它還可以匹配其他情況。

2.反向引用失效問題

當正則表達式中使用二者選一的選項"|"時,如果分組括號()中的內容沒有參與匹配,後向引用將不起作用。例如(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.貪婪匹配問題

所謂的貪婪匹配,是指當正則表達式能匹配多個內容時,取最長的那個。最簡單的例子,給定數據"abcdsbaz",正則表達式"a.*b"可以匹配該數據中"ab"和"abcdsb",由於貪婪匹配,它會取最長的"abcdsb"。

echo "abcdbaz" | grep -o "a.*b"
abcdb

基礎正則表達式和擴展正則表達式一直以來的一個不足之處在於無法原生態克服貪婪匹配,像Perl正則或其他編程語言的正則實現的比較完整,在""或"+"這種多次重復的匹配後加上一個"?"就可以明確表示采取懶惰匹配的模式,例如"a.?b"。

echo "abcdbaz" | grep -P -o "a.*?b"
ab

想要克服基礎正則或擴展正則的貪婪匹配,只能"投機取巧"地采用不包含符號"[^]"來實現。例如上面的:

echo "abcdbaz" | grep -o "a[^b]*b" 
ab

這種投機取巧的方式,性能比較差,因為基礎或擴展正則表達式的引擎總是會先匹配出最長的內容,然後往回匹配,這稱為"回溯"。例如"abcdsbaz"在被"a[^b]*b"匹配時,先匹配出"abcdsb",再一個字符一個字符地回退匹配,直到回退到第一個"b"才是最短的結果。

再例如,/etc/passwd文件中每行數據的格式如下:

rootx:0:0:root:/root:/bin/bash

如何使用sed向/etc/passwd中的每個用戶問聲好,輸出格式大致為:"hello root"、"hello nobody"。

首先,得取出文件中的第一列,即用戶名。但由於該文件中所有行都采用冒號分隔各字段,想要使用正則表達式匹配得到第一段,必須克服貪婪匹配。語句如下:

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‘
aaa
bbb
ccc
matched successful
ddd

咋一使用"a"命令,很順利,沒毛病。但是結合"N"試試看?

echo -e "aaa\nbbb\nccc\nddd" | sed ‘/ccc/{amatched successful
;N}‘

aaa
bbb
matched successful
ccc
ddd

不是追加在尾部嗎,怎麽跑匹配行的前面去了?即使"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

註:若您覺得這篇文章還不錯請點擊下右下角的推薦,有了您的支持才能激發作者更大的寫作熱情,非常感謝!

sed修煉系列(四):sed中的疑難雜癥