Perl一行式脚本使用参考
2024-04-17 17:41:55 阿炯

本文总结自骏马金龙的博客空间,Perl一行式脚本可直接在Shell接口中执行,接替Shell脚本三剑客所完成的任务。另外本站亦有此类文章的收录,可做补充:
Perl命令行应用介绍

Perl单行应用详解

Perl one line command 中文简介

章节:
一、相关选项、特殊变量与部分函数参考
二、处理空白符号
三、字段处理和计算
四、文本编解码、替换
五、选择行输出、删除、追加、插入


一、相关选项、特殊变量与部分函数参考

一行式选项

perl一行式语法:
perl [-0aFlimMnps] -[e|E] 'EXPRESSION' ARGUMENTS

其中"-e"或"-E"选项用于指定待运行的表达式,它们之间的不同点在于"-E"会自动启用高版本(v5.10+)功能特性,例如可以直接使用say()函数而无需先use 5.010;。可以指定多个-e/-E,但使用多个的时候,注意每个表达式后面的';'结尾符号,否则语法报错。

-n和-p
这两个选项都表示按照隐含的逻辑直接处理表达式后面的参数代表的文件。如果perl -e命令行中没有这两个选项,则只能自己在-e表达式中编写读取文件、处理数据、输出/删除的代码逻辑。

-n选项使得perl单行命令以类似于如下代码的方式运行:
LINE:
while(<>){
    ...-e expression CODE HERE...
}

由于while中使用的是<>,所以它会从@ARGV中读取参数文件进行处理。

perl -n就像sed -n一样,表示禁止默认的输出。如果想要强制输出,只能在-e表达式中自行指定输出操作,例如print/say/printf。

-p选项使得perl单行命令以类似于如下代码的方式运行:
LINE:
while(<>){
    ...-e expression CODE HERE...
}continue{
    print or die "-p destination: $!\n";
}

perl -p用于强制输出$_,它会覆盖-n选项。

必须要注意的是-n和-p都采用<>来读取文件,而它从文件中读取每一行时会保留每一行的尾随换行符"\n"。

何时使用"-p",何时使用"-n"
-p和-n的逻辑虽然很简单,也如sed的-n和p命令类似,但对于初学perl一行式程序的人来说,仍然很容易迷惑,因为Perl是一门语言,perl一行式也一样可以写成一门简单的语言,这意味着几乎总是有多种一行式的方式实现一个需求。

例如,-p可以被-n + print替代,-n、-p都可以被-e中的while(<>)替代。但既然perl一行式提供了-n和-p的选项,写perl一行式的时候自然应该追求精简化,让-e表达式的代码逻辑更简单。

个人的总结:
1.只要不操作文件,就不需要-n和-p
2.当某些行不需要输出,或者需要被删除的时候,也就是说不需要输出所有行时,不应该使用-p,因为它默认会输出所有行
2.1.换句话说,如果需要输出所有行,就可以考虑使用-p
2.2.使用-p的时候,在-e表达式中只需操作$_,例如对$_的赋值、s替换,此时不需要额外的print,但有些操作是可以隐藏$_的,最常见的就是s替换命令
3.不使用-p的时候,几乎总是可以使用-n,这时需要在-e表达式中手动print
4.如果处理文件的需求实在复杂,那么不要-n和-p,自己在-e中写文件读取的逻辑。但这种情况很少,真的出现这种情况,一般写成Perl脚本更好

-l选项
-l[OCTNUM]

选项开启自动行尾处理功能,它有两个过程:
1.和-n或-p一起使用的时候,将自动对输入行执行chomp剔除输入行终止符,然后在输出的时候自动追加尾随输出分隔符。
2.将print的输出行分隔符变量$\设置为OCTNUM的八进制数值,OCTNUM的ASCII字符将追加在输出的每一行行尾。如果省略了OCTNUM,则将$\设置为输入行分隔符变量$/的值,通常是换行符。

需要注意的是,省略OCTNUM的时候,也就是只有-l的时候,会在处理这个选项的那一刻就完成$\ = $/的赋值,所以对于-ln0e EXPRESSION将进行两段赋值:
# 处理-l的时候
$\ = $/;
# 处理-0的时候
$/ = \0;

这使得输出行分隔符取输入行分隔符的值,并在之后修改输入行分隔符。

注意上面的-0选项不能直接放在-l选项后(也就是-l0ne),这会产生歧义,认为0是-l选项的参数值,而不是-0选项。

一般来说,"-l"选项是用来为print函数追加换行符的,所以"-l"经常结合-n选项一起使用。例如:
$ perl -lne 'print' file.log

-0选项

设置输入行分隔符$/:
-0[octal/hexadecimal]

-0使得perl读取行时,以\0作为行输入分隔符,也就是对行输入分隔符变量$/进行赋值:$/ = "\0";

如果没有给定任何选项参数,则表示设置为null,即等价于$/ = undef,这表示一次性从文件头部一直读取到文件尾部当作一行。

如果给定了OCTNUM,即\0OCTNUM,则将八进制数值OCTNUM对应的ACSII作为行输入分隔符。

特别地,设置-00表示将$/设置为空"",即$/ = "",这表示按段落读取到$_且压缩连续空行。

-a和-F选项

-a选项打开自动分割模式,只能在-n或-p模式下使用。

-a使得-n或-p的while循环中首先对行进行一次隐含的split分割操作,并将分割后的结果放进数组@F中,使其可以成为一个个的字段,并通过$F[n]的方式调用各字段,其中n为字段的索引号,同样从0开始计算。

分割后的元素全都收集到一个数组@F中,所以第一个字段的内容是$F[0],最后一个字段是$F[-1]或$F[$#F]。

如果想取多个字段,可以对数组@F进行切片,例如第3个字段和第第5个字段@F[2,4],第3个字段到倒数第二个字段是@F[2..$#F-1]或@F[1..~~@F-2]。不要忘记,$_这时表示整行,类似于awk里的$0,而Perl中的$0表示的是程序名称(对于perl一行式,$0的值为-e或-E)。

通过-a选项,可以使perl单行程序可以以类似于awk的方式运行,只不过awk的第一个字段是从$1开始的,而-a的第一个字段是从$F[0]开始的。

-a划分字段的分隔符由-F选项指定。如果没有指定-F,则默认以空白符号进行分割(连续空格被认为是单空格)。

可以使用-Fpattern指定字段分隔符,也就是作为split的第一个参数。pattern可以被双斜线//、单引号''或双引号""包围,如果没有给定符号,则默认使用单引号。
-F:、-F/:/、-F":"、-F':'

关于-F的pattern,普通的字符直接包围即可,但想要指定空白符号作为分隔符,必须注意官方手册中的一句话:You can't use literal whitespace or NUL characters in the pattern,也就是说不允许直接指定空白符号作为分隔符。

实际上-Fpattern可以替换为split /pattern/;。所以想要指定空白符号作为分隔符,别用-F,而是直接使用split函数:

$ perl -alne 'split / /;print $F[1]' file.log

此外,空白符号可以使用\s来替代:
$ perl -F'\s+' -alne 'print $F[-1]' file.log

-i选项
-i[extension]

这个选项和sed的-i选项功能一样,都是为了提供副本或原地修改。

如果不指定extension,则perl打开一个临时文件的文件句柄作为print的输出对象,处理完成后将此临时文件重命名为原始文件名。所以,原始文件会被删除,且原始文件的写权限不影响"-i"选项的重命名,只有文件所在目录的写权限才能限制"-i"创建新文件。

如果指定了extension,则表示拷贝原文件为一个新的文件名,然后原始的文件名作为print的输出对象。例如-i'.bak'表示拷贝原始文件并命名为"FILENAME.bak",然后原始的文件名FILENAME是perl的输出目标。也就是说,操作完成后,FILENAME是被修改的,FILENAME.bak是未做修改的原始内容。

如果extension中没有包含*,则字符作为原文件名的后缀,例如-i".bak"。如果extension中包含了*,则每个*都代表原始文件名,这使得可以给文件加前缀、后缀等。例如-i"file-*-1-*.bak"将命名为file-FILENAME-1-FILENAME.bak。

-m和-M选项

这两个选项用来导入模块。
-mmodule、-Mmodule、-M'module ...'、-[mM]module=arg[,arg]...

-m导入模块时,相当于执行了use module();,这表示在程序中必须使用完整的名称来引用模块中的属性。例如-m'List::Util'时,在-e表达式中要使用其max函数,需要指定为List::Util::max @arr。

-M导入模块时,相当于执行了use module;,这时要使用模块中的属性仍然需要些完整的名称。但-M还支持额外的参数。例如-M'List::Util qw(max sum)'表示导入List::Util模块中的max和sum函数,这时在-e表达式中可以直接使用这两个函数名,而无需写完整的名称List::Util::max @arr。

对于-m和-M,还有另一种"="的写法,这种写法使得-m和-M没有任何的区别。例如:
-m'List::Util=sum,max'和-M'List::Util=sum,max'是等价的,它们都表示导入这List::Util模块中的sum和max函数,使得使用这两个函数的时候无需再写完整名称。

-s选项

perl的-s选项允许解析--之后的-xxx=yyy格式的开关选项。

$ perl -e 'xxxxx' -- -name=abc -age=23 a.txt b.txt

从--之后的参数,它们本该被收集到ARGV中,但如果开启了-s选项,这些参数部分如果是-xxx=yyy格式的,则被解析成perl的变量$xxx并为其赋值yyy,而那些不是-xxx=yyy格式的参数则被收集到ARGV数组中。

例如:
$ perl -se '
    print "ARGV: @ARGV\n";
    print "NAME & AGE: $name,$age\n";
    ' -- -name="FreeOA" -age=23 abc def
ARGV: abc def
NAME & AGE: FreeOA,23

上面传递的参数是-name="FreeOA" -age=23 abc def,因为开启了-s选项,所以解析了两个perl变量$name=FreeOA $age=23,另外两个abc def则被收集到ARGV数组中。

特殊变量

$\
表示print函数的输出行分隔符。默认为undef,所以print默认输出时两端数据总是连在一起的。可以指定为换行符"\n",这样print输出每段数据都会自带换行符。
$ perl -e '$\ = "\n";print "FreeOA"'
FreeOA

但必须注意,对于perl一行式程序的-p选项,它通过<>读取数据时会保留每一行的尾随换行符(除非在-e表达式中使用了chomp/chop),这时不应该额外设置$\ = "\n",否则每行后面都会多输出一空行。

$/
表示读取文件时的输入行分隔符,默认为换行符"\n"。在读取文件的时候,通过该特殊变量可以控制如何分行。可以设置为多个字符。

如果设置为undef,表示一次性从文件头一直读取到文件尾当作一行。如果设置为空""或"\n\n",表示按段落读取。不同的是:
1.设置为空""时,表示压缩连续空行为单个空行,压缩后的单空行属于当前段落
2.设置为"\n\n"则盲目地认为下一行一直属于当前段落,即使是空行,也即是不会压缩连续空行

$.
$.表示当前处理的行的行号。

实际上,它表示的是当前正在被打开的文件句柄的行号计数器。只要文件句柄不显式关闭,行号计数器就不会重置(open的隐式关闭以及reopen都不会重置)。

<>读取ARGV文件时从不关闭文件句柄,所以在一行式perl程序中使用了-n/-p时,多个参数文件的行号是连续下去而不会重置的。如果确实想要重置<>所读取的每个文件的行号,可以通过eof函数来判断,在到了文件底部的时候就关闭当前处理的文件。

while(<>){
    print "$. $_\n";
} continue {
    close(ARGV) if eof   #注意,不是eof()
}

例如:
$ perl -e 'while(<>){print "$. $_"} continue {close(ARGV) if eof}' t1.txt t2.txt

eof和eof()是不同的,前者判断每个文件的文件尾部,后者则判断最后一个文件的尾部(也就是无内容可读了)。所以下面的表示在最后一个文件的前面插入一行分割线。两者用法的差别在其函数手册页中有说明。

$ perl -e 'while(<>){print "-" x 30,"\n" if eof();print "$. $_"}' t1.txt t2.txt

@F
是"-a"选项将行自动分割后,各字段所保存的数组容器。具体参见-a选项。

$,和$"
$,变量指定print/say输出无双引号包围的列表/数组时的元素分隔符,默认值为undef,也就是元素之间紧紧相连。

$"变量指定print/say输出双引号包围时的列表/数组时的元素分隔符,默认值为空格。

$ perl -le '@arr=qw(Shell Perl Python);print @arr'
>ShellPerlPython

$ perl -le '@arr = qw(Shell Perl PHP);print "@arr"'
>Shell Perl PHP

指定特殊变量$,或$"的值:
$ perl -le '@arr=qw(Shell Perl Python);$,=" ";print @arr'
>Shell Perl Python

$ perl -le '@arr=qw(Shell Perl Python);$"=":";print "@arr"'
>Shell:Perl:Python


二、处理空白符号

假如文件file.log内容如下:
root   x 0     0 root   /root     /bin/bash
daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
bin    x 2     2 bin    /bin      /usr/sbin/nologin
sys    x 3     3 sys    /dev      /usr/sbin/nologin
sync   x 4 65534 sync   /bin      /bin/sync

每行后加一空行
$ perl -pe '$\ = "\n"' file.log

结果:
root   x 0     0 root   /root     /bin/bash

daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin

bin    x 2     2 bin    /bin      /usr/sbin/nologin

sys    x 3     3 sys    /dev      /usr/sbin/nologin

sync   x 4 65534 sync   /bin      /bin/sync

这里出现了选项 -p 和 -e,出现了特殊变量$\,附带的,稍后还会解释另一个选项 -n。

perl的"-e"选项表示后面接perl的一行式表达式,就像sed的-e选项一样。这是一行式perl程序最常见的一个选项。perl还有一个"-E"选项,和"-e"一样都用来指定一行式表达式,但"-E"可以使用一些高版本的功能。

perl的"-p"选项表示print操作,即对每一读入的行在经过表达式操作后都默认输出,和sed的p命令是一样的。实际上perl中的"-p"选项等价于下面的逻辑,理解了这个逻辑,对理解sed的逻辑会很有帮助。

while(<>){
    ... -e 指定的表达式代码在这里 ...
} continue {
    print or die "-p failed: $!\n";
}

Perl中的continue和其它语言的continue有点不一样,其中的continue表示每轮循环的主体执行完之后,执行另一段代码。也就是说,每一行内容经过"-e"指定的表达式处理后,都会被continue代码块中的print输出。

解释下-p选项的过程:while(<>)每次读取一行数据赋值给默认变量$_,然后经过-e的表达式进行处理,处理完后执行continue的print,这里print没有参数,所以表示输出默认变量$_的内容,也就是被处理后的行数据。

另一个选项 -n,表示处理文件但默认不输出处理后的行。如果想要输出,只能自己在-e表达式中指定输出操作(print/say/printf)。逻辑为:
while (<>) {
    ...  # -e expression here
}

也就是说,-n和-p两个选项会自动读取文件(如果都存在,则-p会覆盖-n),不需要在-e的表达式中自己写读取文件的逻辑。如果没有这两个选项,那么在-e中要自己写才能读参数文件:
$ perl -e 'while(<>){...}'

一般来说,选择使用-p还是-n的规则如下:
1.某些行不需要输出或者需要被删除的时候,不应该使用-p,因为它默认会输出所有行
1.1.换句话说,如果不需要输出所有行时,不使用-p,需要输出所有行,可以考虑使用-p
2.不使用-p的时候,几乎都可以使用-n

最后是关于特殊变量$\表示print的输出行分隔符(awk的ORS变量),它默认为undef,所以print输出的每段数据之间都是紧连在一起的。此处示例将$\指定为换行符。

由于<>读取数据时已经将文本中每一行的\n也读取了,所以加上$\已经有两个连续的\n,于是每行后面都会多一空行。实际上没有必要为每一读入的行都设置$\,可以将它设置在BEGIN块中:
$ perl -pe 'BEGIN{$\ = "\n"}' file.log

对于本示例"每行之后加上空行"有多种解决方式。例如:
$ perl -pe '$_ .= "\n"' file.log
$ perl -nE 'say "$_\n"' file.log
$ perl -pe 's/$/\n/' file.log
......

每行后加空行,但空行除外
$ perl -pe '$_ .= "\n" unless /^$/'

这里使用unless逻辑进行空行匹配,如果匹配空行,就不对当前行追加尾随换行符。unless测试等价于if的否定测试。

有些空行可能是包含了空白符号(空格、制表符等)的行,这些空白肉眼不可识别,但却占了字符空间,使得无法匹配^$,所以更好的匹配模式是:
$ perl -pe '$_ .= "\n" if /\S/'

\S表示任意单个非空白字符,\s表示任意单个空白字符。所以这里的逻辑是:只要行能匹配非空白字符,就追加尾随换行符。

每行后加N空行
想在每行后面加两空行、三空行、四空行、N空行该如何解决?
$ perl -pe '$_ .= "\n" x 3' file.log

Perl的字符串可以使用小写字母x表示重复N次,例如"a" x 3得到"aaa","ab" x 2得到"abab"。上面的示例表示为每行都追加3个换行符。

通过字符串重复操作,可以很轻松地输出等长的分割线:
$ perl -e '
    print "-" x 30,"\n";print "FreeOA\n";
    print "-" x 30,"\n";print "Net\n";'
------------------------------
FreeOA
------------------------------
Net

每行前加空行
最简单的方式是使用s替换操作。
$ perl -pe 's/^/\n/' file.log

移除所有空白行
$ perl -ne 'print unless /^$/' file.log

此处使用了"-n"选项,表示禁止默认的输出。print和匹配操作的对象都使用默认变量$_。等价于:
整个逻辑是:只要匹配了空行/^$/,就不输出。

这也有好几种方式可以实现,例如:
$ perl -ne 'print if /\S/' file.log

比较独特的一种实现方式是使用length函数:
$ perl -lne 'print if length' file.log

这里涉及选项"-l"和函数length(),且print和length都没有指定操作对象,所以使用默认变量$_,等价于:
$ perl -lne 'print $_ if length $_' file.log

length函数可以获取字符串的字符个数,注意是字符数不是字节数。

-l选项在结合-n或-p使用的时,会自动对读入的行移除尾随换行符,然后在输出的时候自动追加尾随输出分隔符。

这里的逻辑是:如果是空行,那么在被-l移除换行符后length返回0,也就是布尔假,所以只有不是空行的行才会被输出。

压缩连续空行:按段落读取
先准备一段测试数据paragraph.log:
first paragraph:
    first line in 1st paragraph
    second line in 1st paragraph
    third line in 1st paragraph


second paragraph:
    first line in 2nd paragraph
    second line in 2nd paragraph
    third line in 2nd paragraph


third paragraph:
    first line in 3rd paragraph
    second line in 3rd paragraph
    third line in 3rd paragraph

sed/awk中想要压缩连续空行,总要多读入几行进行连续空行的判断。例如:
$ sed -nr '$!N;/^\n$/!P;D' paragraph.log

但在perl一行式中,这会变得无比的简单:
$ perl -00pe '' paragraph.log

这里两个关注点:-00和-e ''。

-e ''的表达式部分为空,表示什么也不做;什么也不做的时候,也可以写成-e0。
$ perl -00pe0 paragraph.log

-0OCTNUM表示设置输入行分隔符$/。

如果省略8进制值OCTNUM,则-0表示设置$/为undef,即$/ = undef,也就是一次性从文件头读到文件尾当作一行赋值给$_。

这里指定了8进制的值为0,对应于ASCII的空字符串,即等价于$/ = "",它表示按段落读取(slurp读取模式),并压缩连续的空行为单个空行。

什么是段落?中间隔了至少一空行的是上下两个段落,段落意味着可能包含了连续的多行。但是如果隔了连续的空行呢?设置$/ = ""会按段落读取,并压缩连续的空行为单空行,然后作为上面的段落的一部分。设置$/ = "\n\n"也表示按段落读取,但它不会压缩连续的空行。

如何知道是否是按段落读取?可用下面的示例进行测试:
$ perl -ne 'BEGIN{$/ = "";} print $_."xxxxx" if /2nd/' paragraph.log

会发现追加的几个字符"xxxxx"是单独附加在第二段落的尾部的,而不是能匹配"2nd"的每一行上。

压缩/扩展所有连续空行为N空行
在上面一节压缩连续空行的基础上,实现这个目的已经非常容易了:
$ perl -00pe '$_ .= "\n" x 3' paragraph.log

这表示将每个段落之间规范为4个连续的空行进行分隔。之所以是4空行而不是3,是因为压缩成单空行后,又追加了3空行。

压缩/扩展单词间的空格数量
要实现这样的功能,这个对于sed来说也非常的容易。这里给几个简单示例。

1.每行单词间的空给双倍化:每个空白都扩成2空格
$ perl -lpe 's/ /  /g' file.log

2.移除每行单词间的所有空白
$ perl -lpe 's/ //g' file.log

3.每行单词间连续空白压缩为单空格
$ perl -lpe 's/\s+/ /g' file.log

4.所有字符间插入一个空格
$ perl -lpe 's// /g' file.log

注意,上面插入空格时,也会在行首和行尾插入空格符号。

直接(原位)修改文件
perl的"-i"选项可以用来原地修改、拷贝副本。用法和sed的"-i"一致。

例如:
$ perl -i".bak" -lpe 's/$/\n/g' file.log

$ cat file.log
root   x 0     0 root   /root     /bin/bash

daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin

bin    x 2     2 bin    /bin      /usr/sbin/nologin

sys    x 3     3 sys    /dev      /usr/sbin/nologin

sync   x 4 65534 sync   /bin      /bin/sync

$ cat file.log.bak
root   x 0     0 root   /root     /bin/bash
daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
bin    x 2     2 bin    /bin      /usr/sbin/nologin
sys    x 3     3 sys    /dev      /usr/sbin/nologin
sync   x 4 65534 sync   /bin      /bin/sync


三、字段处理和计算

获取每行最后一个字段
$ perl -alne 'print $F[$#F]' file.log

这里涉及到了选项"-a"、数组@F。这里同时还会解释-F选项,它和-a常一起使用。

选项"-a"和awk的自动字段分割一样,会自动将每行数据划分为几个字段。划分字段的分隔符由-F选项指定。如果没有指定-F,则默认以空白符号进行分割(连续空格被认为是单空格)。

分割后的元素全都收集到一个数组@F中,所以第一个字段的内容是$F[0],最后一个字段是$F[-1]或$F[$#F]。

如果想取多个字段,可以对数组@F进行切片,例如第3个字段和第第5个字段@F[2,4],第3个字段到倒数第二个字段是@F[2..$#F-1]或@F[1..~~@F-2]。

获取范围字段
正如上面所解释的,如果想要获取第二个字段到倒数第二个字段:
$ perl -lane 'print "@F[1..~~@F-2]"' file.log
$ perl -lane 'print "@F[1..$#F-1]"' file.log

指定字段分隔符
之所以单独拿出来解释,是因为"-F"指定分隔符时,空白符号的特殊之处。对于普通字符,-F自然没有什么问题:
$ perl -F: -alne 'print $F[1]' /etc/passwd

但是想指定空白字符作为字段的分隔符时,"-F"选项将出现故障:
$ perl -F" " -alne 'print $F[1]' file.log

会发现空格分隔符根本没起作用,而是按照NUL作为分隔符对每个字符都分割了。该问题在-F选项的官方手册中已经注明了:
You can't use literal whitespace or NUL characters in the pattern

如果想要指定空白符号作为字段分隔符,可以考虑其它方式。例如使用\s的正则模式;或者直接不使用-F,而是直接在-e表达式中使用split函数进行行的分割。
$ perl -F'\s+' -anle 'print $F[1]' file.log
$ perl -alne 'split / /;print $F[1]' file.log

计算所有行数值的总和
假如文件内容为:
1 3 5 9
2 3 1
10 2 3 6

想要总计所有这些数值之和,可以使用如下方式:
$ perl -M'List::Util=sum' -alne '$num += sum @F;END{print $num}' file.log

或者将所有行读取到数组中,最后对数组加总:
$ perl -M'List::Util=sum' -alne 'push @S,@F;END{print sum @S}' file.log

这种方式对于大文件肯定是不如前一种方式友好的,因为它会将所有行内容都存储起来,而前一种方式为所有行都只存储一个结果$num,占用的内存要低的多的多。

打乱字段的顺序
$ perl -M'List::Util=shuffle' -alne 'print "@{[shuffle @F]}"' file.log

在List::Util模块中有一个函数shuffle,它会按照随机的顺序打乱一个列表(了解即可,这不是本文的重点)。例如:
$ perl -M'List::Util=shuffle' -le 'print "@{[shuffle 1..10]}"'
8 1 3 7 10 5 2 4 6 9

$ perl -M'List::Util=shuffle' -le 'print "@{[shuffle 1..10]}"'
1 2 7 10 8 4 3 5 6 9

$ perl -M'List::Util=shuffle' -le 'print "@{[shuffle 1..10]}"'
2 5 7 1 4 6 3 8 9 10

这里介绍的重点是"@{[ shuffle @F ]}",它是让一个操作可以插入到双引号中的方法。

虽然目的是插入到双引号中,但它的最终目标是为了让数组元素输出时以空格分隔。所以这种技巧不是唯一的方法。

指定字段输出分隔符(无引号包围)
上一小节通过@{[shuffle @F]}的方式可以将打乱数组的操作插入到双引号中。下面是其它方法。

1.指定数组的字段输出分隔符$,
$ perl -M'List::Util=shuffle' -alne 'BEGIN{$,=" "}print shuffle @F' file.log

默认情况下,print/say输出列表的时候,如果数组/列表不插入到双引号中,各元素之间紧连在一起被输出:
$ perl -le '@arr=qw(Shell Perl Python);print @arr'
ShellPerlPython

特殊变量'$,'指定的就是print/say输出数组且不插入双引号时的元素分隔符,其默认值为undef。

例如指定为空格:
$ perl -le '@arr=qw(Shell Perl Python);$,=" ";print @arr'
Shell Perl Python

2.将列表join成字符串,join的连接符指定为空格即可
$ perl -M'List::Util=shuffle' -anle 'print join " ",shuffle @F' file.log

指定字段分隔符(引号包围)
变量$,控制无双引号包围的数组/列表在print/say输出时的元素分隔符。其实双引号包围的数组/列表被print/say输出时也可以指定元素分隔符。

控制输出双引号包围的列表的元素分隔符的特殊变量是$",默认值为空格。

# 默认空格分隔双引号中的元素
$ perl -le '@arr = qw(Shell Perl PHP Python);print "@arr"'
Shell Perl PHP Python

# $"改变双引号中的元素分隔符
$ perl -le '@arr = qw(Shell Perl PHP Python);$" = ":";print "@arr"'
Shell:Perl:PHP:Python

所有行中最小数值
假如文件内容为:
1 3 0 9
2 -3 1
10 -2 -3 6

所有行的最小值为-3,如何取得?

最简单的方式是将所有行都读入并保存到数组中,然后使用List::Util模块的min函数取得。
$ perl -M'List::Util=min' -anle 'push @nums,@F;END{print min @nums}' num.log

但这对于大文件来说内存占用率会很高。比较好的方式是从每行中取出最小值,保留到数组中,最后从这个数组中取出最小值(稍后继续解释更好的方式)。
$ perl -M'List::Util=min' -anle 'push @nums,min @F;END{print min @nums}' num.log

如果文件的行数量非常大,这也会在内存中保留很多数值,也不是最佳方式。

更好的方式是从每行中取出最小值保存下来,然后和后面的行结合在一起取最小值。这样的方式使得整个处理过程都只占用一行内存空间。
$ perl -M'List::Util=min' -anle '$min = min ($min // (),@F);END{print $min};' num.log

这里的关键是min $min//(),@F:
首先,$min//()表示如果$min未定义,则返回空列表(),否则返回$min。如果这里不进行$min是否已定义的判断,那么第一次使用$min的时候,它被当作空。所以如果文件中没有负数,下面的操作将会因此而返回空。

$ perl -M'List::Util=min' -anle '$min = min ($min,@F);END{print $min};' num.log

再者,上面$min//()在$min未定义的时候返回的是空列表(),不能编写为返回0或空,否则就多出了一个要比较的值。最后,min函数操作的是一个列表,而Perl会将多个列表压扁形成一个大列表,所以$min//(),@F被压成了一个被min函数操作的列表,而()表示空列表,这使得第一次使用$min的时候不会影响要比较的值。

将所有数值取绝对值
假如文件内容为:
1 3 0 9
2 -3 1
10 -2 -3 6

要返回所有数值的绝对值,可以借用默认函数abs来操作。简单的逻辑可以遍历@F,并应用abs函数,最后追加回列表被输出:
$ perl -lane '@line=();for(@F){push @line,abs};print "@line";' num.log

在Perl中,对于列表中每个元素都要检查、操作的情形,可以使用map函数:
$ perl -lane 'print "@{[map {abs} @F]}"' num.log

其中map {op} LIST表示对LIST中的每个元素都执行op操作,操作后的值构成一个新列表。@{[ ]}的格式在前面已经出现多次了,不再解释。

统计所有匹配字段数量
$ perl -lane '/pattern/ && ++$num for @F;END{print $num || 0}' file.log

这种统计方式是安全的。先划分字段,然后匹配每个字段,只要匹配到就将计数器变量加1。最后输出计数器的值。但可能匹配不到任何东西,所以必须给计数器变量设置一个默认值,也就是$num || 0。

另一种改写方式是:
$ perl -anle '$t += /pattern/ for @F;END{print $t}' file.log

这里采用的赋值方式$t += /pattern/,因为/pattern/返回的是匹配成功的数量,不匹配成功则会返回0,所以无需像前面一样设置计数器变量的默认值。

如果使用grep来改写,则一行式命令如下:
$ perl -alne '$num += grep /pattern/,@F;END{print $num}' file.log

grep返回一个新的表示能匹配的元素列表,但是在标量上下文中列表返回的是元素个数,所以可直接加总计算。

生成10个5-15之间的随机数
$ perl -le 'print "@{[ map int(rand(10)+5),1..10 ]}"'
10 9 9 11 13 10 14 12 13 6

rand(10)表示生成0-9之间的随机浮点数,int(rand(10))表示生成0-9之间的随机整数,加上5表示5-14的随机整数,整个过程执行10次,所以生成了10个随机整数。

生成字母表
$ perl -le 'print a..z'
abcdefghijklmnopqrstuvwxyz

更准确的写法是加上双引号:
$ perl -le 'print "a".."z"'
abcdefghijklmnopqrstuvwxyz

逗号分隔字母表:
$ perl -le 'print join ",","a".."z"'
a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z

输出"a"到"zz":
$ perl -lE '$, = ",";say "a".."zz"'

..符号会按照字符序列进行递增,"z"递增后得到"aa",再递增得到"ab","az"递增得到"ba",依次类推。所以这里会返回大量字符(共702个字符):

生成8字符随机密码
$ perl -le 'print map { ("a".."z")[rand 26] } 1..8'

这里("a".."z")[rand 26]取26个字母中的一个随机字母,1..8表示生成8个字符。

如果想要包含大小写字母、数字,可以:
$ perl -le 'print map { ("a".."z","A".."Z",0..9)[rand 62] } 1..8'

生成8字符随机密码,包含特殊字符
如果想要让生成的随机密码中包含大小写字母、数字、各种标点符号,可以通过ascii码表来指定范围,然后使用chr函数来转换ascii码为字符。根据ascii,从33开始到126结束是大小写字母、数字、各种标点符号部分。所以:
$ perl -le 'print map { chr int(rand(94)) + 33 } 1..8'

int(rand(94) + 33)表示生成33-126(包括126)之间的随机整数,chr函数可以将数值转换为对应的字符。

生成10个随机密码
$ perl -le 'for (0..9){print map { chr int(rand(94)) + 33 } 1..13}'


四、文本编解码、替换

文本大小写转换
全部字符转换成大写或小写,有几种方式:
# 转大写
$ perl -nle 'print uc' file.log
$ perl -ple '$_ = uc' file.log
$ perl -nle 'print "\U$_"' file.log

# 转小写
$ perl -nle 'print lc' file.log
$ perl -ple '$_ = lc' file.log
$ perl -nle 'print "\L$_"' file.log

每行首字母大小写转换:
$ perl -nle 'print lcfirst' file.log
$ perl -lpe '$_ = ucfirst' file.log
$ perl -lne 'print \u\L$_' file.log

单词首字母大写,其它小写:
$ perl -ple 's/(\w+)/\u$1/g' file.log

修剪前缀、后缀空白
去掉前缀空白的方式:
$ perl -ple 's/^\s+//' file.log

去掉后缀空白的方式:
$ perl -lpe 's/\s+$//' file.log

同时去掉前缀和后缀空白:
$ perl -lpe 's/^\s+|\s+$//' file.log

反序输出所有段落
$ perl -00 -e 'print reverse <>' file.log

前面的文章压缩连续的空行解释过,-00是按段落读取且压缩连续的空行。

reverse <>中reverse的操作对象期待的是一个列表,所以<>会一次性读取整个文件且按照段落读取,每个段落是列表中的一个元素。最后reverse函数反序这个列表,然后被print输出。

反序输出所有行
$ perl -e 'print reverse <ARGV>' file.log
sync   x 4 65534 sync   /bin      /bin/sync
sys    x 3     3 sys    /dev      /usr/sbin/nologin
bin    x 2     2 bin    /bin      /usr/sbin/nologin
daemon x 1     1 daemon /usr/sbin /usr/sbin/nologin
root   x 0     0 root   /root     /bin/bash

这里reverse <ARGV>表示一次性读取file.log的所有行并进行反转。

也可以使用下面这种方式,但如果文件结尾不正确(缺少eof),可能会卡住:
$ perl -e 'print reverse <>' file.log

ROT13字符映射
Perl中可使用tr///或y///进行字符一一映射的替换。它们和unix下的tr命令作用类似。
$ perl -le '$string="hello";$string =~ y/a-zA-Z/N-Za-mA-Mn-z/;print $string'
URYYb

BASE64编码、解码
MIME::Base64模块提供了base64编码、解码的方法。

编码:
$ perl -MMIME::Base64 -e 'print encode_base64("coding")'
Y29kaW5n

解码:
$ perl -MMIME::Base64 -le 'print decode_base64("Y29kaW5n")'
coding

编码文件:
$ perl -MMIME::Base64 -0777 -ne 'print encode_base64($_)' file.log

解码文件:
$ perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' file

URL转义
使用URI::Escape模块即可进行URL转义,该模块需要额外安装。
$ perl -MURI::Escape -le 'print uri_escape("http://example.com")'
http%3A%2F%2Fexample.com

反转义:
$ perl -MURI::Escape -le 'print uri_unescape("http%3A%2F%2Fexample.com")'
http://example.com

HTML编码、解码
先安装额外HTML格式的编解码模块cpan HTML::Entities。
$ perl -MHTML::Entities -le 'print encode_entities("<html>")'
$ perl -MHTML::Entities -le 'print decode_entities("&lt;html&gt;")'


五、选择行输出、删除、追加、插入

对于Perl的一行式perl程序来说,选择要输出的、要删除的、要插入/追加的行是非常容易的事情,因为print/say决定行是否输出/插入/追加/删除。虽然简单,但对于广泛应用在sed的示例还是可以拿到这里来讨论一番。因为输出/删除/插入/追加行都是通过print/say在不同条件下的操作,所以本文只会介绍输出操作,删除/插入/追加其实都是同样的原理。

输出第1行
$ perl -lne 'print;exit' file.log

输出第13行
$ perl -ne 'print if $. == 13' file.log

输出前10行
$ perl -ne 'print if $.<=10' file.log
$ perl -ne 'print if 1..10' file.log
$ perl -ne '$. <= 10 && print' file.log
$ perl -ne 'print; exit if $. == 10' file.log

输出最后一行
$ perl -ne '$last=$_;END{print $last}' file.log

或者通过文件结尾eof来判断:
$ perl -ne 'print if eof' file.log

这里的eof函数的作用是:如果下一行读取到了文件尾部eof,就返回1。

输出倒数10行
这个实现起来可能稍显复杂,但逻辑很简单:向一个数组中添加10行元素,如果数组元素个数超过了10,则剔除数组的第一个元素。
$ perl -ne '
    push @lines,$_;
    if(@lines>10){shift @lines;}
    END{print @lines}' /etc/passwd

这里是shift一个元素来保证"窗口"的稳定性:最多只有10个元素。另一种稳妥的方式是直接切片,从数组中取最后10个元素:
$ perl -ne '
    push @lines,$_;
    @lines = @lines[@lines-10..$#lines] if @lines>10;
    END{print @lines}
    ' /etc/passwd

输出倒数第11行到倒数第2行
有了前一个示例作为基础,这个需求很容易实现。保留一个11行元素的数组,最后输出前10个元素即可。
$ perl -ne '
    push @a,$_;
    shift @a if @a>11;
    END{print @a[0..$#a-1]}
    ' /etc/passwd

输出文件的第偶数行
这个很简单,只需判断行号的奇偶性即可。
$ perl -ne 'print if $. % 2 == 0' file.log
$ perl -ne 'print unless $. % 2' file.log

输出能匹配的行
$ perl -ne 'print if /regexp/' file.log

输出两个匹配之间的行
$ perl -ne 'print if /regexp1/../regexp2/' file.log

输出匹配行的前一行
只需将每行保留到变量中,如果当前行匹配了,则输出上一行保存的值。
$ perl -ne '/regexp/ && $last && print $last;$last = $_' file.log

如果想要输出匹配的前M行,只需把这些数量的行保存到数组中,并不断地shift剔除就可以。

输出匹配行的后一行
$ perl -ne '$p && print; $p = /regexp/' file.log

Perl中正则表达式的匹配操作返回的是成功与否的布尔真假,所以$p = /regexp/表示如果匹配了,则$p的值为真,否则为假。

如果$p为真,则下一行将被输出,且继续对输出行进行匹配,如果输出行仍然能匹配,则继续输出下一行。

上面的过程可以改写成逻辑更为清晰的一行式:
$ perl -ne 'if($p){print;$p=0}++$p if /regexp/' file.log

上面的$p是一个状态标记变量,如果匹配成功,就标记为真值,并在输出的时候重置状态变量。

还可以采用另一种处理逻辑:自己编写从<>读取行的while循环,如果匹配了就继续读入下一行。因为读入的下一行可能继续匹配,所以在while循环中使用redo逻辑回到while循环的开头。
$ perl -se 'while(<>){if(/$reg/){if(eof){ exit; } print $_ = <>;} redo if /$reg/;}' -- -reg="REGEXP" file.log

输出匹配行及其后5行
上面采用状态标记变量$p,这个状态标记变量可以更深入地使用。如果匹配了,则$p设置为5,然后输出后面的行时对$p自减。
$ perl -ne 'if($p){print;$p--} if(/regexp/){$p = 5;print};' file.log

连续行去重
$ perl -ne 'next if "$line" eq "$_";print $line = $_;' file.log