Perl一行式:處理行號和單詞數
perl一行式程式系列文章:Perl一行式
所有行的行號
$ perl -pe '$_ = "$. $_"' file.log
$ perl -ne 'print "$. $n"' file.log
這裡涉及了一個特殊變數$.
。
這個特殊變數代表的是當前處理行的行號。對於Perl的一行式來說,通過<>
隱式開啟的檔案控制代碼預設不會關閉,所以如果引數中有多個檔案,進入下一個檔案時行號不會重置。
例如:
$ cat a.txt aaa bbb $ cat b.txt ccc ddd # 行號不重置 $ perl -pe '$_ = "$. $_"' a.txt b.txt 1 aaa 2 bbb 3 ccc 4 ddd
如果想要每個檔案的行號都獨立計算。可以使用下面這種方式進行判斷:遇到檔案尾部,顯式關閉檔案。
$ perl -e '
while(<>){
print "$. $_"
}continue{
close ARGV if eof
}' a.txt b.txt
1 aaa
2 bbb
1 ccc
2 ddd
非空行的行號
$.
是Perl自帶的檔案控制代碼上的行號計數器,讀取的每一行都會計數。所以如果想要統計檔案中的某些行的行號,使用自帶的$.
是不可行的,只能自己實現行號計數器。
如下:
$ perl -pe '$_ = ++$x." $_" if /\S/' file.log
這裡的邏輯是,只要行中有非空白字元,就自增一個變數的值。自增後的值和字串進行串聯,並賦值給$_
被-p輸出。
非空行行號並刪除空白行
因為要刪除某些行不輸出,所以不能使用-p選項,它會將所有行都輸出,除非使用s///
來刪除整行。可以考慮使用-n選項。
$ perl -ne 'print ++$x." $_" if /\S/' file.log
輸出匹配行的行號
例如輸出檔案中匹配"nologin"單詞的行號,其它行照常輸出。
$ perl -pe '$_ = "$. $_" if /nologin/' file.log root x 0 0 root /root /bin/bash 2 daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin 3 bin x 2 2 bin /bin /usr/sbin/nologin 4 sys x 3 3 sys /dev /usr/sbin/nologin sync x 4 65534 sync /bin /bin/sync
如果想要單獨計數被匹配行的行號,可以自己寫計數器。
$ perl -pe '$_ = ++$num." $_" if /nologin/' file.log
root x 0 0 root /root /bin/bash
1 daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin
2 bin x 2 2 bin /bin /usr/sbin/nologin
3 sys x 3 3 sys /dev /usr/sbin/nologin
sync x 4 65534 sync /bin /bin/sync
如果需要格式化輸出,使得沒有匹配的行也和帶有行號的行對齊。可以進行多分支的賦值:
$ perl -pe '
$_ = do {
if(/nologin/){
++$num." $_"
}else{
" $_"
}}' file.log
root x 0 0 root /root /bin/bash
1 daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin
2 bin x 2 2 bin /bin /usr/sbin/nologin
3 sys x 3 3 sys /dev /usr/sbin/nologin
sync x 4 65534 sync /bin /bin/sync
或者3目邏輯運算:
$ perl -pe '$_ = /nologin/ ? ++$num." $_" : " $_"' file.log
更規範的格式化可以使用printf來對齊,因為這裡我使用-p選項,使用使用sprintf格式化字串儲存到$_
變數上。
$ perl -pe '
$_ = do {
if(/nologin/){
sprintf("%-3s %s", ++$num, $_);
}else{
sprintf("%-3s %s","", $_);
}}' file.log
root x 0 0 root /root /bin/bash
1 daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin
2 bin x 2 2 bin /bin /usr/sbin/nologin
3 sys x 3 3 sys /dev /usr/sbin/nologin
sync x 4 65534 sync /bin /bin/sync
輸出匹配行及行號
例如輸出能匹配"nologin"的行以及它們的行號。
因為只輸出某些匹配行,而不是所有行,所以不使用-p選項。
$ perl -ne 'print "$. $_" if /nologin/' file.log
2 daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin
3 bin x 2 2 bin /bin /usr/sbin/nologin
4 sys x 3 3 sys /dev /usr/sbin/nologin
如果匹配行的行號要獨立計數,則不使用$.
,自己寫個自增的計數器即可:
$ perl -ne 'print ++$num." $_" if /nologin/' file.log
1 daemon x 1 1 daemon /usr/sbin /usr/sbin/nologin
2 bin x 2 2 bin /bin /usr/sbin/nologin
3 sys x 3 3 sys /dev /usr/sbin/nologin
統計行數
$ perl -lne 'END{print $.}' file.log
5
這裡使用END語句塊,表示執行完主邏輯程式碼後程序退出前執行的,因為這個示例中沒有主邏輯程式碼,所以讀取完所有行後就會執行END語句塊。另外,這裡的-l選項主要用來為print追加換行符。
上面的語句僅會輸出行號,不會輸出檔名,而且多個檔案的時候只會輸出總行數,而不是每個檔案單獨統計。
還有其它實現方式,介紹兩個:
# (1)
perl -le 'print scalar(@tmp = <>)' file.log
perl -le 'print [email protected] = <>' file.log
# (2)
perl -ne '}{print $.' file.log
上面的方式(1)沒有使用-p和-n,所以自己在-e表示式中寫<>
。而@tmp = <>
是讓<>
以列表的方式一次性讀取所有行,然後scalar強制轉換其為標量上下文,於是得到行數量。它等價於scalar( () = <> )
,還等價於$num = () =<>
。
scalar()可以替換成~~
符號,它是兩個位元位取反操作,等價於什麼都不做,但它工作在標量上下文,所以可以用來轉換上下文。
上面的方式(2)使用的是超乎想象的}{
,這不是Perl中的什麼特殊符號,僅僅只是結合-n選項時的一個技巧。-n選項的程式碼邏輯如下:
while(<>){
... -e expression here
}
所以,-e中指定}{print $.
表示破壞原始的-n邏輯,使之變成下面的邏輯:
while(<>){
}{print $.
}
這個格式化一下就是:
while(<>){}
{
print $.
}
也就是說,while迴圈體內不做任何操作,直到<>
讀取完成後,while結束,然後執行一次性語句塊{print $.}
。所以,-ne }{xxx
等價於在END語句塊中執行xxx
操作。
模仿 wc -l
wc -l
會單獨輸出每個檔案的行數,並總計所有檔案的行數。例如:
$ wc -l file.log
5 file.log
$ wc -l file.log paragraph.log
5 file.log
18 paragraph.log
23 total
所以,這也可以使用perl一行式程式來實現。因為這個邏輯中需要單獨統計每個檔案,所以必須顯式區分每個檔案。使用eof判斷每個檔案的尾部即可。
$ perl -M'List::Util qw(sum)' -lne '
# 將@ARGV儲存起來,以便後續能夠按先後順序獲取所有檔名
BEGIN{
@files = @ARGV;
}
# 每個檔案處理完時,儲存行和檔案資訊,並關閉檔案以便重置行號計數器
if(eof){
# 將每個檔案的行數註冊到一個hash結構中
# hash的key為當前處理的檔名
$line_filename{$ARGV} = $.;
close ARGV;
}
END{
# 獲取總行數以及總行數的字元長度,以便格式化對齊
$total_lines = sum values %line_filename;
$longest = length $total_lines;
# 輸出每個檔案對應的行數及檔名,且按照@ARGV的順序輸出
foreach (@files){
printf "%${longest}d %s\n",$line_filename{$_},$_;
}
# 輸出總行數
print "$total_lines total";
}
' file.log paragraph.log
這是遇到的第一個比較大的程式,這樣的邏輯應該寫成Perl指令碼而不是一行式程式。不過這裡的幾個知識點很適合引入Perl一行式。
先分析下這段程式的邏輯:要統計總行數,且要輸出每個檔案對應的行數,輸出時還要進行格式化對齊,所以先將每個檔案對應的行數儲存到一個hash結構中,最後在END語句塊中計算總行數,並計算總行數有多少個字元以便確定格式化對齊時的字元數量。
上面使用了-M
選項,它表示匯入一個模組。此處所匯入的模組是List::Util
模組,它是額外的列表(陣列)工具模組,該模組中有不少處理列表的工具。例如這裡使用qw(sum)
表示匯入這個模組中的sum函式,用於對列表元素進行加總。如果不寫qw(sum)
,那麼在使用sum函式的時候,需要寫完整的名稱List::Util::sum @arr
。
"-l"選項的目的是給print函式追加換行符。
另外這裡使用了BEGIN語句塊來儲存@ARGV
陣列,雖然%line_filename
中也能取得所有的檔名,但hash結構中的元素是無序的,要保證檔名的順序,只能使用陣列(列表)來儲存。再者,因為@ARGV
中的引數檔案會隨著<>
的讀取而被剔除出@ARGV
,所以應該在BEGIN中對@ARGV
進行儲存。
統計非空白行的行數
用計數器實現非常簡單:
$ perl -lne '++$num if /\S/;END{print $num+0;}' paragraph.log
這裡的邏輯非常簡單。唯一需要注意的是$num+0
,因為檔案可能是空的,使得END語句塊中的$num
變數仍處於未定義狀態。加上一個+0
,可以保證它會輸出數值格式,未定義時則輸出0。
為了保證得到數值,可以使用int函式進行轉換:
$ perl -lne '++$num if /\S/;END{print int $num;}' paragraph.log
我準備在這裡引入grep函式的簡單用法。
Shell中有個grep命令可以用來匹配內容,在Perl中也有一個grep函式,它的簡單工作方式可以類同於shell的grep命令,用於篩選列表中符合條件的元素,並將這些元素構成一個新的列表。比如能正則匹配的元素、操作後布林真的元素。
所以,可以使用grep函式來匹配非空白行:
$ perl -e '@lines = grep /\S/,<>;print "@lines"' paragraph.log
grep期待的是列表上下文,使得<>
一次性讀完所有行形成一個列表,然後grep對這個列表的每個元素進行篩選,只要是非空白行都放入一個新的列表。
那麼要統計非空白行數就非常簡單了,直接將grep的結果轉換成標量上下文就可以。
$ perl -le 'print ~~grep /\S/,<>' paragraph.log
前面說過,~~
可以用來轉換標量上下文。
其實Perl grep函式要強大的多,它支援完整的流程控制邏輯。如有需要,參考Perl grep函式。
計數每個單詞
為檔案中每行中的單詞進行計數。
$ perl -pe 's/(\w+)/"<".++$num.">.$1"/ge' file.log
<1>.first <2>.paragraph:
<3>.first <4>.line <5>.in <6>.1st <7>.paragraph
<8>.second <9>.line <10>.in <11>.1st <12>.paragraph
<13>.third <14>.line <15>.in <16>.1st <17>.paragraph
這裡使用s///
命令的e修飾符。該修飾符可以評估s/reg/replacement/
的replacement部分,將其作為Perl的程式碼被perl執行,然後進行s替換操作。
正如上面的示例,每一行匹配後評估replacement部分是"<".++$num.">.$1"
,被perl執行的話,這裡的++$num
就會在perl環境下執行自增操作,$1
也會被替換成已匹配的分組,最後完成s的替換。
下面的示例可能會更容易理解一些:
$ perl -pe 's/(\w+)/++$num/ge' file.log
1 2:
3 4 5 6 7
8 9 10 11 12
13 14 15 16 17
如果想讓它們頂格輸出,可以繼續刪除行首空白:
$ perl -pe 's/(\w+)/++$num/ge;s/^\s+//' file.log
1 2:
3 4 5 6 7
8 9 10 11 12
13 14 15 16 17
再比如,每行的單詞單獨計數,只需在每次讀入行的開頭進行計數器重置即可:
$ perl -pe '$num=0;s/(\w+)/"<".++$num.">.$1"/ge' file.log
<1>.first <2>.paragraph:
<1>.first <2>.line <3>.in <4>.1st <5>.paragraph
<1>.second <2>.line <3>.in <4>.1st <5>.paragraph
<1>.third <2>.line <3>.in <4>.1st <5>.paragraph