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

sed系列文章:html

sed修煉系列(一):花拳繡腿之入門篇
sed修煉系列(二):武功心法(info sed翻譯+註解)
sed修煉系列(三):sed高級應用之實現窗口滑動技術
sed修煉系列(四):sed中的疑難雜症mysql


1.sed中使用變量和變量替換的問題

在腳本中使用sed的時候,極可能須要在sed中引用shell變量,甚至想在sed命令行中使用變量替換。也許不少人都遇到過這個問題,但引號卻死活調試不出正確的位置。其實這不是sed的問題,而是shell的特性。搞懂sed如何解決引號的問題,對理解shell引號問題有很大幫助,舉一反三,之後在使用awk、mysql等等自帶語法解析的工具時就不會再疑惑。正則表達式

例以下面想輸出a.txt的倒數5行的語句。可能順手就寫出了下面的命令行:sql

total=`wc -l <a.txt`
sed -n '$((total-4)),$p' a.txt

但很不幸,這會報錯。一方面,"$"在sed中是特殊符號,放在定址表達式中時,它表示的是輸入流的最後一行的標記。而$(())中也出現了"$"符號,這會讓sed去解析該符號。另外一方面,$(())這部分是使用shell計算而不是使用sed計算的,所以必需要將其暴露給shell,以便能讓shell能解析它。shell

再說說shell中單引號、雙引號和不加引號的狀況。數據庫

  • 單引號:單引號內的全部字符變爲字面符號。但注意:單引號內不能再使用單引號,即便使用了反斜線轉義也不容許。
  • 雙引號:雙引號內的全部字符變爲字面符號,但"\"、"$"、"`"(反引號)除外,若是開啓了"!"引用歷史命令時,則感嘆號也除外。
  • 不使用引號:幾乎等同於使用了雙引號,但會進行大括號和波浪號擴展。

上面關於雙引號的狀況,描述的並非真正的完整,但已足夠。這些只是它們的字面意義,引號真正的意義在於:決定命令行中哪些"單詞"須要被shell解析,也決定哪些是字面意義不用被shell解析詳細內容見:shell解析命令行的過程以及eval命令編程

顯然,單引號內全部字符都成爲了字面符號,shell不會解析其內任何單詞,例如單引號內變量再也不被解析、命令替換和算術運算再也不執行、不會進行路徑擴展等等。總之,單引號內的字符全是普通字符,若是某些字符須要交給自帶解析功能的命令解析,必須使用單引號。例如,"$"、"!"和"{}"在sed中均有特殊意義,要想讓sed能解析它們,必須對它們使用單引號,不然必出錯,或者產生歧義。例以下面3個sed語句中的符號都必須使用單引號才能獲得正確結果。bash

sed '$d' filename
sed '1!d' filename
sed -n '2{p;q}' filename

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

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%p /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/{a\ matched 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",也就獲得前面"有悖期待"的結果。

6.sed中感嘆號取反的彎彎繞繞

你知道使用"!"號取反,但也許你並無發現感嘆號能夠放在定址表達式後,也能夠放在命令的前面。這二者雖然都是取反,但意義決然不一樣,最終致使的結果也不一樣。

  1. 感嘆號在定址表達式後,表示對行進行篩選。表示知足條件的行不執行命令,但不知足的行會執行。
  2. 感嘆號在命令的前面,表示知足條件的行不執行該命令,且不知足的行也不執行(不執行是由於沒有被定址表達式匹配到)。這是對模式空間中的行篩選要執行的命令。

假如文件a.txt中包含了3行:

djkaldahsdf
abcskdf2das
chhdsjaj

對於如下三個sed腳本:

  • (1)./^abc/!{d}
  • (2)./^abc/{!d}
  • (3)./^abc/!d

示例(1)中感嘆號放在定址表達式後,這表示不是以字母"abc"開頭的行會執行d刪除命令。而那些以"abc"開頭的行,則不符合定址表達式,後續的d命令不會執行。也就是說,該sed腳本的做用是:除了"abc"開頭的行,其他行全刪除,所以只輸出第2行。

示例(2)中感嘆號放在命令的前面,而非定址表達式後面。這表示的是以"abc"開頭的行不執行d命令。而那些不以"abc"開頭的行因爲不知足定址條件,也不會執行d命令。也就是說,該sed腳本的d命令是多餘的,任何行都不會刪除。所以全部行都輸出。

示例(3)等價於示例(1),由於定址匹配動做優先於命令的執行,感嘆號直接被認爲是定址表達式的一部分。

但無論哪一種狀況,對於不知足定址表達式的(定址後的感嘆號也算是定址表達式的一部分)行,都不會執行後續任何命令,這些行是直接自動輸出的,由"-n"選項控制是否將其輸出。

7.sed卡死,cpu 100%問題

有些人可能遇到過這種問題,特別是sed處理以UTF-8格式導出的數據庫文件。

之因此會出現這樣的問題,是由於字符集的問題,確切地說是本地環境(locale)和文件的編碼不一致。

若是出現這樣的問題,能夠將LC_COLLATE和LC_CTYPE環境變量設置爲C。也能夠簡單地設置LANG=C或LC_ALL=C。

相關文章
相關標籤/搜索