强大的Perl-one-liner之——Perl 的特殊变量

在这个附录中,我总结了 Perl 使用最频繁的特殊变量,也就是系统预置的变量。比如$_, $., S/, $\, $1, $2, $,, $F, @ARGV 等等。

A.1 变量 $_

这个$_变量,叫做Perl的缺省变量(default variable),这个变量可以说是Perl里面用的最频繁的变量。这个变量的发音是“美元下划线(也就是dollar underline)”。

当你使用 -p-n 这两个参数的时候。输入文件的当前行的内容就存储在这个变量当中。同时如果你用了操作符和函数,但是没有写输入的变量,那么他们默认的处理对象就是 $_ 这个变量的内容。这里有一个例子。

perl -le '$_ = "foo"; print'

在这个例子中,我将字符串 "foo" 放在了 $_ 这个变量当中,然后使用了print这个函数。不对这个函数设置参数的时候,print就是打印出$_这个变量的内容,也就是字符串"foo"。

同样的,$_ 也被用在正则表达式当中。请先看下面的一个例子:

perl -ne '/foo/ && print'

这个一行命令将会打印出匹配到"foo"字符串的输入行。这个正则表达式/foo/会隐式地作用于$_这个变量,也就是当前的行。
你可以写出完整的代码行,但是那样的话相比而言需要敲击很多次的键盘。

perl -ne 'if ($_ =~ /foo/) { print $_ }'

如果Perl解释器匹配到了字符串,然后就会打印出来。你也可以直接把输入文件中所有行第一次出现的字符串for给替换掉。只需要在命令中简单地调用s/foo/bar/

perl -pe 's/foo/bar/'

有趣的是,Perl是从sed这个Linux命令中借鉴$_这个变量的。还记得sed 有一个"pattern space"吗?$_这个变量同样也可以叫做Perl的"pattern space"。如果你用sed来实现上述Perl-one-line的功能,那将会是sed 's/foo/bar/'因为sed会把每一行放在"pattern space"当中,然后隐式地调用s命令。Perl从sed当中借鉴了很多的概念和命令。

$_变量配合 -n 参数使用Perl one line

当使用 -n 参数时,Perl 将会把你后面的代码用如下循环包裹进你的程序。

while ( <> ){
        # 你的代码将会从这里开始执行 (当然你需要用 -e 参数来声明)
}

代码中的 while ( <> ) 循环 将会从标准输入或者文件中一行一行地读入文本并进行处理。每一行的文本将会被放在$_ 这个变量当中,然后你可以通过修改$_变量修改这一行,并打印出$_变量的内容。比如你可以反转字符串并将其打印出来:

perl -lne 'print scalar reverse'

译者注:
如果对上面的语法感觉很疑惑,可以到这个网站查询其用法:https://perlmaven.com/reverse
通过这个例子我们可以看出,Perl有一个独特的语境设置(Perl有list和scalar两者语境),同一个内容在不同的语境中有不同的效果。我得知劳拉其实是一个语言学家,他加上了这些语言学的元素。有人批判说这个是文字游戏,其实你结合生活一想,现实中又何尝不是这样:一个会说话的人和一个不会说话的人,对同一件事情说不来的时候,给人的感觉是很不一样的,对他人的影响能力也是不一样的。

在上面的例子中,因为我用了-n参数,实际上这个程序是这样的:

while (<>) {
      print scalar reverse;
}

这个程序也等于:

while (<>) {
        print scalar reverse $_
}

因为Perl的函数如果没有参数的话,会自动作用于$_这个变量,所以这会造成 reversereverse $_ 在功能上是一样的。你需要使用scalar函数使reverse函数发挥作用的时候在一个scalar的环境里面。否则如果不加scalar的话,那么它就在list的环境里面(因为print函数会强制变成list的环境),这样就发挥不了reverse函数的作用,不信的话你可以试一试。

$_变量配合 -p 参数使用Perl one line

当你使用 -p 参数的时候,Perl 会将如下的一个循环包裹你的代码。

while (<>) {
        # 你的代码
} continue {
        print or die "-p failed: $!\n";
}
译者注:
上面的这个continue有些奇怪!
经过不完全地测试,上述代码等同于:
while (<>) {
        # 你的代码
        print or die "-p failed: $!\n";
}

使用 -p 参数的结果几乎与-n一模一样,只是使用-p参数后,每次循环后$_的内容都会被打印出来。
如果你使用-p参数来反转每一行的内容的话,你可以这样做:

perl -pe '$_ = reverse $_'

这个语句就会变成:

while (<>) {
        $_ = reverse $_;
        print or die "-p failed: $!\n";
}

在这里,我修改了$_ 变量,使其等于reverse $_,这样就使每一行都反转了一下。
如果想了解更多关于这个参数的的用法,请看 2.1章节。

显式使用 $_ 变量

$_经常被显式地使用,这里有一些使用的例子。

perl -le '@vals = map { $_ * 2 } 1..10; print "@vals"'

运行这个指令后输出的结果是:2 4 6 8 10 12 14 16 18 20
在这里,我用了map这个函数, 去map 一个list 中的所有元素然后返回一个新的列表,新列表中的每一个元素是map 中的表达式作用后的结果。

译者注:这里有函数式编程的一些影子。

在这个例子中,list 是(1 .. 10) 表达式是 $_ * 2, 表达式的意思对list中的每个元素都乘以2。正如你看到的一样,我直接显式地运用了 $_。当map函数对list进行遍历的时候,每个元素都很方便地可以用$_变量来表示。
那么现在我们学会利用便捷的map函数来解决一些问题吧。比如对于文本的每一行都乘以2。

perl -lane 'print " @{[map { $_ * 2 } @F] } " ' 

这个one-liner 将表达式 $_ *2 作用于@F数组的每个元素。"@{[ . . .]} "这个看着很疯狂的写法 只是一种用来执行 引号中代码的一种写法。(4.4章节可以看它的解释)

另一个显式地利用 $_ 变量的一个函数式 grep,这个函数能够让你过滤一个list中的元素。这里有一个例子:

perl -le ' @vals = grep { $_ > 5} 1..10;print "@vals" '

这个语句的执行结果是“6 7 8 9 10”。正如你看到的,grep函数可以过滤掉1 2 3 4 5这些小于等于5的元素。
下面让我们来应用一下grep,比如找到并且打印出当前行中所有满足回文结构 (palindromes) 的元素。

perl -alne 'print "@{[grep { $_ eq reverse $_ } @F]}"'

在这里grep函数判定的条件是 $_ eq reverse $_,如果当前元素满足回文结构则被返回,否则就被过滤掉。例如,如果输入的是:

civic foo mom dad
bar baz 1234321 x

那么输出的结果就是:

civic mom dad
1234321 x

正如你所看到的,所有的这些输出元素都是回文结构。
你甚至可以通过在终端键入perldoc perlvar学习更多关于 $_ 这个变量的知识。

A.2 变量 $.

当Perl读取一个文件的时候, $. 这个变量总是会包含当前读取行的内容。例如在下面这个例子中,我们对文件file中的内容标注行号。

perl -lne 'print "$. $_" file

你也可以将当前行的内容变成原来行内容并在每一行的末尾加上行号:

perl -pe '$_ = "$. $_"' file

当出入两个文件的时候,变量$.不会被重置,因此如果想对两个文件同时标注行号,可以直接这样写:

perl -pe '$_ = "$. $_"' file1 file2

这样的话Perl会在读完file1这个文件之后继续标注file2这个文件,且行号是连续的。(如果file1包括了10行,那么Perl读取file2的第一行的时候,它标注的行号就是11)。
如果你想重新设定$.这个变量,你可以显式地关闭当前的文件句柄 ARGV;

perl -pe '$_ = "$. $_"; close ARGV if eof' file1 file2

ARGV是一个特殊的的文件句柄,它包含了当前打开的文件。通过呼叫 eof,Perl就会检查一下当前处理的行是不是文件的最后一行。如果是最后一行,那close函数就会关闭它,也就是把$.变量重置为0.

行的终止符号默认是"\n",当然你也可以手动地通过$/变量改变行的终止符。下一节我们就来讨论一下$/这个内置变量。

A.3 变量 $/

这个变量告诉Perl 行终止符是什么,也就是Perl认为怎么样才叫做一行。我们再来看那个标注行号的小语句:

perl -lne 'print "$. $_"' file

Perl 读写文件的内容知道读到换行符,然后将这些内容放入$_这个变量。然后使$.变量的值增加1,再进一步Perl会调用 print 函数打印出 "$. $_"的内容,也就是打印出行号和当前行的内容。
这里有另外的一个例子,如果你有一个文件的内容如下,你可以将$/变量设置为":",那么Perl就会一个数字一个数字地读取,每一行就是一个数字。

3:9:0:7:1:2:4:3:8:4:1:0:0:1:...

如果你将$/变量设置为 undef,Perl会将整个文件视为一行放入$_变量(这个叫做 slurping)。

perl -le '$/ = undef; open $f, "<", "file"; $contents = <$f>"

这个语句会一次性地将 file 这个文件的内容放到$contents这个变量中。你也可以将 $/变量设置为一个整型数字的reference。例如

$/ = \1024

在这个例子中,Perl一次读取1024字节的内容,将其放入$_变量。(这也叫做 record-by-record reading

译者注:经过译者自己的调试,下面的两个结果是不一样的。

# 
perl -lpe ' BEGIN{$/ = \1}' file
# 第一次赋值`$_`的时候行分割符还是"\n"
perl -lpe ' $/ = \2;' file

你也可以用-0参数去设置分隔符,例如 -0072 等于$/ = :

A.4 变量 $\

这个变量会在每次调用print函数后加上这个变量的内容。比如你可以在每次print后面加上"."这个内容。

perl -e '$\ = ". "; print "hello"; print "world"'

执行这个语句屏幕将会打印出:

hello. world.

当你需要再每个print 的内容后面加一些东西的时候,修饰这个变量特别有用。
请记住这个变量,例如你想在每次print 之后都加上"\n"分隔符,那么你可以在一开始就将$\变量赋值为"\n"。值得注意的是,在Perl的有些版本中有一个say函数,这个函数和print很像,但是它会在每次打印内容后就加上"\n"。

A.5 变量 $1, $2, $3 等等

这些变量一般用来捕获最终的匹配值,这些匹配值一般都用圆括号来捕获。例如,

perl -nle 'if (/She said: (.*)/) { print $1 }'

Perl 会在当前行中匹配字符串"She said:"和后面的内容然后捕获所有的后面的内容。再把这些内容放入$1变量,最后打印出来。
当你再用另外一个括号的时候,后来一个捕获的内容就会放在$2这个变量中。$3也是如此。

perl -nle 'if (/(She|He) said: (.*)/) { print "$1: $2" }'

在这个语句中,首先"She"/"He"字符串将会被捕获在$1变量中,然后 "said: "字符串后面的内容也会被捕获并放在$2这个变量当中,最后我们将这两个变量以"$1: $2"这种形式打印出来。你使用多少个圆括号就会有多少个捕获的变量,各个变量的名称依次是: $1$2$3……

注意:圆括号还有一个功能就是包裹一个连续的字符串,所以有时候你不想捕获这些内容,那怎么办呢?你可以在圆括号中用?:这样的符号。例如,可以将上面One-line语句中的(She|He)变成(?:She|He)

perl -nle 'if (/(?:She|He) said: (.*)/) { print "Someone said: $1" }'

这个语句不会捕获"She"或者"He"字符串。故第二个圆括号中捕获的内容就会在 $1这个变量中。
在Perl 5.10版本之后,你可以用写了名字的群(如?<name>...)去捕获你要的字符串。当你这样做的时候你就不需要用$1 $2这些变量去调用捕获的字符串,而是用 $+{name}去调用这些捕获的字符串。例如,下面的这个语句可以捕获"She"或者"He" 在 gender这个群,"said:" 后面的内容在 text 这个群。

perl -nle 'if (/(?<gender>She|He) said: (?<text>.*)/) {
        print "$+{gender}: $+{text}"
}'

A.6 变量 $,

当你打印很多个值时,$,变量是输出域的分隔符。在默认的情况下,他处于未定义状态,也就是 undefined,就是说所有输出的变量都是紧密地被打印在一起的。例如如果你执行下面的语句:

perl -le 'print 1, 2, 3'

你将会得到 "123" 这个输出结果,如果你将 $,变量设置为一个分号,

perl -le '$,=":"; print 1, 2, 3'

你将会得到 "1:2:3"

A.7 变量 $"

请先看下面的两个例子:

perl -le '@data=(1,2,3); print "@data"'

输出的结果是 "1 2 3" 。

perl -le '@data=(1,2,3); $" = "-"; print "@data"'

输出的结果是 "1-2-3"。

这个变量默认的值是一个空白符,是打印数组时的分隔符。

A.8 变量 @F

当使用 -a参数的时候,Perl会根据空白符自动分割每一行然后将所有的元素都放在@F这个数组当中。例如输入的行是"foo bar baz",然后 @F就是一个 ("foo","bar","baz")的数组。
这样的技术就会允许你单独操作每一个域(field)。 例如,你可以利用$F[2]打印出第三个域的值:

perl -ane 'print $F[2]'

你也可以做很多次的运算,比如将第五个域的值乘以2:

perl -ane '$F[4] *= 2; print "@F"'

在这里,第五个域的值乘以了2, print "@F"语句打印出整个数组的值,用一个空格来分割。
你可以将-a参数和-F参数连用,后者可以生命用哪个字符串作为输入行的分隔符。例如去处理每行用分号分割的 /etc/passwd 文件时,你可以写如下的语句:

perl -a -F: -ne 'print $F[0]' /etc/passwd

A.9 变量@ARGV

这个变量包含了在运行命令时,传给Perl的参数。例如,下面的语句将会打印出 "foo bar baz" :

perl -le 'print "@ARGV"' foo bar baz

注意当你使用 -n或者-p参数时,Perl将会一个接一个地打开文件,然后将@ARGV数组中的元素逐个删去。所以如果你要通过这个数组去得到所有传入的文件名的话,那你就要在 BEGIN{} 这个代码块中进行赋值:

perl -nle 'BEGIN { @A = @ARGV }; ...' file1 file2

然后你可以通过@A数组引用。
还有一个相似的变量,$ARGV,这个变量包含了当前读取文件的文件名。如果是从标准输入读取的,那么文件名叫做"-"。

A.10 变量 %ENV

这个哈希包含了你当前运行Shell的所有环境。
下面的这行语句打印出了所有的环境变量:

perl -le 'print "$_: $ENV{$_}" for keys %ENV'

这个语句循环了所有这个哈希中的所有元素,每次都将 key 放入 $_变量然后打印出key对应的value。

·

推荐阅读更多精彩内容