Perl一行式:處理行號和單詞數

perl一行式程序系列文章Perl一行式html


全部行的行號

$ perl -pe '$_ = "$. $_"' file.log
$ perl -ne 'print "$. $n"' file.log

這裏涉及了一個特殊變量$.shell

這個特殊變量表明的是當前處理行的行號。對於Perl的一行式來講,經過<>隱式打開的文件句柄默認不會關閉,因此若是參數中有多個文件,進入下一個文件時行號不會重置。express

例如:數組

$ cat a.txt
aaa
bbb
$ cat b.txt
ccc
ddd

# 行號不重置
$ perl -pe '$_ = "$. $_"' a.txt b.txt
1 aaa
2 bbb
3 ccc
4 ddd

若是想要每一個文件的行號都獨立計算。可使用下面這種方式進行判斷:遇到文件尾部,顯式關閉文件。bash

$ 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輸出。scala

非空行行號並刪除空白行

由於要刪除某些行不輸出,因此不能使用-p選項,它會將全部行都輸出,除非使用s///來刪除整行。能夠考慮使用-n選項。code

$ perl -ne 'print ++$x." $_" if /\S/' file.log

輸出匹配行的行號

例如輸出文件中匹配"nologin"單詞的行號,其它行照常輸出。htm

$ 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 ~~@tmp = <>' 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
相關文章
相關標籤/搜索