首頁 > 軟體

Linux sed 命令詳解系列教學之各種問題解決

2020-06-16 16:54:15

本文目錄:
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不會解析其內任何單詞,例如單引號內變數不再被解析、命令替換和算術運算不再執行、不會進行路徑擴充套件等等。總之,單引號內的字元全是普通字元,如果某些字元需要交給自帶解析功能的命令解析,必須使用單引號。例如,"$"、"!"和"{}"在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互動的問題可以得出一套結論:

  • 遇到需要被shell解析的都不加引號,或者加雙引號;
  • 遇到shell和所執行命令共有的特殊字元時,要想被sed解析,必須加單引號,或者在雙引號在加反斜線跳脫;
  • 那些無關緊要的字元,無論加什麼引號。

因此,使用命令替換的方式讓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_pass和old_pass中包含了"/"和"$"符號,因此"s"命令的分隔符使用了"%"替代。再仔細觀察new_pass,其內有"."符號,這是正規表示式的元字元,因此它還可以匹配其他情況。

2.反向參照失效問題

當正規表示式中使用二者選一的選項"|"時,如果分組括號()中的內容沒有參與匹配,後向參照將不起作用。例如(a)1u|b1將只匹配"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 "aaanbbbncccnddd" | sed '/ccc/a matched successful'
aaa
bbb
ccc
matched successful
ddd

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

echo -e "aaanbbbncccnddd" | sed '/ccc/{a
matched successful
;N}'

aaa
bbb
matched successful
ccc
ddd

不是追加在尾部嗎,怎麼跑匹配行的前面去了?即使"N"讀取了下一行,也應該是追加在"ddd"的下一行吧?想要真正弄明白這個問題,對sed模式空間的輸出機制必須瞭如指掌,可以參考Linux 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"才會讀入下一行,最後輸出模式空間中的內容"cccnddd",也就得到前面"有悖期待"的結果。

本文永久更新連結地址https://www.linuxidc.com/Linux/2018-03/151220.htm


IT145.com E-mail:sddin#qq.com