Perl-单行代码指南-全-

Perl 单行代码指南(全)

原文:zh.annas-archive.org/md5/a201d6221e8a2a222530ba7ddf1b1981

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:Perl 单行命令入门

Perl 单行命令是小巧而强大的 Perl 程序,能够在一行代码中完成一个任务。它们可以做得非常好——比如更改行间距、给行编号、执行计算、转换和替换文本、删除和打印特定行、解析日志、就地编辑文件、计算统计数据、执行系统管理任务或一次更新多个文件。Perl 单行命令将使你成为一个 Shell 战士:曾经需要几分钟(甚至几小时)才能解决的问题,现在只需几秒钟!

在本章节中,我将向你展示单行命令的样子,并让你尝尝本书其余部分的内容。本书需要一定的 Perl 知识,但大多数单行命令都可以在不深入了解语言的情况下进行调整和修改。

我们来看一些例子。这里是一个例子:

perl -pi -e 's/*you*/*me*/g' *file*

这个单行命令会将文件file中所有的you替换为me。如果你问我,这非常有用。想象一下,你在一台远程服务器上,需要替换文件中的文本。你可以打开文本编辑器执行查找替换,或者直接通过命令行进行替换,一下子就完成了。

这个单行命令和本书中的其他命令在 UNIX 系统上运行良好。我使用的是 Perl 5.8 来运行它们,但它们也适用于更新版的 Perl,例如 Perl 5.10 及更高版本。如果你在 Windows 计算机上,你需要稍微修改它们。为了让这个单行命令在 Windows 上工作,可以将单引号换成双引号。有关在 Windows 上使用 Perl 单行命令的更多信息,请参见附录 B。

在本书中,我将使用 Perl 的-e命令行参数。它允许你使用命令行来指定要执行的 Perl 代码。在前面的单行命令中,代码的意思是“执行替换(s/you/me/g命令)并将you替换为me,全局替换(/g标志)。”-p参数确保代码在每一行输入上都执行,并且执行后打印该行。-i参数确保file文件就地编辑。就地编辑(in-place)意味着 Perl 会直接在文件中进行所有替换,覆盖你想要替换的内容。我建议你通过在-i参数中指定备份扩展名来始终备份你正在编辑的文件,例如:

perl -pi.bak -e 's/*you*/*me*/g' *file*

现在,Perl 首先创建一个file.bak备份文件,然后才修改file的内容。

那么,如何在多个文件中进行相同的替换呢?只需在命令行中指定文件:

perl -pi -e 's/*you*/*me*/g' *file1 file2 file3*

这里,Perl 首先在file1中将you替换为me,然后在file2file3中执行相同的操作。

你也可以像这样,只对匹配we的行进行相同的替换:

perl -pi -e 's/*you*/*me*/g if /*we*/' *file*

在这里,你使用条件if /we/来确保 s/you/me/g只在匹配正则表达式/we/的行上执行。

正则表达式可以是任何内容。比如你想只对包含数字的行执行替换。你可以使用/\d/正则表达式来匹配数字:

perl -pi -e 's/*you*/*me*/g if /\d/' *file*

如何查找文件中出现超过一次的所有行?

perl -ne 'print if $a{$_}++' *file*

这个单行命令记录你迄今为止遇到的行,并在%a哈希中计数它看到这些行的次数。如果已经见过该行,条件$a{$_}++为真,因此它会打印该行。否则,它会“自动”在%a哈希中创建一个包含当前行的元素并增加其值。$_特殊变量包含当前行。这个单行命令还使用了-n命令行参数来遍历输入,但与-p不同,它不会自动打印行。 (现在不要担心所有的命令行参数;你会随着本书的学习逐步了解它们!)

如何给行编号?超级简单!Perl 的$.特殊变量保持当前行号。只需将其与行一起打印出来:

perl -ne 'print "$. $_"' *file*

你也可以通过使用-p参数并修改$_变量来实现同样的功能:

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

在这里,每一行都被替换为字符串"$. $_",这等于当前行号后跟该行的内容。(请参阅第 17 页的单行命令 3.1 以获取完整解释。)

如果你省略了单行命令末尾的文件名,Perl 将从标准输入读取数据。从现在开始,我将假设数据来自标准输入,并省略文件名。如果你想在整个文件上运行单行命令,随时可以加回文件名。

你还可以将前面两个单行命令结合起来,创建一个只对重复行编号的命令:

perl -ne 'print "$. $_" if $a{$_}++'

你还可以使用List::Util CPAN 模块中的sum函数对每一行的数字进行求和。CPAN(Comprehensive Perl Archive Network;* www.cpan.org/ *)是一个包含超过 100,000 个可重用 Perl 模块的档案。List::Util是 CPAN 上的一个模块,包含各种列表工具函数。你不需要安装这个模块,因为它已经随 Perl 一起提供(它是 Perl 核心的一部分)。

perl -MList::Util=sum -alne 'print sum @F'

-MList::Util命令行参数导入了List::Util模块。这个单行命令中的=sum部分导入了List::Util模块中的sum函数,使得程序能够使用这个函数。接下来,-a启用了当前行自动分割成@F数组的字段。默认情况下,分割是在空白字符上进行的。-l参数确保print在每行结束时输出一个换行符。最后,sum @F计算@F列表中所有元素的总和,print打印结果并跟随一个换行符(这是我用-l参数添加的)。(请参阅第 30 页的单行命令 4.2 了解更详细的解释。)

如何查找 1299 天前的日期?试试这个:

perl -MPOSIX -le
  '@t = localtime; $t[3] -= 1299; print scalar localtime mktime @t'

我在一行代码 4.19(第 41 页)中详细解释了这个例子,但基本上,你修改了 localtime 返回的结构的第四个元素,这正好是天数。你只需从当前日期减去 1299 天,然后通过 localtime mktime @t 重新组合结果成新的时间,并以标量上下文打印结果,显示人类可读的时间。

那么如何生成一个八个字母的密码呢?给你一个:

perl -le 'print map { ("a".."z")[rand 26] } 1..8'

"a".."z" 生成从 az 的字母列表(总共 26 个字母)。然后你随机选择一个字母八次!(这个例子在第 51 页的一行代码 5.4 中有详细解释。)

或者,假设你想找到与某个 IP 地址对应的十进制数。你可以使用 unpack 很快地找到它:

perl -le 'print unpack("N", 127.0.0.1)'

这个一行代码使用了 v-字符串,即版本字面量。V-字符串提供了一种方法,通过指定的序号来组合字符串。IP 地址 127.0.0.1 被视为 v-字符串,这意味着数字 127001 被连接成一个由四个字符组成的字符串,其中第一个字符的序号值为 127,第二和第三个字符的序号值为 0,最后一个字符的序号值为 1。接下来,unpack 将它们解包成一个单一的十进制数字,按照“网络”(大端)顺序进行排列。(更多内容请参见第 45 页的一行代码 4.27)

那么计算呢?让我们找出表格中第一列数字的总和:

perl -lane '$sum += $F[0]; END { print $sum }'

行会使用 -a 参数自动分割成字段,可以通过 @F 数组访问。数组的第一个元素 $F[0] 就是第一列,所以你只需用 $sum += $F[0] 将所有列的值相加。当 Perl 程序完成时,它会执行 END 块中的任何代码,在这个例子中就是打印出总和。简单!

现在,让我们找出有多少数据包通过了 iptables 规则:

iptables -L -nvx | perl -lane '$pkts += $F[0]; END { print $pkts }'

iptables 程序在第一列输出数据包。你只需要将第一列中的数字相加,就能知道通过防火墙规则的包的数量。虽然 iptables 也会输出表头,但你可以安全地忽略这些,因为 Perl 会将它们转换为零,以便进行 += 操作。

如何获取系统中所有用户的列表?

perl -a -F: -lne 'print $F[4]' /etc/passwd

-a-F 参数结合使用,让你可以指定分割行的字符,默认情况下是空格。在这里,你可以使用冒号字符作为分隔符,正好是 /etc/passwd 的记录分隔符。接下来,你打印第五个字段 $F[4],它包含用户的真实姓名。

如果你在命令行参数上迷路了,记住 Perl 配备了一个很棒的文档系统,叫做 perldoc。在命令行输入 perldoc perlrun,这将显示如何运行 Perl 以及所有命令行参数的文档。当你突然忘记哪个命令行参数做什么,需要快速查找时,这个非常有用。你也可以阅读 perldoc perlvar,它解释了变量;perldoc perlop,它解释了操作符;以及 perldoc perlfunc,它解释了函数。

Perl 一行命令让你能够快速完成许多任务。你将在本书中找到超过 130 个一行命令。阅读它们,尝试它们,没多久你就会成为本地的 Shell 大师。(只是不要告诉你的朋友——除非你想要竞争。)

享受吧!

第二章 空格

在本章中,我们将探讨各种改变行和词间距的一行命令,执行诸如在文件中双倍或三倍行距、删除空行以及双倍行距词汇等任务。你还将了解各种命令行参数,如-p-e-n,以及特殊变量,如$_$\

2.1 双倍行距文件

perl -pe '$\ = "\n"' *file*

这个一行命令实现了文件的双倍行距。这里需要解释三件事:-p-e命令行选项以及简短的$\ = "\n" Perl 程序。

使用-e选项可以直接在命令行输入 Perl 程序。通常你不会为每个小程序创建源文件;使用-e你可以很容易地将程序直接写入命令行,作为一行命令。在这种情况下,整个 Perl 程序包含在这一行命令中,即$\ = "\n"。务必使用单引号(')包裹程序,否则你的 shell 会将$\等内容解释为 shell 变量,而这些变量没有值,实际上会将它们删除!

现在让我们看看-p选项。指定-p告诉 Perl 假定在你的程序周围有一个循环:

while (<>) {
    # your program goes here (specified by -e)
} continue {
    print or die "-p failed: $!\n";
}

从广义上讲,这个构造会遍历所有输入,执行你的代码,并打印$_的值(print语句打印$_的值),这使你能够快速修改输入的全部或部分行。$_变量是一个特殊的变量,它会被当前的文本行替换。它也可以替换为其他内容。你将在本书中学到关于$_的所有知识。(有关它的使用案例,请参见附录 A。)

但更详细地理解这个循环的工作原理是很重要的。首先,while (<>)循环从标准输入中读取每一行并将其放入$_变量中。接着,执行由-e指定的代码,然后是print or die部分。

continue语句在每一行之后执行print or die语句,尝试打印$_变量的内容。如果尝试失败(例如,终端不可写,或者标准输出被重定向到无法写入的地方),die会使 Perl 退出并显示错误信息。

在这行命令中,-e指定的代码是$\ = "\n",因此 Perl 执行的程序看起来像这样:

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

这个 Perl 程序将每一行读入$_变量,然后设置$\为换行符并调用print。另一个特殊的变量是$\。它类似于 Awk 中的ORS(输出记录分隔符)变量,它会在每次print操作之后附加。没有参数的print语句打印$_的内容,并在输出的末尾附加$\。结果是,每一行被原样打印,并在末尾附加$\,其值为换行符。现在,输入变为双倍行距。

实际上,你并不需要为每一行都设置$\为换行符;你可以只在程序的开始处设置一次:

perl -pe 'BEGIN { $\ = "\n" }' *file*

这个一行代码在 Perl 执行任何操作之前,只会将\设置为换行符一次,位于BEGIN代码块中。BEGIN代码块是一个特殊的代码块,在 Perl 程序中执行的所有其他操作之前首先被执行。以下是展开后的 Perl 程序,它的运行方式与前一个一行代码完全相同:

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

这是另一种对文件进行双倍行距的方式。这个一行代码在每行的末尾添加一个换行符,然后print该行:

perl -pe '$_ .= "\n"' *file*

这个一行代码等同于

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

$_ = $_ . "\n"与写$_ .= "\n"是等效的。这个表达式只是将$_"\n"连接起来。(句点(.)是字符串连接操作符。)

但可能最简洁的双倍行距方式是使用替换操作符s

perl -pe 's/$/\n/' *file*

这个一行代码将正则表达式$(匹配行尾的字符)替换为换行符,有效地在行尾添加了一个换行符。

如果你使用的是 Perl 5.10 或更高版本,你可以使用say操作符。say操作符的作用类似于print,但它总是在行尾添加一个换行符。在 Perl 5.10 中,这个一行代码可以这样写:

perl -nE 'say' *file*

-E命令行参数的作用与-e命令行参数完全相同,但它还启用了 Perl 5.10 的功能,包括say操作符。-n参数与-p类似,但你必须自己打印每一行。(我在一行代码 2.6 中更详细地解释了-n参数。)这个一行代码打印该行,接着由say操作符追加另一个换行符。

例如,如果文件包含四行:

line1
line2
line3
line4

运行这些一行代码中的任何一行都会输出以下内容:

line1

line2

line3

line4

在这些最初的几个示例中,我将文件名作为最后一个参数传递给一行代码。当我这么做时,一行代码会在该文件的内容上进行操作。如果我没有给一行代码传递文件名,它们将操作来自标准输入的数据。从现在开始,我不会在一行代码的末尾指定文件,但如果你想在文件上运行一行代码,你始终可以将文件名加回来。在编写一行代码时,最好通过直接在标准输入中键入一些内容来快速测试它们是否正确。然后,当你确信这行代码有效时,你可以在末尾传递一个或多个文件名。

再次提醒,不要忘记 Perl 的便捷文档系统perldoc。只需在命令行输入perldoc perlrun,就能显示如何运行 Perl 及所有命令行参数的相关信息。

2.2 对文件进行双倍行距操作,排除空行

perl -pe '$_ .= "\n" unless /^$/'

这个一行代码通过在每个非空行的末尾添加一个换行符来双倍行距所有非空行。unless表示“如果不是”,unless /^$/表示“如果不是‘行首到行尾’”。条件“行首到行尾”仅对空行成立。

以下是展开后的这个一行代码的样子:

while (<>) {
    unless (/^$/) {
        $_ .= "\n"
    }
} continue {
    print or die "-p failed: $!\n";
}

这是一个更好的测试,它考虑到了行中的空格和制表符:

perl -pe '$_ .= "\n" if /\S/'

这里,行与\S进行匹配——这是一个正则表达式序列,是\s的反向,\s匹配任何空白字符(包括制表符、垂直制表符、空格、换行符和回车符)。\s的反向是任何非空白字符。结果是,所有包含至少一个非空白字符的行都被双倍行距。

2.3 将文件设置为三倍行距

你也可以通过在每行末尾输出更多的换行符,轻松地将文件设置为三倍行距:

perl -pe '$\ = "\n\n"'

或者

perl -pe '$_ .= "\n\n"'

或者

perl -pe 's/$/\n\n/'

这些命令与本章中的第一个命令类似,不同之处在于每行末尾添加了两个换行符。

2.4 将文件设置为 N 行间距

perl -pe '$_ .= "\n"x7'

这行命令在每行后插入七个换行符。注意,我使用了"\n" x 7来将换行符重复七次。x运算符将左侧的值重复N次。

例如,这一行

perl -e 'print "foo"x5'

打印foofoofoofoofoo

顺便提一下,有时候当你需要生成一定量的数据时,x运算符非常有用。例如,要生成 1KB 的数据,你可以这样做:

perl -e 'print "a"x1024'

这行命令打印字符* a * 1024 次。

2.5 在每行前添加一个空行

perl -pe 's/^/\n/'

这行命令使用了s/regex/replace/运算符。它用替代值替换给定的正则表达式。在这行命令中,运算符是s/^/\n/,正则表达式是^,替代值是\n^模式匹配文本的开始位置,而s运算符将其替换为\n,即换行符。因此,换行符被插入到这一行之前。要插入其他内容,只需将\n替换为你要插入的部分。

2.6 删除所有空行

perl -ne 'print unless /^$/'

这行命令使用了-n标志,告诉 Perl 在程序周围假定一个与-p不同的循环:

while (<>) {
    # your program goes here
}

将这个循环与当你指定-p时 Perl 假定的循环进行比较,你会看到这个循环没有continue { print or die }部分。在这个循环中,每一行都由钻石运算符<>读取并存储在特殊变量$_中,但它不会被打印!你必须自己打印这一行——如果你想选择性地打印、修改或删除行,这是一个非常有用的功能。

在这行命令中,代码是print unless /^$/,所以整个 Perl 程序变成了:

while (<>) {
    print unless /^$/
}

进一步解析,你得到这个:

while (<>) {
    print $_ unless $_ =~ /^$/
}

这行命令打印所有非空行。(你在第 11 页的一行命令 2.2 中看到了/^$/正则表达式。)

这行命令还删除了所有空行:

perl -lne 'print if length'

这个单行命令使用了-l命令行参数,它会自动去掉输入行末尾的换行符(基本上就是去掉末尾的换行),然后在打印时再将其加回到行尾。指定给-e参数的代码是'print if length',意思是“如果行有内容,就打印”。空行的长度为 0,因此不会被打印(在 Perl 中,0 是一个假值,因此if length条件会返回假)。所有其他非空行都有长度,并且会被打印。如果没有-l,字符串的末尾仍然会有换行符,因此长度会是 1 或 2 个字符!^([1])

这是另一个去除所有空白行的单行命令:

perl -ne 'print if /\S/'

这个单行命令与前两个稍有不同。print unless /^$/print if length都会打印出仅由空格和/或制表符组成的行。这样的行看起来像是空的,可能需要过滤掉。这个单行命令使用了\S(在 2.2 的单行命令中解释过),这是一个匹配非空字符的正则表达式序列。仅包含空格和/或制表符的行不匹配\S,因此不会被打印。

正如你所看到的,你可以用很多不同的方式写同样的程序。事实上,Perl 的座右铭是有不止一种方法可以做这件事,简称TIMTOWTDI,发音为“Tim Toady”。(有趣的冷知识:Perl 的发明者 Larry Wall 在 Twitter 和 IRC 上使用@TimToady 作为昵称。)

2.7 删除所有连续的空白行,只留下一个

perl -00 -pe ''

这个单行命令真的很难理解,不是吗?首先,它没有任何代码!-e是空的。接下来,它有一个愚蠢的-00命令行选项,它启用了段落吸入模式,这意味着 Perl 按段落而不是按行读取文本。(段落是由两个或更多换行符分隔的文本。)段落被放入$_中,并且-p选项会打印它。

你甚至可以用更简洁的方式写这个:

perl -00pe0

这里,指定给-e的代码是0,它什么也不做。

这是我最喜欢的单行命令之一,因为如果你之前没有见过它,可能很难理解,而我喜欢思考难题。(-e没有指定代码!它怎么可能有用?)

2.8 将所有空白行压缩/扩展为连续的 N 行

假设你有一个文件,每个段落后都有两个空白行,而你希望将段落之间的行距扩展为三行。你可以像这样将 2.4 和 2.7 的单行命令结合起来:

perl -00 -pe '$_ .= "\n"x2'

这个单行命令通过-00选项按段落读取行,然后在每个段落后加上三个换行符。代码"\n"x2打印两个换行符,这两个换行符会加到段落末尾已经存在的空白行后面。

以类似的方式,你也可以减少段落之间的行距。假设你有一个文件,出于某种奇怪的原因,段落之间有十个空白行,而你希望将这些空白行压缩为三行。你可以再次使用相同的单行命令!

2.9 在所有单词之间加倍行距

perl -pe 's/ /  /g'

在这里,你使用替代操作符 s将一个空格“ ”替换为两个空格“ ”,并且每一行都全局替换(/g标志使替换全局进行),就这么简单!

这是一个示例。假设你有这样一行文本:

this line doesn't have enough whitespace!

运行这个单行命令会增加单词之间的间距:

this  line  doesn't  have  enough  whitespace!

2.10 删除单词之间的所有空格

perl -pe 's/ +//g'

这个单行命令使用“+”正则表达式来匹配一个或多个空格。当它找到匹配项时,会将其替换为空字符串,并全局删除所有单词之间的空格。

如果你还想去除可能添加间距的制表符和其他特殊字符,可以使用\s+正则表达式,它表示“匹配空格、制表符、垂直制表符、换行符或回车符”:

perl -pe 's/\s+//g'

这是一个示例。假设你有这样一行文本:

this line has too much whitespace said cowboy neal

运行这个单行命令会移除所有空格:

thislinehastoomuchwhitespacesaidcowboyneal

2.11 将所有单词之间的空格更改为一个空格

perl -pe 's/ +/ /g'

这个单行命令与前一个类似,只是它将一个或多个空格替换为一个空格。

例如,如果你有这样一行:

this   line has really           messed-up                    spacing

运行这个单行命令将单词之间的间距标准化为一个空格:

this line has really messed-up spacing

2.12 在所有字符之间插入空格

perl -lpe 's// /g'

在这里,你匹配看似没有任何东西的地方,并将其替换为一个空格。这个“什么也没有”实际上意味着“字符之间的匹配”,因此你会在所有字符之间插入一个空格。(匹配包括文本的开始和结束。)

例如,给定这一行:

today was a great day

运行这个单行命令会产生以下结果:

t o d a y   w a s   a   g r e a t   d a y

可能很难看到所有空格被添加到哪里,所以我们通过修改这个单行命令来在所有字符之间插入一个冒号来说明这一点:

perl -lpe 's//:/g'

这将输出:

:t:o:d:a:y: :w:a:s: :a: :g:r:e:a:t: :d:a:y:

如你所见,空格(或冒号)也会插入到文本的开头和结尾。还要注意,现有的空格也算作字符,因此它们是三倍的间距。


^([1]) Windows 使用两个字符表示换行符。

第三章 编号

在本章中,我们将介绍各种用于为行和单词编号的单行命令,你还将了解$.这个特殊变量。你还会学习到 Perl 高尔夫,一种“运动”,它要求编写最短的 Perl 程序来完成任务。

3.1 在文件中为所有行编号

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

正如我在单行命令 2.1(第 7 页)中解释的那样,-p告诉 Perl 假设程序(由-e指定)周围有一个循环,它读取输入的每一行到$_变量中,执行程序,然后打印$_变量的内容。

这个单行命令仅通过将$.变量附加到$_上来修改$_。特殊变量$.包含输入的当前行号。结果是,每一行的行号都会被添加到行首。

同样,你也可以使用-n参数并打印字符串"$. $_",即当前行号后跟该行内容:

perl -ne 'print "$. $_"'

假设一个文件包含三行:

foo
bar
baz

运行这个单行命令会为它们编号:

1 foo
2 bar
3 baz

3.2 在文件中仅为非空行编号

perl -pe '$_ = ++$x." $_" if /./'

在这里,你使用了“条件成立时执行操作”语句,该语句只有在条件为真时才会执行操作。在这种情况下,条件是正则表达式/./,它匹配所有非换行符的字符(即,匹配非空行)。操作$_ = ++$x." $_"将变量$x(递增后)附加到当前行的前面。由于没有使用strict pragma,变量$x在第一次递增时会自动创建。

结果是,在每个非空行上,变量$x会递增,并被添加到该行的前面。空行不会被修改,且原样打印。

单行命令 2.2(第 11 页)展示了通过正则表达式\S匹配非空行的另一种方法:

perl -pe '$_ = ++$x." $_" if /\S/'

假设一个文件包含四行,其中两行是空的:

line1

line4

运行这个单行命令只为第一行和第四行编号:

1 line1

2 line4

3.3 在文件中为非空行编号并打印(删除空行)

perl -ne 'print ++$x." $_" if /./'

这个单行命令使用了-n程序参数,它将行放入$_变量中,然后执行由-e指定的程序。与-p不同,-n在执行完-e中的代码后不会自动打印行,因此你需要显式调用print来打印$_变量的内容。

这个单行命令只在含有至少一个字符的行上调用print,与前一个单行命令一样,它会为每个非空行将行号保存在变量$x中并递增。空行会被忽略,不会被打印。

假设一个文件包含和单行命令 3.2 相同的四行:

line1

line4

运行这个单行命令会删除空行,并为第一行和第四行编号:

1 line1
2 line4

3.4 为所有行编号,但仅为非空行打印行号

perl -pe '$_ = "$. $_" if /./'

这个单行命令与单行命令 3.2 类似。在这里,只有当行中至少包含一个字符时,才会修改保存整行的$_变量。所有其他空行会原样打印,不带行号。

假设一个文件包含四行:

line1

line4

运行这个单行命令会为所有行编号,但只打印第一行和第四行的行号:

1 line1

4 line4

3.5 仅编号匹配模式的行;打印其余行原样

perl -pe '$_ = ++$x." $_" if /*regex*/'

在这里,依然使用了“条件下的动作”语句,条件仍然是一个模式(正则表达式):/regex/。动作与单行命令 3.2 中的相同。

假设一个文件包含以下行:

record foo
bar baz
record qux

如果你想为包含record一词的行编号,可以将单行命令中的/regex/替换为/record/

perl -pe '$_ = ++$x." $_" if /record/'

当你运行这个单行命令时,它会给你以下输出:

1 record foo
bar baz
2 record qux

3.6 只编号并打印匹配模式的行

perl -ne 'print ++$x." $_" if /*regex*/'

这个单行命令几乎与单行命令 3.3 完全相同,只是它仅编号并打印匹配/regex/的行。它不会打印不匹配的行。

例如,一个文件包含与单行命令 3.5 中相同的行:

record foo
bar baz
record qux

假设你只想编号并打印包含record一词的行。在这种情况下,改变/regex//record/并运行单行命令将得到如下结果:

1 record foo
2 record qux

3.7 为所有行编号,但只对匹配特定模式的行打印行号

perl -pe '$_ = "$. $_" if /*regex*/'

这个单行命令类似于单行命令 3.4 和 3.6。在这里,如果某一行匹配/regex/,则在该行前面加上行号;否则,该行将不带行号直接打印。

/regex/替换为/record/并在与单行命令 3.6 相同的示例文件上运行此单行命令将给出如下输出:

1 record foo
bar baz
3 record qux

3.8 使用自定义格式为文件中的所有行编号

perl -ne 'printf "%-5d %s", $., $_'

这个单行命令使用printf打印行号和行内容。printf进行格式化输出。你指定格式并将数据传递给它,然后它会根据格式打印数据。这里,行号的格式是%-5d,它将行号左对齐,占据五个字符的位置。

这是一个示例。假设这个单行命令的输入是

hello world
bye world

然后输出如下:

1     hello world
2     bye world

其他格式字符串包括%5d,它将行号右对齐,位置宽度为五个字符,以及%05d,它用零填充并右对齐行号。以下是使用%5d格式字符串打印行号的输出:

    1 hello world
    2 bye world

这是使用%05d格式字符串得到的输出:

00001 hello world
00002 bye world

要了解更多关于可用的各种格式,请在命令行运行perldoc -f sprintf

3.9 打印文件的总行数(模拟wc -l

perl -lne 'END { print $. }'

这个单行命令使用了 Perl 从 Awk 语言中借来的END块。END块在 Perl 程序执行完毕后执行。在这里,Perl 程序是由-n参数创建的对输入的隐式循环。一旦它遍历完输入,特殊变量$.就包含了输入中的行数,END块将打印这个变量。-l参数设置了print的输出记录分隔符为换行符,因此你无需手动打印换行符,像这样:print "$.\n"

你也可以用这个单行命令做相同的事情:

perl -le 'print $n = () = <>'

如果您对 Perl 上下文理解得很透彻,这个单行代码很容易理解。() = <> 这一部分告诉 Perl 在列表上下文中评估 <> 操作符(即钻石操作符),这使得钻石操作符将整个文件作为行的列表读取。接下来,您将这个列表赋值给 $n。因为 $n 是标量,所以这个列表赋值是在标量上下文中评估的。

这里真正发生的事情是 = 操作符是右结合的,这意味着右边的 = 会先执行,左边的 = 会后执行:

perl -le 'print $n = (() = <>)'

在标量上下文中评估列表赋值会返回列表中的元素个数;因此,$n = () = <> 构造等同于输入中的行数,也就是文件中的行数。print 语句会打印这个数字。-l 参数确保在打印数字后添加一个换行符。

您还可以从这个单行代码中省略变量 $n,并通过 scalar 操作符强制标量上下文:

perl -le 'print scalar(() = <>)'

在这里,您不需要通过再次将其赋值给另一个标量来在标量上下文中评估列表赋值,您只需要使用 scalar 操作符在标量上下文中评估列表赋值。

现在来看一个更明显的版本:

perl -le 'print scalar(@foo = <>)'

在这里,您不使用空列表 () 来强制 <> 处于列表上下文中,而是使用变量 @foo 来实现相同的效果。

这里有另一种方法:

perl -ne '}{print $.'

这个单行代码使用了所谓的Eskimo 操作符 }{(实际上是一个巧妙的构造)与 -n 命令行参数配合使用。正如我之前解释的,-n 参数强制 Perl 在程序周围假设一个 while(<>) { } 循环。Eskimo 操作符强制 Perl 跳出这个循环,这样单行代码就扩展成了:

while (<>) {
}{               # eskimo operator here
    print $.;
}

如您所见,这个程序只是循环遍历所有输入,并在完成后打印 $.,即输入中的行数。如果您稍微调整一下格式,这一点就会更加明显:

while (<>) {}
{
    print $.;
}

如您所见,这只是一段空循环,它循环遍历所有输入,然后是一个用大括号包裹的 print 语句。

3.10 打印文件中非空行的数量

perl -le 'print scalar(grep { /./ } <>)'

这个单行代码使用了 Perl 的 grep 函数,它类似于 UNIX 的 grep 命令。给定一个值列表,grep { condition } list 只返回那些使得 condition 为真的值。在这个例子中,条件是一个正则表达式,用来匹配至少一个字符,因此输入会被过滤,grep{ /./ } 返回所有非空行。为了得到行数,您需要在标量上下文中评估 grep 并打印结果。

一些 Perl 程序员喜欢创建最短的 Perl 程序来完成某个特定的任务——这项练习叫做Perl 高尔夫。这个单行代码的高尔夫版将 scalar() 替换为 ~~(双重按位取反)并去掉空格,将其缩短成这样:

perl -le 'print ~~grep{/./}<>'

这个双重按位取反技巧实际上是scalar的同义词,因为按位取反作用于标量值,因此grep在标量上下文中执行。

你可以通过去掉print后的空格并去除大括号,使这段代码更简洁:

perl -le 'print~~grep/./,<>'

如果你使用的是 Perl 5.10 或更高版本,你还可以使用-E命令行开关和say操作符:

perl -lE 'say~~grep/./,<>'

真正的高尔夫大师之作!

3.11 打印文件中空行的数量

perl -lne '$x++ if /^$/; END { print $x+0 }'

在这里,你使用变量$x来计数遇到的空行数量。一旦遍历完所有行,你在END块中打印$x的值。你使用$x+0的构造来确保如果没有空行,输出0。(否则$x将没有被创建并且是未定义的,给未定义的值加上+0会输出0。)$x+0的替代方法是int运算符:

perl -lne '$x++ if /^$/; END { print int $x }'

你还可以通过以下方式修改之前的一行代码:

perl -le 'print scalar(grep { /^$/ } <>)'

或者用~~来写:

perl -le 'print ~~grep{ /^$/ } <>'

~~进行了两次按位取反操作,这使得grep在标量上下文中执行,并返回空行的数量。

这最后两个版本不如带有END块的一行代码高效,因为它们将整个文件读入内存,而带有END块的一行代码是逐行处理的,因此只在内存中保留一行输入。

3.12 打印文件中匹配模式的行数(模拟grep -c

perl -lne '$x++ if /*regex*/; END { print $x+0 }'

这段一行代码基本上与 3.11 相同,只是当某行匹配正则表达式/regex/时,它会将行计数器$x递增 1。$x+0的技巧确保在没有行匹配/regex/时,输出0。(详见 3.11 中的$x+0技巧的详细解释。)

3.13 为所有行中的单词编号

perl -pe 's/(\w+)/++$i.".$1"/ge'

这个一行代码使用了/e标志,使得 Perl 将replace部分的s/regex/replace/表达式作为代码来执行!

这里的代码是++$i.".$1",意思是“将变量$i递增 1,然后将其加到字符串".$1"前面(即一个点和匹配组$1的内容)。”这里的匹配组是每个单词:(\w+)

一句话来说,这段一行代码匹配一个单词(\w+),将其放入$1,然后执行++$i.".$1"代码,为单词全球编号(/g标志)。完成了—所有单词都被编号。

例如,如果你有一个文件,包含以下三行:

just another
perl hacker
hacking perl code

运行这段一行代码会为文件中的每个单词编号,并生成以下输出:

1.just 2.another
3.perl 4.hacker
5.hacking 6.perl 7.code

3.14 为每一行中的单词编号

perl -pe '$i=0; s/(\w+)/++$i.".$1"/ge'

这类似于一行代码 3.13,只是你在每行开始时将变量$i重置为0。以下是运行这段一行代码在 3.13 中的例子时的结果:

1.just 2.another
1.perl 2.hacker
1.hacking 2.perl 3.code

如你所见,每一行中的单词编号是独立于其他行的。

3.15 用数字位置替换所有单词

perl -pe 's/(\w+)/++$i/ge'

这个单行命令几乎与单行命令 3.13 相同。在这里,你只需用每个单词的数字位置来替换它,而这个数字位置保存在变量$i中。例如,如果你在单行命令 3.13 和 3.14 中的文件上运行这个单行命令,它会将文件中的单词替换为它们的数字位置,输出如下:

1 2
3 4
5 6 7

好玩!

第四章 计算

本章将介绍各种用于计算的一行代码,如查找最小值和最大值、计数、打乱和排列单词,以及计算日期和数字。你还将了解-a-M-F命令行参数、$特殊变量,以及@{[ ... ]}构造,它允许你在双引号内运行代码。

4.1 检查一个数字是否是质数

perl -lne '(1x$_) !~ /¹?$|^(11+?)\1+$/ && print "$_ is prime"'

这行代码使用了 Abigail 巧妙的正则表达式来检测给定的数字是否是质数。(不要把这个正则表达式当真;我把它放在这里是为了其艺术价值。如果是严肃的用途,请使用 CPAN 的Math::Primality模块来判断一个数字是否是质数。)

这是这个巧妙的一行代码的工作原理:首先,数字通过(1x$_)转换成其一元表示法。例如,5被转换为1x5,即111111重复5次)。接下来,一元数字会被测试是否与正则表达式匹配。如果不匹配,则该数字是质数;否则,它是合成数。!~运算符是=~运算符的反义,如果正则表达式不匹配,则返回 true。

正则表达式由两部分组成:第一部分¹?$匹配1和空字符串。显然,空字符串和 1 都不是质数,因此这一部分的正则表达式会将它们排除。

第二部分^(11+?)\1+$决定了两个或更多的1是否重复构成整个数字。如果是,正则表达式匹配,意味着该数字是合成数。如果不是,它是质数。

现在考虑正则表达式的第二部分如何作用于数字 5。数字 5 的一元表示法是 11111,因此(11+?)匹配前两个1,回溯引用\1变成 11,整个正则表达式变为¹¹(11)+$。由于它无法匹配五个1,所以失败。接下来,它尝试匹配前三个1。回溯引用变为 111,整个正则表达式变为¹¹¹(111)+$,仍然不匹配。该过程对 1111 和 11111 也重复,最终整个正则表达式没有匹配,说明该数字是质数。

那么数字 4 呢?数字 4 在一元表示法中是 1111。(11+?)匹配前两个1。回溯引用\1变成 11,整个正则表达式变为¹¹(11)+$,匹配原始字符串并确认该数字不是质数。

4.2 打印每行所有字段的总和

perl -MList::Util=sum -alne 'print sum @F'

这个单行命令通过-a命令行选项打开字段自动分割,并通过-Mlist::Util=sum导入List::Util模块的sum函数。 (List::Util是 Perl 的核心模块之一,所以你不需要安装它。)自动分割默认发生在空白字符上,结果字段会被放入@F变量中。例如,行1 4 8会按空格分割,使得@F变成(1, 4, 8)sum @F语句将求和@F数组中的元素,得到13

-Mmodule=arg选项从module导入arg。它等同于写成

use module qw(arg);

这个单行命令相当于

use List::Util qw(sum);
while (<>) {
    @F = split(' ');
    print sum @F, "\n";
}

你可以通过为-F命令行开关指定一个参数来改变自动分割的默认行为。假设你有以下一行:

1:2:3:4:5:6:7:8:9:10

如果你希望找到所有这些数字的总和,可以简单地将:指定为-F开关的参数,像这样:

perl -MList::Util=sum -F: -alne 'print sum @F'

这将按冒号字符分割行并求和所有的数字。输出是55,因为这是从 1 到 10 的数字之和。

4.3 打印所有行中所有字段的总和

perl -MList::Util=sum -alne 'push @S,@F; END { print sum @S }'

这个单行命令不断将分割后的字段推送到@S数组中。当输入结束且 Perl 准备退出时,END { }代码块会被执行,并输出@S中所有项目的总和。这样会对所有行中的所有字段求和。

请注意,如何将@F数组推送到@S数组实际上是将元素附加到它。与许多其他编程语言不同,推送数组 1 到数组 2 会将数组 1 放入数组 2 中,而不是将数组 1 的元素附加到数组 2 中。Perl 默认执行列表展平。

不幸的是,使用这个方法对所有行的所有字段求和会创建一个庞大的@S数组。一个更好的解决方法是只保留运行时的和,像这样:

perl -MList::Util=sum -alne '$s += sum @F; END { print $s }'

在这里,每一行被分割成@F,然后将值求和并存储在运行时的和变量$s中。一旦所有输入被处理完,单行命令会打印出$s的值。

4.4 将每一行的所有字段洗牌

perl -MList::Util=shuffle -alne 'print "@{[shuffle @F]}"'

这个单行命令最棘手的部分是@{[shuffle @F]}构造。这个构造允许你执行引号内的代码。通常,文本和变量会放入引号中,但使用@{[ ... ]}构造,你也可以运行代码。

在这个单行命令中,执行的代码是shuffle @F,它对字段进行洗牌并返回洗牌后的列表。[shuffle @F]创建了一个包含洗牌字段的数组引用,@{ ... }则是解除引用。你只需创建一个引用并立即解除引用。这使得你能够执行引号内的代码。

让我们看几个例子,理解为什么我选择在引号内执行代码。如果我写了print shuffle @F,那么行中的字段会被连接起来。对比一下这个单行命令的输出:

$ echo a b c d | perl -MList::Util=shuffle -alne 'print "@{[shuffle @F]}"'
b c d a

转换成:

$ echo a b c d | perl -MList::Util=shuffle -alne 'print shuffle @F'
bcda

在第一个例子中,打乱顺序的字段数组(双引号内)会被插值,并且数组元素用空格分隔,因此输出为b c d a。在第二个例子中,插值没有发生,Perl 会逐个输出元素,而不进行分隔,输出为bcda

你可以使用$这个特殊变量来改变打印时数组元素之间的分隔符。例如,当我将分隔符更改为冒号时,会发生以下情况:

$ echo a b c d | perl -MList::Util=shuffle -alne '$,=":"; print shuffle @F'
b:c:d:a

你还可以使用join函数通过空格连接@F中的元素:

perl -MList::Util=shuffle -alne 'print join " ", shuffle @F'

在这里,join函数使用给定的分隔符连接数组的元素,而@{[ ... ]}结构是实现这一操作最简洁的方式。

4.5 找出每行中数值最小的元素(最小元素)

perl -MList::Util=min -alne 'print min @F'

这个一行代码与前面的例子有些相似。它使用了List::Util中的min函数。一旦通过-a自动拆分行并将元素存入@F数组,min函数就能找到数值最小的元素并打印出来。

例如,如果你有一个文件包含以下行:

-8  9  10 5
7   0  9  3
5  -25 9  999

运行这行代码会产生以下输出:

-8
0
-25

第一行的最小数字是-8;第二行的最小数字是0;第三行的最小数字是-25

4.6 找出所有行中数值最小的元素(最小元素)

perl -MList::Util=min -alne '@M = (@M, @F); END { print min @M }'

这行代码结合了 4.3 和 4.5 的代码。@M = (@M, @F)结构等同于push @M, @F。它将@F的内容附加到@M数组中。

这个一行代码将所有数据存储在内存中,如果你在一个非常大的文件上运行它,Perl 可能会耗尽内存。最好的方法是找出每一行的最小元素,并将该元素与前一行的最小元素进行比较。如果当前行的元素小于前一行的元素,那么它就是目前为止最小的元素。处理完所有行后,你可以通过END块打印出找到的最小元素:

perl -MList::Util=min -alne '
  $min = min @F;
  $rmin = $min unless defined $rmin && $min > $rmin;
  END { print $rmin }
'

在这里,你首先找到当前行的最小元素并将其存储在$min中。然后检查当前行的最小元素是否是迄今为止最小的元素。如果是,就将它赋值给$rmin。当你遍历完所有行后,END块会执行并打印出$rmin

假设你的文件包含以下行:

-8  9  10 5
7   0  9  3
5  -25 9  999

运行这行代码会输出-25,因为这是文件中最小的数字。

如果你使用的是 Perl 5.10 或更高版本,你可以通过以下一行代码实现相同的功能:

perl -MList::Util=min -alne '$min = min($min // (), @F); END { print $min }'

这个单行代码使用了 // 运算符,这是 Perl 5.10 新增的运算符。这个运算符类似于逻辑 OR 运算符 (||),不同之处在于它测试的是左边的定义性而非真假值。这意味着它测试左边是否已定义,而不是是否为真或假。在这个单行代码中,表达式 $min // () 如果 $min 已定义,则返回 $min,否则返回一个空列表 ()// 运算符避免了你使用 defined 来测试定义性。

考虑当这个单行代码在前面的文件上运行时会发生什么。首先,Perl 读取行 -8 9 10 5,将其拆分并将数字放入 @F 数组中。此时,@F 数组变为 (-8, 9, 10, 5)。接着,它执行 $min = min ($min // (), @F)。由于 $min 尚未定义,$min // () 解析为 (),所以整个表达式变为 $min = min ((), (-8, 9, 10, 5))

Perl 设计上就支持列表扁平化,因此在将参数传递给 min 函数时,表达式变成了 $min = min(-8, 9, 10, 5)。这定义了 $min,并将其设置为 -8。Perl 继续执行下一行,在这一行中,它将 @F 设置为 (7, 0, 9, 3),然后再次求值 $min = min($min // (), @F)。因为 $min 已经定义,所以 $min // () 会被解析为 $min,表达式变成 $min = min(-8, 7, 0, 9, 3)。此时,-8 仍然是最小的元素,所以 $min 保持为 -8。最后,Perl 读取最后一行,执行 $min = min(-8, 5, -25, 9, 999),发现 -25 是文件中的最小元素。

4.7 找到每行的数值最大元素(最大元素)

perl -MList::Util=max -alne 'print max @F'

这与单行代码 4.5 的作用相同,唯一不同的是将 min 替换为 max

4.8 找到所有行的数值最大元素(最大元素)

perl -MList::Util=max -alne '@M = (@M, @F); END { print max @M }'

这个单行代码与单行代码 4.6 和 4.7 类似。在这个单行代码中,每一行都会自动拆分并放入 @F 数组中,然后该数组会与 @M 数组合并。当输入处理完毕后,END 块执行,并打印出最大元素。

这是另一种找到最大元素的方法,它只保留当前的最大元素,而不是将所有元素保存在内存中:

perl -MList::Util=max -alne '
  $max = max @F;
  $rmax = $max unless defined $rmax && $max < $rmax;
  END { print $rmax }
'

如果你使用的是 Perl 5.10 或更高版本,可以使用 // 运算符来简化这个单行代码:

perl -MList::Util=max -alne '$max = max($max // (), @F); END { print $max }'

这与单行代码 4.6 相同,唯一不同的是将 min 替换为 max

4.9 将每个字段替换为其绝对值

perl -alne 'print "@{[map { abs } @F]}"'

这个单行代码首先使用 -a 选项自动拆分该行。拆分后的字段会存储在 @F 变量中。接着,它使用 map 函数对每个字段调用绝对值函数 abs。本质上,map 函数会将给定的函数应用到列表的每个元素,并返回一个包含应用该函数结果的新列表。例如,如果列表 @F(-4, 2, 0),对其进行 abs 映射后会得到列表 (4, 2, 0)。最后,这个单行代码会打印出新列表中的正值。

@{[ ... ]} 结构,介绍于一行代码 4.4,允许你执行引号内的代码。

4.10 打印每行的字段总数

perl -alne 'print scalar @F'

这个一行代码强制在标量上下文中求值 @F,在 Perl 中这意味着“@F 中的元素数量”。因此,它打印每行中的元素数量。

例如,如果你的文件包含以下几行:

foo bar baz
foo bar
baz

运行这个一行代码将产生以下输出:

3
2
1

第一行有三个字段,第二行有两个字段,最后一行有一个字段。

4.11 打印每行的字段总数,后跟该行内容

perl -alne 'print scalar @F, " $_"'

这个一行代码和一行代码 4.10 相同,只是在末尾添加了 $_,它打印整行内容。(记住,-n 会将每一行放入 $_ 变量中。)

让我们在和一行代码 4.10 相同的示例文件上运行这个一行代码:

foo bar baz
foo bar
baz

运行这个一行代码将产生以下输出:

3 foo bar baz
2 foo bar
1 baz

4.12 打印所有行的字段总数

perl -alne '$t += @F; END { print $t }'

在这里,这行代码将每行的字段数累加到变量 $t 中,直到所有行都被处理完。接下来,它打印结果,结果包含所有行的单词数量。注意,你将 @F 数组添加到标量变量 $t 中。由于 $t 是标量,@F 数组在标量上下文中求值并返回它所包含的元素数量。

在以下文件上运行这个一行代码:

foo bar baz
foo bar
baz

输出数字 6,因为该文件总共包含六个单词。

4.13 打印匹配模式的字段总数

perl -alne 'map { /*regex*/ && $t++ } @F; END { print $t || 0 }'

这个一行代码使用 map@F 数组中的每个元素应用操作。在这个示例中,操作检查每个元素是否匹配 /regex/,如果匹配,则增加 $t 变量。然后它打印 $t 变量,其中包含匹配 /regex/ 模式的字段数量。$t || 0 结构是必要的,因为如果没有字段匹配,$t 将不存在,所以必须提供一个默认值。你可以提供任何其他默认值,甚至是字符串,而不是 0

循环会是一个更好的方法:

perl -alne '$t += /*regex*/ for @F; END { print $t }'

在这里,@F 中的每个元素都与 /regex/ 进行测试。如果匹配,/regex/ 返回真;否则返回假。当按数值使用时,真转化为 1,假转化为 0,因此 $t += /regex/ 会将 10 加到 $t 变量中。结果是,匹配的数量被计入 $t。在 END 块中打印结果时不需要默认值,因为无论字段是否匹配,+= 操作符都会执行。你总会得到一个值,有时这个值会是 0

另一种做法是使用在标量上下文中的 grep

perl -alne '$t += grep /*regex*/, @F; END { print $t }'

在这里,grep 返回匹配的数量,因为它在标量上下文中被求值。在列表上下文中,grep 返回所有匹配的元素,但在标量上下文中,它返回匹配元素的数量。这个数字会累计到 $t 中,并在 END 块中打印。在这种情况下,你不需要为 $t 提供默认值,因为 grep 在这些情况下会返回 0

4.14 打印匹配模式的行数

perl -lne '/*regex*/ && $t++; END { print $t || 0 }'

在这里,/regex/ 如果当前输入行匹配这个正则表达式,则评估为真。写 /regex/ && $t++ 就等同于写 if ($_ =~ /regex/) { $t++ },它会在行匹配指定模式时递增 $t 变量。在 END 块中,$t 变量包含模式匹配的总次数并被打印;但是如果没有匹配的行,$t 会再次未定义,因此你必须打印一个默认值。

4.15 打印数字 π

perl -Mbignum=bpi -le 'print bpi(21)'

bignum 包导出了 bpi 函数,用于按所需精度计算 π 常数。这个一行代码将 π 打印到 20 位小数。(注意,你需要指定 n+1 才能将其精确到 n 位。)

bignum 库还导出了常数 π,已预计算到 39 位小数:

perl -Mbignum=PI -le 'print PI'

4.16 打印数字 e

perl -Mbignum=bexp -le 'print bexp(1,21)'

bignum 库导出了 bexp 函数,它接受两个参数:要将 e 提高的幂数和所需的精度。这个一行代码将常数 e 打印到 20 位小数。

例如,你可以打印 e² 的值,精确到 30 位小数:

perl -Mbignum=bexp -le 'print bexp(2,31)'

与 π 相似,bignum 也导出了常数 e,并已预计算到 39 位小数:

perl -Mbignum=e -le 'print e'

4.17 打印 UNIX 时间(自 1970 年 1 月 1 日 00:00:00 UTC 起的秒数)

perl -le 'print time'

内置的 time 函数返回自纪元以来的秒数。这个一行代码仅打印当前时间。

4.18 打印格林威治标准时间和本地计算机时间

perl -le 'print scalar gmtime'

gmtime 函数是一个内置的 Perl 函数。当在标量上下文中使用时,它返回本地化为格林威治标准时间(GMT)的时间。

内置的 localtime 函数与 gmtime 类似,不同之处在于,当在标量上下文中使用时,它返回计算机的本地时间:

perl -le 'print scalar localtime'

在列表上下文中,gmtimelocaltime 都返回一个包含九个元素的列表(UNIX 程序员称之为 struct tm),其中包含以下元素:

($second,             [0]
$minute,              [1]
$hour,                [2]
$month_day,           [3]
$month,               [4]
$year,                [5]
$week_day,            [6]
$year_day,            [7]
$is_daylight_saving   [8]
)

你可以 切片 这个列表(即从中提取元素),或者如果只需要其中的一部分信息,可以打印单个元素。例如,要打印 H:M:S,可以从 localtime 中切片元素 210,如下所示:

perl -le 'print join ":", (localtime)[2,1,0]'

要单独切片元素,可以指定一个要提取的元素列表,例如 [2,1,0]。或者按范围切片:

perl -le 'print join ":", (localtime)[2..6]'

这行代码打印小时、日期、月份、年份和星期几。

你也可以使用负索引从列表的另一端选择元素:

perl -le 'print join ":", (localtime)[-2, -3]'

这个一行代码打印元素 76,它们分别是年份中的第几天(例如,第 200 天)和星期几(例如,第 4 天)。

4.19 打印昨天的日期

perl -MPOSIX -le '
  @now = localtime;
  $now[3] -= 1;
  print scalar localtime mktime @now
'

记住,localtime返回一个包含九个元素的列表(参见一行代码 4.18),这些元素是各种日期信息。列表中的第四个元素是当前月的日期。如果从这个元素中减去 1,你将得到昨天的日期。

mktime函数根据这个修改过的九元素列表构建 UNIX 纪元时间,而scalar localtime结构打印出新的日期,即昨天。这行代码在一些极端情况下也能正常工作,比如当前日期是月初时。你需要POSIX包,因为它导出了mktime函数。

例如,如果现在是Mon May 20 05:49:55,运行这行代码将打印出Sun May 19 05:49:55

4.20 打印出 14 个月、9 天和 7 秒前的日期

perl -MPOSIX -le '
  @now = localtime;
  $now[0] -= 7;
  $now[3] -= 9;
  $now[4] -= 14;
  print scalar localtime mktime @now
'

这行代码修改了@now列表中的第一个、第四个和第五个元素。第一个元素是秒,第四个是天,第五个是月。mktime命令根据这个新的结构生成 UNIX 时间,而在标量上下文中计算的localtime则打印出 14 个月、9 天和 7 秒前的日期。

4.21 计算阶乘

perl -MMath::BigInt -le 'print Math::BigInt->new(5)->bfac()'

这行代码使用了Math::BigInt模块中的bfac()函数(即 Perl 核心自带,无需安装)。Math::BigInt->new(5)构造创建了一个值为5Math::BigInt对象,接着调用该对象的bfac()方法来计算5的阶乘。将5改为任何你想要的数字来找到它的阶乘。

计算阶乘的另一种方法是将 1 到n的数字相乘:

perl -le '$f = 1; $f *= $_ for 1..5; print $f'

在这里,我将$f设置为1,然后从1循环到5,并将$f乘以每个值。结果是1201*2*3*4*5),即 5 的阶乘。

4.22 计算最大公约数

perl -MMath::BigInt=bgcd -le 'print bgcd(@list_of_numbers)'

Math::BigInt有几个其他有用的数学函数,包括bgcd,它计算一组数字的最大公约数(gcd)。例如,要找到(20, 60, 30)的最大公约数,可以像这样执行这行代码:

perl -MMath::BigInt=bgcd -le 'print bgcd(20,60,30)'

要从文件或用户输入中计算最大公约数,可以使用-a命令行参数,并将@F数组传递给bgcd函数:

perl -MMath::BigInt=bgcd -anle 'print bgcd(@F)'

(我在一行代码 4.2 中解释了-a参数和@F数组,见第 30 页。)

你也可以使用欧几里得算法来找到$n$m的最大公约数。这行代码正是做了这件事,并将结果存储在$m中:

perl -le '
  $n = 20; $m = 35;
  ($m,$n) = ($n,$m%$n) while $n;
  print $m
'

欧几里得算法是最古老的求最大公约数算法之一。

4.23 计算最小公倍数

最小公倍数(lcm)函数blcm包含在Math::BigInt中。使用这行代码来找到(35, 20, 8)的最小公倍数:

perl -MMath::BigInt=blcm -le 'print blcm(35,20,8)'

要从包含数字的文件中找到最小公倍数,可以使用-a命令行开关和@F数组:

perl -MMath::BigInt=blcm -anle 'print blcm(@F)'

如果你知道一些数论知识,可能会记得最大公约数和最小公倍数之间是有关系的。给定两个数字$n$m,你知道它们的最小公倍数是$n*$m/gcd($n,$m)。因此,这行代码如下所示:

perl -le '
  $a = $n = 20;
  $b = $m = 35;
  ($m,$n) = ($n,$m%$n) while $n;
  print $a*$b/$m
'

4.24 生成 5 到 15 之间(不包括 15)的 10 个随机数

perl -le 'print join ",", map { int(rand(15-5))+5 } 1..10'

这行代码打印了 10 个介于 5 和 15 之间的随机数。看起来可能很复杂,但其实很简单。int(rand(15-5))其实就是int(rand(10)),它返回一个从 0 到 9 之间的随机整数。加上5后,它返回一个从 5 到 14 之间的随机整数。范围1..10使得它生成 10 个随机整数。

你也可以将这个一行代码写得更详细一些:

perl -le '
  $n=10;
  $min=5;
  $max=15;
  $, = " ";
  print map { int(rand($max-$min))+$min } 1..$n;
'

在这里,所有的变量都更为明确。要修改这个一行代码,可以更改变量$n$min$max$n变量表示要生成多少个随机数,而$min-$max是生成随机数时的范围。

$变量被设置为空格,因为它是print的输出字段分隔符,默认值是undef。如果你没有将$设置为空格,数字就会被连接在一起打印。(参见第 32 页的一行代码 4.4,讨论了$的用法。)

4.25 生成列表的所有排列

perl -MAlgorithm::Permute -le '
  $l = [1,2,3,4,5];
  $p = Algorithm::Permute->new($l);
  print "@r" while @r = $p->next
'

这行代码使用了Algorithm::Permute模块的面向对象接口来找到列表的所有排列,也就是所有重新排列项的方式。Algorithm::Permute的构造函数接收一个元素数组的引用,用于排列。在这个特定的一行代码中,元素是数字1, 2, 3, 4, 5

next方法返回下一个排列。反复调用它可以遍历所有排列,每个排列都会被放入@r数组中并打印出来。(注意:输出的排列列表会迅速变大。对于一个包含n个元素的列表,有n!n的阶乘)个排列。)

打印所有排列的另一种方式是使用permute子例程:

perl -MAlgorithm::Permute -le '
  @l = (1,2,3,4,5);
  Algorithm::Permute::permute { print "@l" } @l
'

如果你将@l更改为仅包含三个元素(1, 2, 3)并运行它,得到的结果如下:

1 2 3
1 3 2
3 1 2
2 1 3
2 3 1
3 2 1

4.26 生成幂集

perl -MList::PowerSet=powerset -le '
  @l = (1,2,3,4,5);
  print "@$_" for @{powerset(@l)}
'

这行代码使用了来自 CPAN 的List::PowerSet模块。该模块导出了powerset函数,它接收一个元素列表并返回一个数组引用,该数组包含指向子集数组的引用。你可以通过在命令行运行cpan List::PowerSet来安装此模块。

for循环中,你调用powerset函数并将@l的元素列表传递给它。接下来,你取消引用powerset的返回值,它是一个包含子集的数组引用,然后再取消引用每个子集@$_并打印出来。

幂集是所有子集的集合。对于一个包含n个元素的集合,幂集中恰好有 2^(n)个子集。这里是(1, 2, 3)的幂集示例:

1 2 3
2 3
1 3
3
1 2
2
1

4.27 将 IP 地址转换为无符号整数

perl -le '
  $i=3;
  $u += ($_<<8*$i--) for "127.0.0.1" =~ /(\d+)/g;
  print $u
'

这行代码将 IP 地址127.0.0.1转换为一个无符号整数,方法是首先对 IP 地址进行全局匹配(\d+)。执行for循环遍历全局匹配时,会迭代所有的匹配项,这些匹配项就是 IP 地址的四个部分:127001

接下来,匹配结果被加总到$u变量中。第一个位移 8 × 3 = 24 位,第二个位移 8 × 2 = 16 位,第三个位移 8 位。最后一个直接加到$u中。得到的整数恰好是2130706433(一个非常极客的数字)。

这里有更多的单行代码:

perl -le '
  $ip="127.0.0.1";
  $ip =~ s/(\d+)\.?/sprintf("%02x", $1)/ge;
  print hex($ip)
'

这行代码利用了127.0.0.1可以很容易转换为十六进制的事实。在这里,$ip(\d+)匹配,每个 IP 部分都通过sprintf("%02x", $1)s操作符内转换为十六进制数字。s操作符的/e标志使得替换部分作为 Perl 表达式进行求值。结果,127.0.0.1被转换为7f000001,然后通过 Perl 的hex操作符将其解释为十六进制数,并转换为十进制数。

你还可以使用unpack

perl -le 'print unpack("N", 127.0.0.1)'

这行代码可能是尽可能简短的了。它使用了vstring 字面量(版本字符串)来表示 IP 地址。vstring 是由指定的序号值组成的字符串字面量。新形成的字符串字面量被解包为网络字节顺序(大端顺序)的数字,并被打印出来。

如果你有一个包含 IP 的字符串(而不是 vstring),你首先需要使用inet_aton函数将其转换为字节形式:

perl -MSocket -le 'print unpack("N", inet_aton("127.0.0.1"))'

在这里,inet_aton将字符串127.0.0.1转换为字节形式(等同于纯粹的 vstring 127.0.0.1),然后unpack将其解包,就像前面的单行代码那样。

4.28 将无符号整数转换为 IP 地址

perl -MSocket -le 'print inet_ntoa(pack("N", 2130706433))'

在这里,整数2130706433被打包成大端字节顺序的数字,然后传递给inet_ntoa函数,该函数将数字转换回 IP 地址。(注意,inet_ntoainet_aton的反向操作。)

你也可以这样做:

perl -le '
  $ip = 2130706433;
  print join ".", map { (($ip>>8*($_))&0xFF) } reverse 0..3
'

在这里,$ip首先向右移动 24 位,然后与0xFF进行按位与运算,生成 IP 的第一部分,即127。接着,向右移动 16 位并与0xFF按位与,得到0,然后再向右移动 8 位并与0xFF按位与,得到另一个0。最后,整个数字与0xFF按位与,得到1

map { ... }的结果是一个列表(127, 0, 0, 1)。这个列表现在通过点号"."连接,生成 IP 地址127.0.0.1

你可以用特殊变量$替代join,它在print语句中充当值分隔符:

perl -le '
  $ip = 2130706433;
  $, = ".";
  print map { (($ip>>8*($_))&0xFF) } reverse 0..3
'

因为reverse 0..33,2,1,0相同,所以你也可以写成:

perl -le '
  $ip = 2130706433;
  $, = ".";
  print map { (($ip>>8*($_))&0xFF) } 3,2,1,0
'

第五章:处理数组和字符串

在本章中,我们将看一些用于创建字符串和数组的单行命令,用于做一些事情,比如生成密码、创建特定长度的字符串、查找字符的数值以及创建数字数组。你还将学习范围操作符..x操作符、$特殊变量以及@ARGV数组。

5.1 生成并打印字母表

perl -le 'print a..z'

这个单行命令打印了从az的所有字母,结果为abcdefghijklmnopqrstuvwxyz。字母是通过范围操作符..生成的,当它在列表上下文中用于字符串时(这里由print提供),它应用了神奇的自动增量算法,将字符串推进到下一个字符。因此,在这个单行命令中,范围a..z的自动增量算法产生了从az的所有字母。

我确实把这个单行命令做得很简洁。如果我使用了strict,它就无法工作,因为裸词az。这个版本在语义上更正确:

perl -le 'print ("a".."z")'

记住,范围操作符..会生成一个值的列表。如果你愿意,你可以通过设置$特殊变量来打印这些值,并用逗号分隔:

perl -le '$, = ","; print ("a".."z")'

$是字段分隔符。它在每个字段之间由print输出。然而,从语义上讲,使用join将字母列表用逗号分隔会更具吸引力,因为即使不直接使用print,它也能正常工作:

perl -le '$alphabet = join ",", ("a".."z"); print $alphabet'

这里,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

5.2 生成并打印从“a”到“zz”的所有字符串

perl -le 'print join ",", ("a".."zz")'

这个单行命令再次使用了范围操作符..,但这一次,它不像之前的单行命令那样停在z。相反,它将z前进一个字符,产生aa。然后继续进行,产生abac,依此类推,直到到达az。此时,它将字符串推进到ba,继续生成bbbc,依此类推,直到最终达到zz

你还可以通过这样做来生成所有从aazz的字符串:

perl -le 'print join ",", "aa".."zz"'

这个单行命令的输出是:

aa, ab, ..., az, ba, bb, ..., bz, ca, ..., zz

5.3 创建十六进制查找表

@hex = (0..9, "a".."f")

在这个单行命令中,@hex数组包含了数字 0、1、2、3、4、5、6、7、8、9 以及字母 a、b、c、d、e、f。你可以使用这个数组将一个数字(在变量$num中)从十进制转换为十六进制,使用以下的进制转换公式。(这不是一个单行命令,我加入它是为了说明如何使用@hex查找数组。)

perl -le '
  $num = 255;
  @hex = (0..9, "a".."f");
  while ($num) {
    $s = $hex[($num % 16)].$s;
    $num = int $num/16;
  }
  print $s
'

但是,当然,如果我使用printf(或sprintf)并使用%x格式说明符,转换数字为十六进制会更容易。

perl -le 'printf("%x", 255)'

要将数字从十六进制转换回十进制,使用hex操作符:

perl -le '$num = "ff"; print hex $num'

hex操作符接受一个十六进制字符串(可以以0x开头或不以0x开头),并将其转换为十进制。

5.4 生成一个随机的八字符密码

perl -le 'print map { ("a".."z")[rand 26] } 1..8'

在这里,map 操作符执行代码 ("a".."z")[rand 26] 八次,因为它会遍历范围 1..8。在每次迭代中,代码会从字母表中随机选择一个字母。当 map 完成迭代后,它会返回生成的字符列表,print 会打印它,从而将所有字符连接起来。

若要在密码中包含数字,可以将 0..9 添加到字符列表中,并将 26 改为 36,因为现在有 36 种可能的字符:

perl -le 'print map { ("a".."z", 0..9)[rand 36] } 1..8'

如果你需要更长的密码,可以将 1..8 改为 1..20,生成一个 20 个字符长的密码。

5.5 创建特定长度的字符串

perl -le 'print "a"x50'

这个单行代码创建了一个由 50 个字母 a 组成的字符串并打印出来。操作符 x 是重复操作符。在这里,字母 ax50 重复 50 次。这个单行代码在你需要为调试或其他任务生成特定数据时非常有用。例如,如果你需要 1KB 的数据,只需执行以下操作:

perl -e 'print "a"x1024'

我去掉了 -l 参数,因为它会输出一个额外的换行符,导致数据量为 1025 字节。

当你在列表上下文中使用重复操作符,并且将一个列表作为第一个操作数时,你会创建一个包含给定元素重复的列表,像这样:

perl -le '@list = (1,2)x20; print "@list"'

这个单行代码创建了一个包含 20 次 (1, 2) 重复的列表,形如 (1, 2, 1, 2, 1, 2, ...)。(x 左侧的括号表示一个列表。)

5.6 从字符串创建数组

@months = split ' ', "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"

这里,@months 被填充了包含月份名称的字符串中的值。由于所有月份名称由空格分隔,split 操作符将它们分割并放入 @months 中。因此,$months[0] 包含 Jan$months[1] 包含 Feb,……,$months[11] 包含 Dec

你也可以使用 qw/.../ 操作符做同样的事情:

@months = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/

qw/.../ 操作符接受一个由空格分隔的字符串,并创建一个数组,其中每个单词都是数组的一个元素。

虽然这本身不是一个单行代码,但这是一个有用的、符合习惯的方式来创建数组,写单行代码时会很有帮助。

5.7 从命令行参数创建字符串

perl -le 'print "(", (join ",", @ARGV), ")"' *val1 val2 val3*

这个单行代码使用了 @ARGV 数组,包含了所有传递给 Perl 的参数。在这个单行代码中,传递给 Perl 的值是 val1val2val3,所以 @ARGV 包含了字符串 val1val2val3。这个单行代码打印出字符串 (val1,val2,val3),例如,可以用来生成 SQL 查询。

如果你熟悉 SQL 中的 INSERT 查询,你会知道其最基本的形式是 INSERT INTO table VALUES (val1, val2, val3, ...)。如你所见,这个单行代码生成了 SQL 查询中的 VALUES 部分。

你可以轻松修改这个单行代码来打印整个 INSERT 查询:

perl -le '
  print "INSERT INTO table VALUES (", (join ",", @ARGV), ")"
' val1 val2 val3

下面是这个单行代码的输出:

INSERT INTO *table* VALUES (*val1,val2,val3*)

5.8 查找字符串中字符的数值

perl -le 'print join ", ", map { ord } split //, "hello world"'

这个单行命令将字符串 "hello world" 拆分成一个字符列表,使用 split //, "hello world"。然后它将 ord 操作符映射到每个字符上,返回每个字符的数字值。最后,所有的数字值通过逗号连接并打印出来。输出如下:

104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100

你也可以使用 unpack 操作符,通过指定 C* 作为解包模板来实现:

perl -le 'print join ", ", unpack("C*", "hello world")'

模板中的 C 代表“无符号字符”,而 * 代表“所有字符”。

要查找字符的十六进制值,你可以这样做:

perl -le '
  print join ", ", map { sprintf "0x%x", ord $_ } split //, "hello world"
'

在这里,map 操作符对每个字符执行 sprintf "0x%x", ord $_,返回字符的十六进制值,并加上前缀 '0x'。输出如下:

0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64

同样,要获取字符的八进制值,你可以这样做:

perl -le '
  print join ", ", map { sprintf "%o", ord $_ } split //, "hello world"
'

输出如下:

150, 145, 154, 154, 157, 40, 167, 157, 162, 154, 144

最后,为了生成以 0 开头的正确八进制值,你可以在 sprintf 函数中指定 %#o 格式:

perl -le '
  print join ", ", map { sprintf "%#o", ord $_ } split //, "hello world"
'

下面是输出结果:

0150, 0145, 0154, 0154, 0157, 040, 0167, 0157, 0162, 0154, 0144

5.9 将一系列数字 ASCII 值转换为字符串

perl -le '
  @ascii = (99, 111, 100, 105, 110, 103);
  print pack("C*", @ascii)
'

就像我在前面的单行命令中使用 C* 模板将字符串解包成一个值的列表一样,我也可以使用相同的模板将它们打包成一个字符串。单行命令的输出如下:

coding

将一系列数字 ASCII 值转换为字符串的另一种方法是使用 chr 操作符,它接受代码点值并返回对应的字符:

perl -le '
  @ascii = (99, 111, 100, 105, 110, 103);
  $str = join "", map chr, @ascii;
  print $str
'

在这里,你只需将 chr 操作符映射到 @ascii 数组中的每个数字值上,这样就会生成一个与这些数字值对应的字符列表。接下来,你将字符连接在一起生成 $str,然后打印出来。

你还可以对这个单行命令进行简化,得到以下代码:

perl -le 'print map chr, 99, 111, 100, 105, 110, 103'

你还可以使用 @ARGV 数组,将 ASCII 值作为参数传递给单行命令:

perl -le 'print map chr, @ARGV' 99 111 100 105 110 103

5.10 生成一个包含 1 到 100 之间的奇数的数组

perl -le '@odd = grep {$_ % 2 == 1} 1..100; print "@odd"'

这个单行命令生成了一个从 1 到 99 的奇数数组(即 1, 3, 5, 7, 9, 11, … , 99)。它使用 grep 对列表 1..100 中的每个元素进行代码 $_ % 2 == 1 的评估,仅返回那些评估结果为真的元素。在这个例子中,代码检查除以 2 的余数是否为 1。如果是,那么该数字是奇数,并被放入 @odd 数组。

你也可以通过利用奇数的最低有效位被设置的特点,并测试最低有效位来编写这段代码:

perl -le '@odd = grep { $_ & 1 } 1..100; print "@odd"'

表达式 $_ & 1 用于隔离最低有效位,grep 仅选择那些最低有效位被设置的数字——也就是所有奇数。

5.11 生成一个包含 1 到 100 之间偶数的数组

perl -le '@even = grep {$_ % 2 == 0} 1..100; print "@even"'

这个单行命令几乎与 5.10 中的命令相同,唯一的区别是 grep 测试条件是“这个数字是否为偶数(除以 2 后余数为 0)?”

5.12 查找字符串的长度

perl -le 'print length "one-liners are great"'

length 子例程用于查找字符串的长度。

5.13 查找数组中的元素个数

perl -le '@array = ("a".."z"); print scalar @array'

在标量上下文中评估数组会返回数组的元素个数。

你也可以通过在数组的最后一个索引上加1来做到这一点:

perl -le '@array = ("a".."z"); print $#array + 1'

这里,$#array返回@array中的最后一个索引。因为这个数字比数组中元素的数量少 1,所以你可以在结果上加上1来找到数组中的元素总数。

例如,假设你想找出当前目录中有多少个文本文件。你可以使用@ARGV并将*.txt通配符传递给 Perl。Shell 将*.txt通配符扩展为匹配*.txt的文件名列表,然后 Perl 将它们放入@ARGV数组,并在标量上下文中打印该数组。输出将是当前目录中文本文件的数量:

perl -le 'print scalar @ARGV' *.txt

如果你的 Shell 不支持文件名扩展(也称为通配符匹配),或者你使用的是 Windows,你可以使用钻石操作符与*.txt参数:

perl -le 'print scalar (@ARGV=<*.txt>)'

在这种情况下,钻石操作符会进行通配符匹配,并返回一个匹配*.txt的文件名列表。在标量上下文中评估这个列表会返回匹配的文件数量。

第六章 文本转换与替换

本章将介绍各种单行命令,这些命令可以更改、转换和替换文本,包括 base64 编码和解码、URL 转义和反转义、HTML 转义和反转义、文本大小写转换以及反转行。你还将了解 ytruclcreverse 操作符及字符串转义序列。

6.1 对字符串进行 ROT13 编码

perl -le '$string = "*bananas*"; $string =~ y/A-Za-z/N-ZA-Mn-za-m/; print $string'

这个单行命令使用 y 操作符(也叫做 tr 操作符)来执行 ROT13 编码。ytr 操作符执行字符串转译。给定 y/search/replace/y 操作符将 search 列表中找到的所有字符转译为 replace 列表中对应位置的字符。ytr 操作符通常会被误认为接受正则表达式,但它们并不接受。它们是进行字符转译,并接受 searchreplace 部分中的字符列表。

在这个单行命令中,A-Za-z 会创建以下字符列表:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

N-ZA-Mn-za-m 会创建以下列表:

NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm

注意,在第二个列表中,大写字母和小写字母的偏移量为 13 个字符。现在,y 操作符将第一个列表中的每个字符转换为第二个列表中的字符,从而执行 ROT13 操作。(关于 ROT13 的一个有趣事实是,应用两次 ROT13 操作会产生相同的字符串;也就是说,ROT13(ROT13(string)) 等于 string。)

要对整个文件 bananas.txt 进行 ROT13 编码并将结果打印到屏幕上,只需执行以下操作:

perl -lpe 'y/A-Za-z/N-ZA-Mn-za-m/' *bananas*.txt

你还可以使用 Perl 的 -i 参数对文件进行原地替换。例如,要对 oranges.txt 进行 ROT13 编码并直接修改文件,请执行以下命令:

perl -pi.bak -e 'y/A-Za-z/N-ZA-Mn-za-m/' *oranges*.txt

这个单行命令首先创建一个名为 oranges.txt.bak 的备份文件,然后用 ROT13 编码的文本替换 oranges.txt 的内容。-i 命令的 .bak 部分创建了备份文件。如果你对结果非常确信,可以省略 .bak 部分,但我建议始终使用 -i.bak,因为有一天你可能会犯错,弄乱一个重要的文件。(我说的是亲身经历。)

6.2 对字符串进行 Base64 编码

perl -MMIME::Base64 -e 'print encode_base64("*string*")'

这个单行命令使用了 MIME::Base64 模块。它导出了 encode_base64 函数,该函数接受一个字符串并返回其 base64 编码版本。

要对整个文件进行 base64 编码,请使用以下命令:

perl -MMIME::Base64 -0777 -ne 'print encode_base64($_)' *file*

在这里,-0777 参数与 -n 一起使用,导致 Perl 将整个文件加载到 $_ 变量中。接着,文件被 base64 编码并打印出来。(如果 Perl 没有加载整个文件,它将逐行进行编码,结果会一团糟。)

6.3 对字符串进行 Base64 解码

perl -MMIME::Base64 -le 'print decode_base64("*base64string*")'

MIME::Base64 模块还导出了 decode_base64 函数,该函数接受一个 base64 编码的字符串并进行解码。

整个文件也可以通过类似的方式进行解码:

perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' *file*

6.4 对字符串进行 URL 转义

perl -MURI::Escape -le 'print uri_escape("*[`example.com`](http://example.com)*")'

要使用这行代码,首先需要安装 URI::Escape 模块,可以通过在命令行输入 cpan URI::Escape 来安装。该模块导出了两个函数:uri_escapeuri_unescape。第一个函数执行 URL 转义(有时称为 URL 编码),另一个执行 URL 解转义(或 URL 解码)。现在,要进行 URL 转义,只需调用 uri_escape($string),就完成了!

这行代码的输出是 http%3A%2F%2Fexample.com

6.5 对字符串进行 URL 解转义

perl -MURI::Escape -le 'print uri_unescape("*http%3A%2F%2Fexample.com*")'

这行代码使用了来自URI::Escape模块的uri_unescape函数来执行 URL 解转义。它解转义之前的代码输出,逆转该操作。

这行代码的输出是 http://example.com

6.6 对字符串进行 HTML 编码

perl -MHTML::Entities -le 'print encode_entities("*<html>*")'

这行代码使用了来自HTML::Entities模块的encode_entities函数来编码 HTML 实体。例如,你可以将 <> 转换为 &lt;&gt;

6.7 对字符串进行 HTML 解码

perl -MHTML::Entities -le 'print decode_entities("*&lt;html&gt;*")'

这行代码使用了来自HTML::Entities模块的decode_entities函数。例如,你可以将 &lt;&gt; 转换回 <>

6.8 将所有文本转换为大写

perl -nle 'print uc'

这行代码使用了 uc 函数,默认情况下它作用于 $_ 变量,并返回它包含的文本的大写版本。

你也可以使用 -p 命令行选项,它启用了 $_ 变量的自动打印并对其进行就地修改:

perl -ple '$_ = uc'

或者,你可以将 \U 转义序列应用到字符串插值中:

perl -nle 'print "\U$_"'

这行代码会将其后的所有内容(或者直到第一次出现 \E 为止)转换为大写。

6.9 将所有文本转换为小写

perl -nle 'print lc'

这行代码与前一行类似,lc 函数将 $_ 的内容转换为小写。

你也可以使用转义序列 \L 和字符串插值:

perl -nle 'print "\L$_"'

在这里,\L 会将其后的所有内容转换为小写(或者直到第一次出现 \E 为止)。

6.10 只将每行的第一个字母转换为大写

perl -nle 'print ucfirst lc'

这行代码首先通过lc函数将输入转换为小写,然后使用ucfirst只将第一个字符转换为大写。例如,如果你传入一行文本 foo bar baz,它会输出 Foo bar baz。类似地,如果传入一行 FOO BAR BAZ,它首先将整行转换为小写,然后再将第一个字母转为大写,最终输出 Foo bar baz

你可以使用转义码和字符串插值来做同样的事情:

perl -nle 'print "\u\L$_"'

首先,\L将整行转换为小写,然后\u将第一个字符转换为大写。

6.11 反转字母的大小写

perl -ple 'y/A-Za-z/a-zA-Z/'

这行代码会改变字母的大小写:大写字母变为小写字母,小写字母变为大写字母。例如,文本 Cows are COOL 会变成 cOWS ARE cool。转写操作符 y(在第 59 页的 6.1 一行解释)创建了一个从大写字母 A-Z 到小写字母 a-z 的映射,以及从小写字母 a-z 到大写字母 A-Z 的映射。

6.12 将每行转换为标题式大小写

perl -ple 's/(\w+)/\u$1/g'

这一行代码尝试将字符串转换为标题大小写,意思是每个单词的第一个字母都大写;例如,This Text Is Written In Title Case。这行代码通过匹配每个单词\w+,并用\u$1替换匹配的单词,从而将单词的第一个字母大写。

6.13 删除每行开头的空白字符(空格、制表符)

perl -ple 's/^[ \t]+//'

这一行代码利用替换操作符s删除每行开头的所有空白字符。s/regex/replace/表示将匹配的regex替换为replace字符串。在这个例子中,regex^[ \t]+,意思是“匹配字符串开头的一个或多个空格或制表符”,而replace为空,意味着“将匹配的部分替换为空字符串”。

正则表达式类[ \t]也可以用\s+替换,以匹配任何空白字符(包括制表符和空格):

perl -ple 's/^\s+//'

6.14 删除每行末尾的空白字符(空格、制表符)

perl -ple 's/[ \t]+$//'

这一行代码删除每行末尾的所有空白字符。s操作符的正则表达式表示“匹配字符串末尾的一个或多个空格或制表符”。replace部分为空,这意味着“删除匹配的空白字符”。

你也可以通过写成以下形式来实现相同的效果:

perl -ple 's/\s+$//'

在这里,你可以用\s+替换[ \t]+$,就像在单行代码 6.13 中一样。

6.15 删除每行开头和结尾的空白字符(空格、制表符)

perl -ple 's/^[ \t]+|[ \t]+$//g'

这一行代码结合了单行代码 6.13 和 6.14。它为s操作符指定了全局/g标志,因为你希望它删除字符串开头结尾的空白字符。如果不指定这个标志,它只会删除开头的空白(如果有空白)或结尾的空白(如果开头没有空白)。

你也可以将[ \t]+$替换为\s+,得到相同的结果:

perl -ple 's/^\s+|\s+$//g'

\s+比写[ \t]+更简洁。而s代表空格,这使得它更容易记住。

6.16 将 UNIX 换行符转换为 DOS/Windows 换行符

perl -pe 's|\012|\015\012|'

这一行代码将 UNIX 换行符\012LF)替换为 Windows/DOS 换行符\015\012CRLF)每一行。s/regex/replace/的一个优点是,它可以使用除正斜杠以外的其他字符作为分隔符。这里,使用竖线分隔regexreplace,以提高可读性。

换行符通常表示为\n,回车符表示为\r,但在不同平台上,\n\r的含义可能有所不同。然而,UNIX 换行符始终可以表示为\012LF),而回车符表示为\rCR)。这就是为什么你使用这些数字代码:有时使用灵活的序列更可取,但在这里并不适用。

6.17 将 DOS/Windows 换行符转换为 UNIX 换行符

perl -pe 's|\015\012|\012|'

这一行代码的作用与单行代码 6.16 相反。它将 Windows 换行符(CRLF)转换为 UNIX 换行符(LF)。

6.18 将 UNIX 换行符转换为 Mac 换行符

perl -pe 's|\012|\015|'

Mac OS 以前使用\015CR)作为换行符。这个单行命令将 UNIX 的\012LF)转换为 Mac OS 的\015CR)。

6.19 在每一行中将“foo”替换为“bar”

perl -pe 's/foo/bar/'

这个单行命令使用s/regex/replace/命令,将每一行中第一次出现的foo替换为bar

要将所有的foo替换为bar,请添加全局/g标志:

perl -pe 's/foo/bar/g'

6.20 在匹配“baz”的行中将“foo”替换为“bar”

perl -pe '/baz/ && s/foo/bar/'

这个单行命令大致等价于

while (defined($line = <>)) {
  if ($line =~ /baz/) {
    $line =~ s/foo/bar/
  }
}

这个扩展的代码将每一行放入变量$line中,然后检查该变量中的行是否与baz匹配。如果匹配,则将该行中的foo替换为bar

你也可以这样写

perl -pe 's/foo/bar/ if /baz/'

6.21 逆序打印段落

perl -00 -e 'print reverse <>' *file*

这个单行命令使用了在单行命令 2.7(第 14 页)中讨论的-00参数,启用段落吸取模式,意味着 Perl 按段落读取文本,而不是按行读取。接着,它使用<>运算符让 Perl 从标准输入或指定的文件中读取输入。这里,我指定了file作为参数,因此 Perl 将按段落读取file(得益于-00)。一旦 Perl 读取完文件,它会将所有段落作为一个列表返回,并调用reverse来反转段落列表的顺序。最后,print打印反转后的段落列表。

6.22 逆序打印所有行

perl -lne 'print scalar reverse $_'

这个单行命令在标量上下文中评估reverse运算符。在前面的单行命令中,你看到在列表上下文中评估reverse会反转整个列表,也就是元素的顺序。要对像$_这样的标量值(包含整行内容)执行相同的操作,你必须在标量上下文中调用reverse。否则,它只会反转一个包含单个元素的列表,也就是同样的列表!完成这个操作后,你只需打印反转后的行。

通常,在使用运算符时,你可以省略$_变量,Perl 仍然会在$_变量上应用该函数。换句话说,你可以将相同的单行命令重写为

perl -lne 'print scalar reverse'

或者你可以将-n替换为-p,修改$_变量,并将其值设置为反转:

perl -lpe '$_ = reverse $_'

你也可以这样写

perl -lpe '$_ = reverse'

这里,$_被省略了,因为大多数 Perl 运算符在没有给定参数时,默认使用$_

6.23 逆序打印列

perl -alne 'print "@{[reverse @F]}"'

这个单行命令反转文件中列的顺序。-a命令行参数将每一行按空格分割成列,并将它们放入@F数组,然后反转并打印出来。这个单行命令类似于第 32 页上的单行命令 4.4;我在那里解释了@{[ ... ]}构造。它简单地让你在双引号内运行代码。例如,给定以下输入文件:

one two three four
five six seven eight

这个单行命令反转了列的顺序,输出如下:

four three two one
eight seven six five

如果输入中的列是由空格以外的任何字符分隔的,你可以使用-F命令行参数来设置不同的分隔符。例如,给定以下输入文件:

one:two:three:four
five:six:seven:eight

你可以像这样在单行命令中添加-F:命令行参数:

perl -F: -alne 'print "@{[reverse @F]}"'

它会产生如下输出:

four three two one
eight seven six five

然而请注意,输出中缺少了:字符。要恢复它们,你需要稍微修改单行命令,并将$"变量设置为":",如下所示:

perl -F: -alne '$" = ":"; print "@{[reverse @F]}"'

这会产生预期的输出:

four:three:two:one
eight:seven:six:five

$"变量会改变在数组元素间插入的字符,当数组被插入到双引号字符串中时就是这样。

第七章 选择性地打印和删除行

在本章中,我们将研究各种打印和删除特定行的单行代码。这些单行代码将例如打印重复的行、打印文件中最短的行,以及打印匹配某些模式的行。

但是,每个打印特定行的单行代码也可以被视为删除那些未打印的行。例如,一个打印所有唯一行的单行代码会删除所有重复的行。我只讨论打印内容的单行代码,而不是删除内容的单行代码,因为一个总是另一个的反操作。

7.1 打印文件的第一行(模拟head -1

perl -ne 'print; exit' *file*

这个单行代码非常简单。由于-n选项,Perl 将第一行读取到$_变量中,然后调用print打印$_变量的内容。然后它就退出了。就这样。第一行被打印,这正是你想要的。

你也可以说这个单行代码删除了除了第一行之外的所有行。但不用担心。这个特定的单行代码不会删除文件内容,除非你还指定了-i命令行参数,像这样:

perl -i -ne 'print; exit' *file*

正如我在第一章和第 59 页的单行代码 6.1 中所解释的,-i参数会就地编辑文件。在这种情况下,文件中的所有行都会被删除,除了第一行。使用-i时,务必指定一个备份扩展名,像这样:

perl -i.bak -ne 'print; exit' *file*

这将在内容被覆盖之前创建一个备份文件file.bak

你可以向任何单行代码添加-i命令行参数来更改文件内容。如果不使用-i参数,单行代码只会将文件的新内容打印到屏幕上,而不会修改文件。

7.2 打印文件的前 10 行(模拟head -10

perl -ne 'print if $. <= 10' *file*

这个单行代码使用了$.特殊变量,它表示“当前行号”。每当 Perl 读取一行时,它会将$.增加 1,因此很明显,这个单行代码只是打印前 10 行。

这个单行代码也可以不使用if语句来编写:

perl -ne '$. <= 10 && print' *file*

在这里,只有当布尔表达式$. <= 10为真时才会调用print,这个表达式只有在当前行号小于或等于 10 时才为真。

另一种稍微复杂一些的方法是使用标量上下文中的范围操作符(..):

perl -ne 'print if 1..10' *file*

标量上下文中的范围操作符返回一个布尔值。这个操作符是双稳态的,像一个触发器,模拟了 sed、awk 以及各种文本编辑器中的行范围(逗号)操作符。只要左操作数为假,操作符的值就是假。一旦左操作数为真,范围操作符为真,直到右操作数为真,此时范围操作符又变为假。因此,这个双稳态操作符在第一行时变为真,保持真直到第十行,然后变为假并保持为假。

第四种选择是遵循本章中的第一个示例:

perl -ne 'print; exit if $. == 10' *file*

这里,我对exit加了一个条件,即当前行(我刚刚打印的行)是第 10 行。

7.3 打印文件的最后一行(模拟tail -1

perl -ne '$last = $_; END { print $last }' *file*

打印文件的最后一行比打印第一行要复杂,因为你永远无法知道哪一行是最后一行。因此,你总是需要将刚刚读取的行保存在内存中。在这个单行代码中,你会将当前行$_保存到$last变量中。当 Perl 程序结束时,它会执行END块中的代码,打印最后一行读取的内容。

这里有另一种做法:

perl -ne 'print if eof' *file*

这个单行代码使用了eof(或文件结束)函数,它会返回 1,如果下一个读取操作返回文件结尾。因为在文件的最后一行之后,下一次读取将返回文件结尾,所以这个单行代码可以完成任务。下一次读取意味着 Perl 会尝试从当前文件中读取一个字符,如果读取失败,它会发出到达文件结尾的信号,表示整个文件已被读取。如果读取成功,Perl 会悄悄地将该字符放回输入流,就好像什么都没有发生一样。

7.4 打印文件的最后 10 行(模拟tail -10

perl -ne 'push @a, $_; @a = @a[@a-10..$#a] if @a>10; END { print @a }' *file*

这个单行代码有点复杂。在这里,你将每一行推入@a数组,然后如果数组中包含超过 10 个元素,就用其最后 10 个元素替换@a。表达式@a = @a[@a-10..$#a]的意思是“用@a的最后 10 个元素替换@a”。@a-10会使@a在标量上下文中进行求值,因此返回数组元素的数量减去 10。表达式$#a@a数组的最后一个索引。最后,@a[@a-10..$#a]进行切片(返回)数组的最后 10 个元素,并用它来覆盖@a,使其始终只包含最后 10 个元素。

例如,假设@a包含(line1, line2, line3, line4),你想打印文件的最后四行。当你读取到第五行时,数组变成了(line1, line2, line3, line4, line5),而@a-4的值是 1,因为@a在标量上下文中是 5。但$#a的值是 4,因为它是数组的最后一个索引。因此,当你取切片@a[@a-4..$#a]时,它变成了@a[1..4],这会丢弃数组的第一个元素,@a数组变成了(line2, line3, line4, line5)

一个更简单的写法是使用shift

perl -ne 'push @a, $_; shift @a if @a>10; END { print @a }' *file*

这个单行代码不需要对@a进行切片,因为你可以保证,如果@a > 10,那么@a == 11shift是一个操作符,用于移除数组的第一个元素。所以在这个循环中,当你有超过 10 行时,你可以简单地移除存储的第一行。

7.5 打印匹配正则表达式的行

perl -ne '/*regex*/ && print'

这个单行代码测试当前行是否匹配/regex/。如果匹配成功,/regex/的匹配就会成功,并且调用print

你可以用if来替代&&,反转/regex/print语句:

perl -ne 'print if /*regex*/'

7.6 打印不匹配正则表达式的行

perl -ne '!/*regex*/ && print'

这段单行代码是对前面那段单行代码的反转。这里,我通过!操作符反转匹配,测试该行是否不匹配/regex/。如果不匹配,我调用print来打印该行。

你也可以反过来写:

perl -ne 'print if !/*regex*/'

你也可以使用unless代替if !

perl -ne 'print unless /*regex*/'

另一种写法是将德摩根定律应用到!/regex/ && print

perl -ne '/*regex*/ || print'

7.7 打印每一行,前面有匹配正则表达式的行

perl -ne '/*regex*/ && $last && print $last; $last = $_'

这段单行代码会在匹配/regex/的行上面打印一行。我们从最后一个语句$last = $_开始,它将每一行保存到$last变量中。假设下一行被读取并且匹配了/regex/。由于上一行已保存在$last中,这段单行代码会直接打印出来。&&系列操作符的意思是,首先正则表达式必须匹配,其次$last必须为真值。(空行仍然会被打印,因为它们包含换行符。)

假设你有一个包含四行的文件:

hello world
magic line
bye world
magic line

如果你想打印所有匹配magic的行上方的行,可以这样做:

perl -ne '/magic/ && $last && print $last; $last = $_'

该单行代码将打印:

hello world
bye world

7.8 打印每一行,前面紧跟着匹配正则表达式的行

perl -ne 'if ($p) { print; $p = 0 } $p++ if /*regex*/'

在这里,我将变量$p设置为 1,如果当前行匹配正则表达式。变量$p为 1 表示下一行应该被打印。现在,当下一行被读取并且$p被设置时,当前行会被打印,$p会被重置为 0。非常简单。

假设你有这样一个四行的文件:

science
physics
science
math

如果你想打印所有匹配science的行下方的行,可以这样做:

perl -ne 'if ($p) { print; $p = 0 } $p++ if /science/'

该单行代码将打印:

physics
math

如果你想使用&&来编写这段代码并避免使用if和大括号,可以这样做:

perl -ne '$p && print && ($p = 0); $p++ if /science/'

你也可以聪明地简化这段单行代码,变成以下内容:

perl -ne '$p && print; $p = /science/'

如果当前行匹配science,则变量$p被设置为真值,下一行将被打印。如果当前行不匹配science,则$p变为未定义,下一行不被打印。

7.9 打印以任意顺序匹配正则表达式 AAA 和 BBB 的行

perl -ne '/*AAA*/ && /*BBB*/ && print'

这段单行代码测试一行是否同时匹配两个正则表达式。如果一行同时匹配/AAA//BBB/,它就会被打印。具体来说,这段单行代码会打印包含*AAA**BBB*的行,例如foo AAA bar BBB baz,但不会打印foo AAA bar AAA,因为它不包含*BBB*

7.10 打印不匹配正则表达式 AAA 和 BBB 的行

perl -ne '!/*AAA*/ && !/*BBB*/ && print'

这段单行代码几乎与之前的一模一样。这里,我测试一行是否不匹配两个正则表达式。如果它既不匹配/AAA/也不匹配/BBB/,它就会被打印。

7.11 打印匹配正则表达式 AAA 后接 BBB 再接 CCC 的行

perl -ne '/*AAA*.**BBB*.**CCC*/ && print'

在这里,我将正则表达式 AAABBBCCC.* 连接在一起,.* 的意思是“匹配任何东西或什么都不匹配”。如果 AAA 后跟 BBB,再后跟 CCC,该行就会打印。例如,这个单行代码会匹配并打印类似 123AAA880BBB222CCC、xAAAyBBBzCCCAAABBBCCC 这样的字符串。

7.12 打印至少 80 个字符的行

perl -ne 'print if length >= 80'

这个单行代码打印所有至少包含 80 个字符的行。在 Perl 中,有时可以省略函数调用的括号 (),所以这里我省略了 length 函数的括号。事实上,调用 lengthlength()length($_) 对于 Perl 来说是等效的。

如果你不想计算行尾,可以通过 -l 打开行尾的自动处理:

perl -lne 'print if length >= 80'

这个开关确保空白行的长度为零,而空白行通常有长度 1 或 2,具体取决于文件格式。(UNIX 换行符的长度为 1;Windows 换行符的长度为 2。)

7.13 打印少于 80 个字符的行

perl -ne 'print if length() < 80'

这个单行代码是前一个的反转。它检查一行的长度是否少于 80 个字符。同样,如果你不想计算行尾,使用 -l

7.14 只打印第 13 行

perl -ne '$. == 13 && print && exit'

正如我在第 70 页的单行代码 7.2 中解释的那样,$. 特殊变量表示“当前行号”。因此,如果 $. 的值是 13,这个单行代码会打印该行并退出。

7.15 打印除第 27 行外的所有行

perl -ne '$. != 27 && print'

与前一个单行代码类似,这个代码检查当前行的行号是否为 27。如果行号不是 27,它会打印;如果是,它就不打印。

你也可以通过交换 print$. != 27 并使用 if 语句修饰符来实现相同的效果—就像这样:

perl -ne 'print if $. != 27'

或者你可以使用 unless

perl -ne 'print unless $. == 27'

7.16 只打印第 13、19 和 67 行

perl -ne 'print if $. == 13 || $. == 19 || $. == 67'

这个单行代码只会打印第 13、19 和 67 行。它不会打印其他行。其工作原理是:如果当前行号(存储在 $. 变量中)是 13、19 或 67,它会调用 print。你可以使用任何行号来打印特定的行。例如,要打印第 13、19、88、290 和 999 行,你可以这样做:

perl -ne 'print if $. == 13 || $. == 19 || $. == 88 || $. == 290 || $. == 999'

如果你想打印更多的行,可以将它们放入一个单独的数组中,然后测试 $. 是否在这个数组中:

perl -ne '
  @lines = (13, 19, 88, 290, 999, 1400, 2000);
  print if grep { $_ == $. } @lines
'

这个单行代码使用 grep 测试当前行 $. 是否在 @lines 数组中。如果当前行号在 @lines 数组中找到,grep 函数会返回一个包含当前行号的元素列表,并且这个列表的值为真。如果当前行号不在 @lines 数组中,grep 函数会返回一个空列表,值为假。

7.17 打印第 17 到 30 行的所有行

perl -ne 'print if $. >= 17 && $. <= 30'

在这个单行代码中,$. 变量代表当前行号。因此,单行代码检查当前行号是否大于等于 17 且小于等于 30。

你也可以使用翻转操作符执行相同的操作,翻转操作符在第 7.2 节的第 70 页中有说明。当与整数一起使用时,翻转操作符作用于$.

perl -ne 'print if 17..30'

7.18 打印两个正则表达式之间的所有行(包括匹配的行)

perl -ne 'print if /*regex1*/../*regex2*/'

这一行代码使用了翻转操作符(在第 7.2 节的第 70 页中有说明)。当与整数一起使用时,操作数会与$.变量进行比较。当与正则表达式一起使用时,操作数会与当前行进行比较,当前行存储在$_变量中。操作符最初返回false。当一行与regex1匹配时,操作符翻转并开始返回true,直到另一行与regex2匹配。此时,操作符最后一次返回true,然后翻转回false状态。从此以后,操作符将一直返回false。因此,这一行代码打印所有在regex1regex2匹配的行之间(包括匹配的行)。

7.19 打印最长的行

perl -ne '
  $l = $_ if length($_) > length($l);
  END { print $l }
'

这一行代码将迄今为止看到的最长行保存在$l变量中。如果当前行$_的长度超过了最长行的长度,则将$l中的值替换为当前行的值。在程序结束前,END块会被执行,并打印保存在$l中的最长行值。

如果你想防止换行符计入行长,请记得使用-l选项。

7.20 打印最短的行

perl -ne '
  $s = $_ if $. == 1;
  $s = $_ if length($_) < length($s);
  END { print $s }
'

这一行代码是上一行的相反操作。因为它查找的是最短的行,并且对于第一行,$s尚未定义,你需要显式地通过$s = $_ if $. == 1将其值设置为第一行。然后它就简单地执行与上一行代码相反的操作。也就是说,它检查当前行是否是迄今为止最短的行,如果是,就将其赋值给$s

7.21 打印所有包含数字的行

perl -ne 'print if /\d/'

这行代码使用正则表达式\d(表示“一个数字”)来检查一行是否包含数字。如果包含,检查成功,行就会被打印。例如,这一行会被打印,因为它包含数字:

coding is as easy as 123

然而,这一行不会被打印,因为它不包含数字:

coding is as easy as pie

7.22 打印所有只包含数字的行

perl -ne 'print if /^\d+$/'

在这一行代码中,正则表达式^\d+$表示“如果一行从头到尾只包含数字,则匹配该行”。例如,这一行会被打印,因为它只包含数字:

3883737189170238912377

然而,这一行不会被打印,因为它还包含一些字符:

8388338 foo bar random data 999

你也可以反转^\d$的正则表达式,改用\D

perl -lne 'print unless /\D/'

这个单行代码非常适合培养你的逻辑推理能力,因为它使用了两次逻辑否定。在这里,只有当一行包含非数字字符时,行才会被打印。换句话说,只有当所有字符都是数字时,它才会打印。(注意,我在这个单行代码中使用了-l命令行参数,因为行末有换行符。如果我不使用-l,那么这一行就会包含换行符—一个非数字字符—因此不会被打印。)

7.23 打印所有只包含字母字符的行

perl -ne 'print if /^[[:alpha:]]+$/

这个单行代码检查一行是否只包含字母字符。如果是,它会打印该行。[[:alpha:]]表示“任何字母字符”。而[[:alpha:]]+表示“所有字母字符”。

7.24 打印每隔一行

perl -ne 'print if $. % 2'

这个单行代码打印第一、第三、第五、第七行(依此类推)。它之所以这样做,是因为$. % 2在当前行号为奇数时为真,在当前行号为偶数时为假。

7.25 从第二行开始打印每隔一行

perl -ne 'print if $. % 2 == 0'

这个单行代码类似于前一个,只是它打印第二、第四、第六、第八行(依此类推),因为$. % 2 == 0在当前行号为偶数时为真。

或者,你可以简单地反转前一个例子中的测试条件:

perl -ne 'print unless $. % 2'

7.26 只打印所有重复的行一次

perl -ne 'print if ++$a{$_} == 2'

这个单行代码跟踪它已经看到的行,并计算每行出现的次数。如果它第二次看到一行,它会打印该行,因为++$a{$_} == 2为真。如果它看到一行超过两次,它就什么也不做,因为该行的计数大于 2。

7.27 打印所有唯一的行

perl -ne 'print unless $a{$_}++'

这个单行代码只有在该行的哈希值$a{$_}为假时才打印该行。每次 Perl 读取一行时,它会增加$a{$_}的值,从而确保这个单行代码只打印从未出现过的行。

第八章 有用的正则表达式

在本章中,我们将讨论各种正则表达式,以及如何在一些方便的单行代码中使用它们。正则表达式包括匹配 IP 地址、HTTP 头和电子邮件地址;匹配数字和数字范围;提取和修改匹配项。我还将分享一些正则表达式难题和最佳实践。本章与前几章略有不同,因为我将从一个正则表达式开始,然后编写一个使用它的单行代码。

8.1 匹配看起来像 IP 地址的东西

/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/

这个正则表达式实际上并不能保证匹配的东西真的是一个有效的 IP 地址;它仅仅匹配看起来像 IP 地址的东西。例如,它可以匹配一个有效的 IP 地址,如 81.198.240.140,也可以匹配一个无效的 IP 地址,如 936.345.643.21

这是它的工作原理。正则表达式开头的 ^ 是一个锚点,用来匹配字符串的开头。接下来,\d{1,3} 匹配一位、两位或三位连续的数字。\. 匹配一个点。结尾的 $ 是一个锚点,用来匹配字符串的结尾。(你使用 ^$ 锚点来防止像 foo213.3.1.2bar 这样的字符串匹配。)

你可以通过将前三个重复的 \d{1,3}\. 表达式分组来简化这个正则表达式:

/^(\d{1,3}\.){3}\d{1,3}$/

假设你有一个文件,内容如下,并且你想提取看起来像 IP 地址的行:

81.198.240.140
1.2.3.4
5.5
444.444.444.444
90.9000.90000.90000
127.0.0.1

要提取仅匹配的行,可以写成这样:

perl -ne 'print if /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/'

这应该会输出:

81.198.240.140
1.2.3.4
444.444.444.444
127.0.0.1

单行代码 8.3 解释了如何精确匹配一个 IP,而不仅仅是看起来像 IP 的东西。

8.2 测试一个数字是否在 0 到 255 的范围内

/^([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/

我喜欢用难题来挑战人们。我最喜欢的一个问题是让某人编写一个匹配数字范围的正则表达式。如果你以前从未做过的话,写出一个其实是相当棘手的。

这是它的工作原理。一个数字可以有一位、两位或三位。如果数字是一位,你可以让它是任何 [0-9]。如果它有两位,你也允许它是 [0-9][0-9] 的任意组合。但是如果数字有三位,它必须是 100 到 199 之间的某个数字,或者是 200 到 249 之间的数字。如果数字是 100 到 199 之间的,那么 1[0-9][0-9] 匹配它。如果数字是 200 到 249 之间的,那么这个数字要么是 200 到 249(由 2[0-4][0-9] 匹配),要么是 250 到 255(由 25[0-5] 匹配)。

让我们确认这个正则表达式真的能匹配从 0 到 255 的所有数字,并编写一个单行代码来实现它:

perl -le '
  map { $n++ if /^([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ } 0..255;
  END { print $n }
'

这个单行代码输出 256,这是 0 到 255 范围内的总数字。它会遍历 0 到 255 的范围,并在每个匹配的数字上增加 $n 变量。如果输出值小于 256,你就知道有些数字没有匹配。

让我们还确保这个单行代码不会匹配超过 255 的数字:

perl -le '
  map { $n++ if /^([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ } 0..1000;
  END { print $n }
'

尽管有 1001 次迭代,从 0 到 1000,最终$n的值和输出应该仍然是 256,因为大于 255 的数字不应该匹配。如果值大于 256,你就会知道匹配的数字太多,正则表达式是错误的。

8.3 匹配一个 IP 地址

my $ip_part = qr/[0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]/;
if ($ip =~ /^$ip_part\.$ip_part\.$ip_part\.$ip_part$/) {
  print "valid ip\n";
}

这个正则表达式结合了前两个正则表达式(8.1 和 8.2)的思想,并引入了qr/.../操作符,它允许你构造一个正则表达式并将其保存在变量中。在这里,我将匹配 0 到 255 范围内所有数字的正则表达式保存在$ip_part变量中。接下来,$ip_part匹配 IP 地址的四个部分。

你可以通过将前三个 IP 部分分组来简化这一点:

if ($ip =~ /^($ip_part\.){3}$ip_part$/) {
  print "valid ip\n";
}

让我们在与一行代码 8.1 相同的文件上运行这个。如果你的输入文件是:

81.198.240.140
1.2.3.4
5.5
444.444.444.444
90.9000.90000.90000
127.0.0.1

你的一行代码是

perl -ne '
  $ip_part = qr{([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])};
  print if /^($ip_part\.){3}$ip_part$/
'

然后输出是

81.198.240.140
1.2.3.4
127.0.0.1

如你所见,只有有效的 IP 地址会被打印出来。

8.4 检查字符串是否像一个电子邮件地址

/\S+@\S+\.\S+/

这个正则表达式确保字符串看起来像一个电子邮件地址;然而,它并不保证字符串就是一个电子邮件地址。首先,它匹配不是空白的内容(\S+)直到@符号;然后它尽可能多地匹配,直到找到一个点;接着它再匹配一些内容。

如果匹配成功,你知道该字符串至少看起来像是一个带有@符号和点的电子邮件地址。例如,cats@catonmat.net匹配,但cats@catonmat不匹配,因为正则表达式无法找到一个完全合格的域名所需的点。

这是一个更加健壮的方法来判断一个字符串是否是有效的电子邮件地址,使用Email::Valid模块:

use Email::Valid;
print Email::Valid->address('cats@catonmat.net') ? 'valid email' : 'invalid email';

在这里,你使用了三元运算符cond ? true : false。如果cond为真,则执行true部分;否则执行false部分。如果电子邮件有效,它将打印valid email;如果无效,则打印invalid email

所以一行代码可以像这样:

perl -MEmail::Valid -ne 'print if Email::Valid->address($_)'

在这里,如果电子邮件地址有效,你只需打印它。

8.5 检查字符串是否是数字

使用正则表达式来判断一个字符串是否是数字很困难。这是一个匹配十进制数字的正则表达式的衍生形式。

我从 Perl 的\d正则表达式开始,它匹配数字 0 到 9:

/^\d+$/

这个正则表达式从字符串的开始^到结束$匹配一个或多个数字\d。但它不匹配像+3-3这样的数字。让我们修改正则表达式以匹配它们:

/^[+-]?\d+$/

在这里,[+-]?表示“匹配数字前的可选加号或减号。”这个正则表达式现在匹配+3-3,但不匹配-0.3。让我们添加这个:

/^[+-]?\d+\.?\d*$/

我通过添加\.?\d*扩展了之前的正则表达式,这匹配一个可选的点后跟零个或多个数字。现在我们可以开始了。这个正则表达式也匹配像-0.30.3这样的数字,但不会匹配像123,456.5这样的数字。

匹配十进制数字的一个更好的方法是使用Regexp::Common模块。例如,要匹配一个十进制数字,你可以使用Regexp::Common中的$RE{num}{real}。以下是一行代码,它过滤输入并只打印十进制数字:

perl -MRegexp::Common -ne 'print if /$RE{num}{real}/'

这个一行代码也匹配并打印类似123,456.5这样的数字。

那么,如何匹配正十六进制数字呢?方法如下:

/⁰x[0-9a-f]+$/i

这个一行代码匹配十六进制前缀0x,后面跟着十六进制数字本身。末尾的/i标志确保匹配时不区分大小写。例如,0x5af匹配,0X5Fa匹配,但97不匹配,因为97没有十六进制前缀。

更好的做法是使用$RE{num}{hex},因为它支持负数、十进制数和数字分组。

那么,如何匹配八进制数字呢?

/⁰[0-7]+$/

八进制数字以 0 为前缀,后跟八进制数字0-7。例如,013是有效的,而09不是,因为它不是一个有效的八进制数字。使用$RE{num}{oct}更好,因为它支持负数八进制数、小数点八进制数以及数字分组。

最后,我们来到了二进制匹配:

/^[01]+$/

二进制基数只由 0 和 1 组成,因此010101匹配,但210101不匹配,因为2不是一个有效的二进制数字。

Regexp::Common还提供了一个更好的正则表达式来匹配二进制数字:$RE{num}{bin}

8.6 检查一个单词是否在字符串中出现两次

/(*word*).*\1/

这个正则表达式匹配一个单词,后面跟着其他字符或什么都没有,再跟着同样的单词。这里,(word)将单词捕获到组 1 中,\1指代组 1 的内容,等同于写/(word).*word/。例如,silly things are silly匹配/(silly).*\1/,但silly things are boring不匹配,因为silly在字符串中没有重复。

8.7 将字符串中的所有整数增加 1

$str =~ s/(\d+)/$1+1/ge

在这里,你使用替换操作符s来匹配所有整数(\d+),将它们放入捕获组 1,然后将它们替换为其值加一:$1+1g标志表示查找字符串中的所有数字,e标志表示将$1+1作为 Perl 表达式求值。例如,this 1234 is awesome 444会变成this 1235 is awesome 445

请注意,这个正则表达式不会增加浮点数,因为它使用\d+来匹配整数。要增加浮点数,请使用一行代码 8.5 中的$RE{num}{real}正则表达式。这里有一个使用$RE{num}{real}的一行代码示例:

perl -MRegexp::Common -pe 's/($RE{num}{real})/$1+1/ge'

如果你传入这个一行代码的输入weird 44.5 line -1.25,它会打印weird 45.5 line -0.25

8.8 从 HTTP 头提取 HTTP 用户代理字符串

/^User-Agent: (.+)$/

HTTP 头部的格式是Key: Value对。你可以通过指示正则表达式引擎将Value部分保存在$1组变量中来轻松解析这样的字符串。例如,如果 HTTP 头包含以下内容:

Host: www.catonmat.net
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_0_0; en-US)
Accept: application/xml,application/xhtml+xml,text/html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

那么,正则表达式将提取字符串Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_0_0; en-US)

8.9 匹配可打印的 ASCII 字符

/[ -~]/

这个正则表达式既巧妙又复杂。要理解它,可以查看man ascii,你会看到空格的值是0x20,而~字符的值是0x7e。表达式[ -~]定义了一个从空格到~的字符范围。因为所有在空格和~之间的字符都是可打印的,所以这个正则表达式匹配所有可打印字符。这是我最喜欢的正则表达式,因为当你第一次看到它时,它让人感到困惑。它匹配什么?一个空格,一个破折号和一个波浪线?不,它匹配的是从空格到波浪线之间的所有字符!

要反转匹配,可以将^放在分组的第一个字符位置:

/[^ -~]/

这个正则表达式匹配的是与[ -~]相反的内容,也就是说,匹配所有不可打印字符。

8.10 提取两个 HTML 标签之间的文本

m|<strong>([^<]*)</strong>|

在我解释这个正则表达式之前,我想说的是,只有在快速处理并需要完成任务时,使用正则表达式匹配 HTML 是可以的。你绝不应该在正式的应用程序中使用正则表达式来匹配和解析 HTML,因为 HTML 实际上是一个复杂的语言,通常无法通过正则表达式来解析。相反,应该使用像HTML::TreeBuilder这样的模块来更清晰地完成任务!

这个正则表达式将<strong>...</strong>HTML 标签之间的文本保存到$1特殊变量中。这个单行代码中最棘手的部分是([^<]*),它匹配直到<字符之前的所有内容。这是一个正则表达式惯用法。

例如,如果你试图匹配的 HTML 是<strong>hello</strong>,那么这个正则表达式会在$1变量中捕获hello。然而,如果你试图匹配的 HTML 是<strong><em>hello</em> </strong>,那么这个正则表达式就不会匹配,因为在<strong></strong>之间还有另一个 HTML 标签。

要提取两个 HTML 标签之间的所有内容,包括其他 HTML 标签,你可以写:

m|<strong>(.*?)</strong>|

这个正则表达式将<strong>...</strong>之间的所有内容保存到$1变量中。例如,如果 HTML 是<strong><em>hello</em> </strong>,那么这个正则表达式将$1设置为<em>hello</em>。正则表达式中的(.*?)部分匹配两个最接近的<strong></strong>标签之间的所有内容。正则表达式中的问号?控制其贪婪度。

如果你想成为一个好公民并使用HTML::TreeBuilder,那么一个执行相同操作的 Perl 程序应该是这样的:

use warnings;
use strict;
use HTML::TreeBuilder;
my $tree = HTML::TreeBuilder->new_from_content(
  "<strong><em>hello</em></strong>"
);
my $strong = $tree->look_down(_tag => 'strong');
if ($strong) {
  print $_->as_HTML for $strong->content_list;
}
$tree->delete;

在这里,我从给定的字符串创建了一个新的HTML::TreeBuilder实例;然后我找到了<strong>标签,并将所有<strong>标签的子元素以 HTML 格式输出。正如你所看到的,虽然像这样的程序不适合作为单行代码来写,但它是一个更加健壮的解决方案。

8.11 将所有<b>标签替换为<strong>

$html =~ s|<(/)?b>|<$1strong>|g

这里,我假设 HTML 内容存储在变量$html中。表达式<(/)?b>匹配开闭的<b>标签,捕获可选的闭合标签斜杠到组$1,然后根据是否找到开闭标签,将匹配到的标签替换为<strong></strong>

请记住,正确的做法是使用HTML::TreeBuilder并编写一个合适的程序。你应该仅在快速解决问题时使用这个正则表达式。下面是一个使用HTML::TreeBuilder的程序示例:

use warnings;
use strict;
use HTML::TreeBuilder;
my $tree = HTML::TreeBuilder->new_from_content("
  <div><p><b>section 1</b></p><p><b>section 2</b></p></div>
");
my @bs = $tree->look_down(_tag => 'b');
$_->tag('strong') for @bs;
print $tree->as_HTML;
$tree->delete;

在这里,我从给定的字符串中创建了HTML::TreeBuilder对象;接下来,我找到了所有的<b>标签,并将它们存储在@bs数组中,然后遍历@bs并将它们的标签名称更改为<strong>

8.12 从正则表达式中提取所有匹配项

my @matches = $text =~ /*regex*/g;

这里,正则表达式匹配的结果在列表上下文中进行评估,这使得它返回所有匹配项。匹配项被放入@matches变量中。

例如,下面的正则表达式从字符串中提取所有整数:

my $t = "10 hello 25 moo 30 foo";
my @nums = $text =~ /\d+/g;

执行这段代码后,@nums包含了(10, 25, 30)。你还可以使用括号仅捕获字符串的一部分。例如,下面是如何捕获只包含多个键值对(如key=value)并用分号分隔的行中的值:

my @vals = $text =~ /[^=]+=([^;]+)/g;

这个正则表达式首先通过[^=]+匹配键,然后匹配分隔键和值的=字符,接着匹配值部分([^;]+)。如你所见,正则表达式中的值部分被括在了括号中,因此这些值会被捕获。

这里有一个示例。假设你有一个文件,内容如下:

access=all; users=peter,alastair,bill; languages=awk,sed,perl

然后你写下了这个一行代码:

perl -nle 'my @vals = $_ =~ /[^=]+=([^;]+)/g; print "@vals"'

运行后输出如下:

all peter,alastair,bill awk,sed,perl

这些是accessuserslanguages键的值!

附录 A. Perl 的特殊变量

在本附录中,我总结了 Perl 最常用的特殊(预定义)变量,如 $_$.$/$\$1$2$3(等等)、$@F@ARGV 等。

A.1 变量 $_

$_ 变量,被称为 默认变量,是 Perl 中最常用的变量。通常这个变量被读作“it”(如果不读作“dollar-underscore”);当你继续阅读时,你会明白原因。

当使用 -n-p 命令行参数时,输入被存储在哪里呢?而且,许多操作符和函数会隐式地作用于它。这里有一个例子:

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

在这里,我将字符串 "foo" 放入 $_ 变量中,然后调用 print。当没有参数时,print 会打印 $_ 变量的内容,即 "foo"

类似地,当 s/regex/replace//regex/ 操作符没有使用 =~ 操作符时,$_ 会被使用。考虑这个例子:

perl -ne '/foo/ && print'

这个一行命令仅打印匹配/foo/的行。/foo/ 操作符隐式地作用于包含当前行的 $_ 变量。你也可以将其重写为以下内容,但那样会需要输入过多:

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

“如果匹配 /foo/,则打印出来”——你大概明白了。你也可以通过调用 s/foo/bar/ 来简单地替换所有行中的文本:

perl -pe 's/foo/bar/'

有趣的是,Perl 借用了 $_ 变量来自 sed。记得 sed 有一个模式空间吗?$_ 变量也可以称为 Perl 的模式空间。如果你在 sed 中编写前面的单行命令 (perl -pe 's/foo/bar/'),它会变成 sed 's/foo/bar/',因为 sed 会将每一行放入模式空间中,并且 s 命令会隐式地作用于它。Perl 从 sed 借用了许多概念和命令。

使用 $_-n 参数

当使用 -n 参数时,Perl 会在你的程序周围加上以下循环:

while (<>) {
    # your program goes here (specified by -e)
}

while (<>) 循环从标准输入或命令行上指定的文件中读取行,并将每一行放入 $_ 变量中。你可以修改这些行并打印它们。例如,你可以反转这些行:

perl -lne 'print scalar reverse'

因为我在这里使用了 -n 参数,所以这个程序变成了:

while (<>) {
    print scalar reverse
}

这等同于

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

这两个程序是等价的,因为许多 Perl 函数会隐式地作用于 $_,这使得写 reversereverse $_ 在功能上是一样的。你需要使用 scalar 来将 reverse 函数放入标量上下文中。否则,它会在列表上下文中(print 强制列表上下文),并且不会反转字符串。(我在第 12 页的单行命令 2.6 中详细解释了 -n 标志,在第 67 页的单行命令 6.22 中讲解了行反转。)

使用 $_-p 参数

当你使用 -p 参数时,Perl 会在你的程序周围加上以下循环:

while (<>) {
    # your program goes here (specified by -e)
} continue {
    print or die "-p failed: $!\n";
}

结果几乎与使用 -n 参数时相同,只是每次迭代后,$_ 的内容会被打印出来(通过 continue 块中的 print)。

为了像使用 -n 时那样反转行,我可以这样做:

perl -pe '$_ = reverse $_'

现在程序变成了:

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

我已将 $_ 变量修改为 reverse $_,这会反转行。continue 块确保它会被打印。(书中第 7 页的单行代码 2.1 更详细地解释了 -p 参数。)

明确使用 $_

$_ 变量也常常被显式使用。以下是一些显式使用 $_ 变量的例子:

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

这行代码的输出是 2 4 6 8 10 12 14 16 18 20。在这里,我使用 map 函数对给定列表中的每个元素应用表达式,并返回一个新的列表,其中每个元素都是该表达式的结果。在这个例子中,列表是 1..101 2 3 4 5 6 7 8 9 10),表达式是 $_ * 2,意味着将每个元素(“它”)乘以 2。如你所见,我明确地使用了 $_。当 map 函数遍历列表时,每个元素会被放入 $_,方便我使用。

现在让我们在一个方便的单行代码中使用 map。怎么样,试试一个将每行的每个元素乘以 2 的例子?

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

这行代码将表达式 $_ * 2 应用到 @F 中的每个元素。看起来很复杂的 "@{[...]}" 只是执行代码的一种方式,放在引号内部。(书中第 30 页的单行代码 4.2 解释了 @F,第 32 页的单行代码 4.4 解释了 "@{[...]}"。)

另一个显式使用 $_ 的函数是 grep,它允许你从列表中过滤元素。这里是一个例子:

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

这行代码的输出是 6 7 8 9 10。如你所见,grep 从列表中过滤掉了大于 5 的元素。条件 $_ > 5 问的是:“当前元素是否大于 5?”——更简洁地说,“它是否大于 5?”

让我们在一行代码中使用 grep。如何用一个查找并打印当前行上所有回文的例子呢?

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 来进一步了解 $_ 变量。perlvar 文档解释了 Perl 中所有的预定义变量。

A.2 变量 $.

在读取文件时,$. 变量总是包含当前正在读取的行号。例如,这行代码为 file 中的每一行编号:

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

你可以使用这行代码来做同样的事情,它将当前行替换为行号,后面跟着相同的行:

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

$. 变量在多个文件之间不会重置,所以要同时为多个文件编号,你可以写:

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

这行代码继续为 file2 中的行编号,接着 file1 停下的位置。(如果 file1 有 10 行,file2 的第一行将被编号为 11。)

要重置 $. 变量,你可以显式地对当前文件句柄 ARGV 执行 close

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

ARGV 是一个特殊的文件句柄,包含当前打开的文件。通过调用 eof,我在检查当前文件是否已到达结尾。如果是,close 会关闭它,从而重置 $. 变量。

你可以通过修改 $/ 变量来改变 Perl 认为的一行是什么。接下来的部分将讨论这个变量。

A.3 变量 $/

$/ 变量是输入记录分隔符,默认值是换行符。这个变量告诉 Perl 什么算作一行。假设你有一个简单的程序,给每一行编号:

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

因为默认情况下 $/ 是换行符,Perl 会读取直到第一个换行符的所有内容,放入 $_ 变量中,并递增 $. 变量。接下来,它调用 print "$. $_",打印当前的行号和该行内容。但如果你将 $/ 的值更改为两个换行符,比如 $/ = "\n\n",Perl 将读取直到第一个两个换行符的所有内容;也就是说,它按段落而不是按行来读取文本。

这是另一个示例。如果你有一个如下的文件,可以将 $/ 设置为 :,然后 Perl 将逐个读取文件中的字符。

3:9:0:7:1:2:4:3:8:4:1:0:0:1:... (goes on and on)

或者如果你将 $/ 设置为 undef,Perl 将在一次读取中读取整个文件(称为 slurping):

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

这一行代码将整个文件 file 读取到变量 $contents 中。

你也可以将 $/ 设置为引用一个整数:

$/ = \1024

在这种情况下,Perl 每次读取 1024 字节。(这也叫做 逐条记录读取。)

你也可以使用 -0 命令行开关为这个变量提供一个值,但请注意,像这样不能进行逐条记录的版本。例如,要将 $/ 设置为 :,请指定 -0072,因为 072 是字符 : 的八进制值。

为了记住这个变量的作用,回想一下,当引用诗歌时,行与行之间是用 / 分隔的。

A.4 变量 $\

每次 print 操作后都会附加美元反斜杠变量。例如,你可以在每个 print 后附加一个点和一个空格 ". "

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

这一行代码将输出以下内容:

hello. world.

修改这个变量特别有帮助,当你想通过双重换行符分隔输出时。

要记住这个变量,只需回想你可能希望在每一行后打印 \n。请注意,对于 Perl 5.10 及之后的版本,say 函数是可用的,它类似于 print,只是它总是在末尾添加一个换行符,并且不使用 $\ 变量。

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

变量 $1$2$3 等包含来自最后一次模式匹配中相应捕获括号对的匹配内容。以下是一个示例:

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

这一行代码匹配包含字符串 She said: 的行,然后捕获该字符串后的所有内容到变量 $1 中并打印出来。

当你使用另一个括号对时,文本将被捕获到变量 $2 中,依此类推:

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

在这一行代码中,首先 "She""He" 被捕获到变量 $1 中,然后她或他说的任何话被捕获到变量 $2 中,并作为 "$1: $2" 打印出来。你将得到与括号对数相同数量的捕获变量。

为了避免将文本捕获到变量中,可以在括号开头使用 ?: 符号。例如,将 (She|He) 更改为 (?:She|He)

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

不会将 "She""He" 捕获到变量 $1 中。相反,第二对括号会将她或他说的话捕获到变量 $1 中。

从 Perl 5.10 开始,你可以使用命名捕获组,例如 (?<name>...)。这样做时,您可以使用 $+{name} 来引用组,而不是使用变量 $1$2 等。例如,这将 "She""He" 捕获到名为 gender 的组中,并将她或他说的话捕获到名为 text 的组中:

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

A.6 变量 $,

$ 变量是 print 打印多个值时的输出字段分隔符。默认情况下它未定义,这意味着所有打印的项都会连接在一起。实际上,如果你这样做:

perl -le 'print 1, 2, 3'

你将打印出 123。然而,如果你将 $ 设置为冒号,则会:

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

你将得到 1:2:3

现在,假设你想打印一组值。如果你这样做:

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

输出是 123。但如果你将变量加上引号,值将以空格分隔:

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

所以输出是 1 2 3,因为数组在双引号字符串中被插值。

A.7 变量 $”

这就引出了 $" 变量:它是一个单一的空格(默认情况下),会在每个数组值之间插入。当你写类似 print "@data" 的代码时,@data 数组会被插入,且 $" 的值会在每个数组元素之间插入。例如,以下代码会打印 1 2 3

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

但如果你将 $" 改为,例如,破折号 -,输出将变为 1-2-3

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

回想一下这里的 @{[...]} 技巧。如果你 print "@{[...]}",你可以执行放在方括号中的代码。更多示例和细节,请参见 A.1 变量 \(_ 一节讨论的 `\)_` 变量,见第 95 页及第 32 页的一行代码 4.4。

A.8 变量 @F

@F 变量是在你的 Perl 程序中使用 -a 参数时创建的,-a 代表自动分割字段。当你使用 -a 时,输入会按空格字符分割,生成的字段会放入 @F 中。例如,如果输入行是 foo bar baz,那么 @F 是一个数组 ("foo", "bar", "baz")

这种技术允许你操作单独的字段。例如,你可以访问 $F[2] 来打印第三个字段,如下所示(记住数组是从索引 0 开始的):

perl -ane 'print $F[2]'

你还可以进行各种计算,比如将第五个字段乘以 2:

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

在这里,第五个字段 $F[4] 被乘以 2,而 print "@F" 会打印所有字段,字段之间用空格分隔。

你也可以将 -a 参数与 -F 参数一起使用,-F 参数指定分隔字符。例如,要处理 /etc/passwd 中以冒号分隔的条目,你可以写:

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

它会打印来自 /etc/passwd 的用户名。

A.9 变量 @ARGV

@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,它包含 ("file1", "file2")。如果你没有这么做,而是直接使用 @ARGV,一开始它将包含 ("file2"),但当 file1 被处理时,它将变为空 ()。这里要小心!

一个外观相似的变量 $ARGV 包含当前正在读取的文件名,如果程序当前从标准输入读取,则为 "-"

A.10 变量 %ENV

%ENV 哈希表包含来自你的 shell 的环境变量。当你希望在脚本中预定义一些值并在 Perl 程序或单行命令中使用这些值时,这个变量非常有用。

假设你想使用 system 函数执行一个不在路径中的程序。你可以修改 $ENV{PATH} 变量并附加所需的路径:

perl -nle '
  BEGIN { $ENV{PATH} .= ":/usr/local/yourprog/bin" }
  ...
  system("yourprog ...");
'

这个单行命令打印所有来自 Perl 的环境变量:

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

它遍历 %ENV 哈希表的键(环境变量名),将每个键放入 $_ 变量中,然后打印该名称后跟 $ENV{$_},即环境变量的值。

附录 B:在 Windows 上使用 Perl 单行命令

在本附录中,我将向你展示如何在 Windows 上运行 Perl,如何在 Windows 上安装 bash 移植版本,并展示如何通过三种不同的方式使用 Perl 单行命令:通过 Windows 的 bash 移植版本、Windows 命令提示符(cmd.exe)以及 PowerShell。

B.1 Windows 上的 Perl

在 Windows 上运行 Perl 之前,你需要安装适用于 Windows 的 Perl。我最喜欢的 Windows Perl 移植版本是 Strawberry Perl(strawberryperl.com/),这是一个包含你在 Windows 上运行和开发 Perl 应用所需的一切的 Perl 环境。Strawberry Perl 的设计尽可能像 UNIX 系统上的 Perl 环境。它包括 Perl 二进制文件、gcc 编译器及相关构建工具,以及许多外部库。

要安装 Strawberry Perl,下载并运行安装程序,点击几次菜单就可以完成安装。我的安装目录选择是c:\strawberryperl。(将任何 UNIX 软件安装到没有空格的目录中是个好主意。)安装完成后,安装程序应该会将安装目录添加到你的路径环境变量中,这样你就可以直接在命令行运行 Perl 了。

不幸的是,与 UNIX 系统的命令行相比,Windows 命令行非常基础。UNIX 系统运行的是一个真正的 shell,具有明确的命令行解析规则,而 Windows 并没有类似的东西。Windows 命令行对于某些符号的处理有奇怪的规则,引用规则不明确,转义规则也很奇怪,这一切都使得运行 Perl 单行命令变得困难。因此,在 Windows 上运行单行命令的首选方法是使用 UNIX shell(如 bash),正如你将在下一节中学到的那样。

B.2 Windows 上的 Bash

在 Windows 上运行 bash shell 非常简单。我推荐 win-bash(win-bash.sourceforge.net/),这是一个适用于 Windows 的独立 bash 移植版本,无需特殊环境或额外的 DLL 文件。下载包是一个包含 bash shell(bash.exe)和一堆 UNIX 工具(如 awk、cat、cp、diff、find、grep、sed、vi、wc 等大约 100 个工具)的 zip 文件。

要安装 bash 和所有相关工具,只需解压文件即可完成安装。我的安装目录选择是c:\winbash,同样是没有空格的目录。从c:\winbash运行bash.exe以启动 bash shell。

如果在安装了 Strawberry Perl 之后启动bash.exe,Perl 应该可以立即使用,因为 Strawberry Perl 的安装程序应该已经将安装目录添加到了路径中。要确认这一点,请运行perl --version。它应该输出已安装 Perl 的版本。如果你收到“找不到perl”的错误,手动将C:\strawberryperl\perl\bin目录添加到PATH环境变量中,可以在命令行输入以下内容:

PATH=$PATH:C:\\strawberryperl\\perl\\bin

Bash 使用 PATH 变量来查找可执行文件并运行它们。通过将 Strawberry Perl 的二进制目录添加到 PATH 变量中,你告诉 bash 去哪里查找 perl 可执行文件。

B.3 在 Windows Bash 中的 Perl 一行命令

在 Windows 上的 bash 和 UNIX 之间有一些重要的区别。第一个区别与文件路径有关。Win-bash 支持 UNIX 风格和 Windows 风格的路径。

假设你将 win-bash 安装在 C:\winbash。当你启动 bash.exe 时,它应该会将根目录 / 映射到当前的 C: 驱动器。要将根目录切换到另一个驱动器,比如 D:,在 bash shell 中输入 cd d:。要切换回 C:,在 shell 中输入 cd c:。现在,你可以通过 /work/report.txt、c:/work/report.txt 或 c:\work\report.txt 访问像 C:\work\report.txt 这样的文件。

使用 win-bash 的最大优势是本书中的所有一行命令都应该能够正常工作,因为你正在运行一个真正的 shell,就像在 UNIX 环境中一样!例如,要给 C:\work\report.txt 文件的每一行加上行号(第 17 页的单行命令 3.1),你可以运行:

perl -pe '$_ = "$. $_"' C:/work/report.txt

或者你可以像在 UNIX 中一样引用该文件:

perl -pe '$_ = "$. $_"' /work/report.txt

或者你也可以使用 Windows 风格的路径:

perl -pe '$_ = "$. $_"' C:\\work\\report.txt

为了避免使用双反斜杠,你可以用单引号引用文件路径:

perl -pe '$_ = "$. $_"' 'C:\work\report.txt'

如果文件名中有空格,你必须始终引用它。例如,要操作 C:\Documents and Settings\Peter\My Documents\report.txt,在传递给一行命令时,需要引用整个路径:

perl -pe '$_ = "$. $_"' 'C:\Documents and Settings\Peter\My Documents\report.txt'

或者使用 UNIX 风格的文件路径:

perl -pe '$_ = "$. $_"' '/Documents and Settings/Peter/My Documents/report.txt'

在这里引用文件名是必要的,因为如果不引用,Perl 会认为你传递的是一堆文件,而不是一个带空格的单一文件。

B.4 在 Windows 命令提示符中的 Perl 一行命令

如果由于某种原因你无法按推荐的方式使用 win-bash,你可以通过 Windows 命令提示符 (cmd.exe) 运行一行命令。如果你在 Windows 命令提示符中运行这些一行命令,你需要稍微修改一下本书中的命令,因为 Windows 解析和处理命令行参数的方式不同。下面是你需要做的。

首先,验证 Perl 是否可以通过命令提示符使用。启动 cmd.exe 并在命令行中输入 perl --version。如果你在安装 Strawberry Perl 后执行此操作,命令应该会输出 Perl 版本信息,这样就可以正常使用了。否则,你需要通过更新 PATH 环境变量来添加 Strawberry Perl 的二进制目录路径:

set PATH=%PATH%;C:\strawberryperl\perl\bin

和 UNIX 一样,PATH 变量告诉命令提示符在哪里查找可执行文件。

在 Windows 命令提示符中转换单行命令

现在让我们来看看如何为命令提示符转换一行命令,从一行命令 2.1(第 7 页),它将文件内容双倍间距开始。在 UNIX 中,你只需运行:

perl -pe '$\ = "\n"' *file*

然而,如果你在 Windows 命令提示符中运行这个一行命令,你必须确保它总是用外部的双引号括起来,并且你已经转义了其中的双引号和特殊字符。做了这些更改后,这个一行命令在 Windows 上应该是这样的:

perl -pe "$\ = \"\n\"" *file*

这行命令变得很乱,但你可以用一些 Perl 技巧使它看起来稍微整洁一些。首先,用 qq/.../ 操作符将一行命令中的双引号替换掉,它会将斜杠之间的任何内容加上双引号。在 Perl 中写 qq/text/ 等价于写 "text"。现在你可以这样重写这行命令:

perl -pe "$\ = qq/\n/" *file*

这要好一些。你还可以改变 qq 操作符用于分隔内容的字符。例如,语法 qq|...| 会将管道符 | 之间的内容加上双引号:

perl -pe "$\ = qq|\n|" file

你甚至可以使用匹配的圆括号或大括号,如下所示:

perl -pe "$\ = qq(\n)" file

或者是这样:

perl -pe "$\ = qq{\n}" file

让我们看看如何将更多的一行命令转换到 Windows。比如将一个 IP 地址转换为整数(第 45 页上的一行命令 4.27)?在 UNIX 中你可以运行:

perl -MSocket -le 'print unpack("N", inet_aton("127.0.0.1"))'

在 Windows 上,你需要将一行命令外部的引号改为双引号,并且转义一行命令内部的双引号:

perl -MSocket -le "print unpack(\"N\", inet_aton(\"127.0.0.1\"))"

或者你可以使用 qq|...| 操作符,避免在一行命令中转义双引号:

perl -MSocket -le "print unpack(qq|N|, inet_aton(qq|127.0.0.1|))"

对于不需要插值的内容,如格式字符串 N 和 IP 地址 127.0.0.1,你也可以使用单引号而不是双引号:

perl -MSocket -le "print unpack('N', inet_aton('127.0.0.1'))"

另一个技巧是使用 q/.../ 操作符,它会将斜杠之间的文本单引号化:

perl -MSocket -le "print unpack(q/N/, inet_aton(q/127.0.0.1/))"

q/N/q/127.0.0.1/ 与写 'N''127.0.0.1' 是一样的。

让我们将另一个 UNIX 的一行命令转换为 Windows。我已将它扩展为多行以便清晰展示:

perl -le '
  $ip="127.0.0.1";
  $ip =~ s/(\d+)\.?/sprintf("%02x", $1)/ge;
  print hex($ip)
'

不幸的是,要将其转换为 Windows,你需要将所有行连接起来(这样结果就不太易读了),并应用新的引用规则:

perl -le "$ip=\"127.0.0.1\"; $ip =~ s/(\d+)\.?/sprintf(\"%02x\", $1)/ge; print hex($ip)"

你也可以通过使用 qq 操作符稍微提高可读性:

perl -le "$ip=qq|127.0.0.1|; $ip =~ s/(\d+)\.?/sprintf(qq|%02x|, $1)/ge; print hex($ip)"

或者通过使用单引号:

perl -le "$ip='127.0.0.1'; $ip =~ s/(\d+)\.?/sprintf('%02x', $1)/ge; print hex($ip)"

符号挑战

你还可能会遇到一行命令中 ^ 符号的问题,因为 Windows 命令提示符将 ^ 作为转义符。为了让 Windows 字面上处理 ^ 符号,你通常需要将每个 ^ 替换为两个 ^^

让我们看几个简单的例子,看看如何打印 ^ 符号。下面是我的第一次尝试:

perl -e "print \"^\""

没有输出!^ 符号消失了。我们再试试输入 ^ 两次:

perl -e "print \"^^\""

成功了!它打印了 ^ 符号。现在让我们尝试使用单引号:

perl -e "print '^'"

这也成功了,打印了 ^,而且我不需要输入两次 ^。使用 qq/^/ 也能成功:

perl -e "print qq/^/"

正如你所见,在 Windows 上运行一行命令可能会有些棘手,因为没有统一的命令行参数解析规则。编写包含 %&<>| 符号的一行命令时,你可能会遇到类似的问题。如果是这样,可以尝试在这些符号前加上 ^ 转义字符,使 % 变成 ^%& 变成 ^&< 变成 ^<> 变成 ^>| 变成 ^|。或者,可以尝试将它们包裹在 qq 操作符中,正如我之前讨论的那样。(更好的方法是安装 win-bash 并通过它来运行一行命令,以避免所有这些问题。)

Windows 文件路径

使用 Windows 命令提示符时,你可以通过多种方式将文件名传递给单行命令。例如,要访问文件 C:\work\wrong-spacing.txt,你可以输入:

perl -pe "$\ = qq{\n}" C:\work\wrong-spacing.txt

或者你也可以反转斜杠:

perl -pe "$\ = qq{\n}" C:/work/wrong-spacing.txt

如果文件名包含空格,你必须对路径进行引号处理:

perl -pe "$\ = qq{\n}" "C:\Documents and Settings\wrong-spacing.txt"

更多 Windows Perl 使用技巧,请参见 Win32 Perl 文档:perldoc.perl.org/perlwin32.html

B.5 PowerShell 中的 Perl 单行命令

在 PowerShell 中运行单行命令与在命令提示符 (cmd.exe) 中运行略有不同。主要区别在于 PowerShell 是一种现代的 Shell 实现,其解析规则与命令提示符不同。在本节中,我将展示如何在 PowerShell 中运行 Perl 单行命令。

首先,你需要验证 Perl 是否在 PowerShell 环境中工作。你可以在 PowerShell 中运行 perl --version。如果命令输出了 Perl 的版本信息,则表示 Perl 可用,你应该能够运行单行命令。否则,更新 Path 环境变量,并通过以下命令将 Strawberry Perl 的二进制目录添加到其中:

$env:Path += ";C:\strawberryperl\perl\bin"

Path 变量告诉 PowerShell 去哪里查找可执行文件,因此当你运行 perl 时,它会搜索所有的目录(通过 ; 字符分隔),找到 perl.exe

在 PowerShell 中转换单行命令

参考单行命令 2.1(第 7 页),它将文件进行双倍空格处理。在 UNIX 中,单行命令看起来是这样的:

perl -pe '$\ = "\n"' *file*

要使这个单行命令在 PowerShell 中运行,你需要改动三个地方:

  • 通过在 $ 符号前添加 `(反引号)字符来转义 PowerShell 中用于变量的 $ 符号:`$

  • cmd.exe 命令提示符一样,请确保单行命令的外部使用双引号。

  • 使用 qq/.../ 运算符来处理单行命令中的双引号,如第 108 页“在 Windows 命令提示符中转换单行命令”一节所述。然而,你不能像在命令提示符中那样使用反斜杠转义双引号;你必须使用 qq/.../ 运算符。

当你将所有这些内容组合起来时,这个单行命令在 PowerShell 中的版本将变为:

perl -pe "`$\ = qq/\n/" file

要指定文件的完整路径,请使用 Windows 风格的路径。例如,要引用位于 C:\work\wrong-spacing.txt 的文件,可以直接在单行命令后输入该路径:

perl -pe "`$\ = qq/\n/" C:\work\wrong-spacing.txt

如果文件名或文件路径包含空格,请这样输入,路径周围加上双引号:

perl -pe "`$\ = qq/\n/" "C:\Documents and Settings\wrong-spacing.txt"

现在来看这个相同单行命令的另一个版本。在 UNIX 中,单行命令看起来是这样的:

perl -pe '$_ .= "\n" unless /^$/' *file*

但是在 PowerShell 中,你必须将外部的单引号改为双引号,转义 $ 符号,并将单行命令中的双引号改为 qq/.../

perl -pe "`$_ .= qq/\n/ unless /^`$/" *file*

现在让我们看看用于给文件中非空行编号的单行命令(第 18 页的单行命令 3.2):

perl -pe '$_ = ++$x." $_" if /./'

转换为 PowerShell 时,单行命令看起来像这样:

perl -pe "`$_ = ++`$a.qq/ `$_/ if /./"

那么检查一个数字是否是质数的艺术性单行命令(第 29 页的单行命令 4.1)怎么样?

perl -lne '(1x$_) !~ /¹?$|^(11+?)\1+$/ && print "$_ is prime"'

在 PowerShell 中,单行命令看起来是这样的:

perl -lne "(1x`$_) !~ /¹?`$|^(11+?)\1+`$/ && print qq/`$_ is prime/"

记得第 46 页提到的将 IP 转换为整数的一行命令吗?这是它在 UNIX 中的写法:

perl -le '
  $ip="127.0.0.1";
  $ip =~ s/(\d+)\.?/sprintf("%02x", $1)/ge;
  print hex($ip)
'

这是 PowerShell 中相同的一行命令:

perl -le "
  `$ip=qq|127.0.0.1|;
  `$ip =~ s/(\d+)\.?/sprintf(qq|%02x|, `$1)/ge;
  print hex(`$ip)
"

PowerShell 3.0+ 中的一行命令

如果你运行的是 PowerShell 3.0 或更高版本,你可以使用 --% 转义序列来防止 PowerShell 进行额外的解析。

要查看你正在运行的 PowerShell 版本,在命令行输入 $PSVersionTable.PSVersion。它应该输出如下表格:

PS C:\Users\Administrator> $PSVersionTable.PSVersion
Major  Minor  Build  Revision
-----  -----  -----  --------
3      0      -1     -1

该表格显示你正在运行 PowerShell 3.0 版本,支持 --% 转义序列。(旧版本的 PowerShell 不支持此序列,这种情况下你必须使用我之前描述的技巧。)

使用 --% 转义序列时,你不需要转义 $ 符号。它还允许你在一行命令中使用反斜杠转义双引号。例如,以下是使用 --% 转义序列的双倍行间距命令:

perl --% -pe "$\ = \"\n\""

你还可以使用 qq/.../ 运算符来避免在一行命令中转义双引号:

perl --% -pe "$\ = qq/\n/"

这是你在 PowerShell 3.0 或更高版本中编写相同一行命令的另一种方式:

perl --% -pe "$_ .= \"\n\" unless /^$/" *file*

这是给行编号的一行命令的写法:

perl --% -pe "$_ = ++$a.qq/ $_/ if /./"

这是使用正则表达式判断一个数是否为质数的一行命令:

perl --% -lne "(1x$_) !~ /¹?$|^(11+?)\1+$/ && print \"$_ is prime\""

这是将 IP 转换为整数的一行命令:

perl --% -le "
  $ip=\"127.0.0.1\";
  $ip =~ s/(\d+)\.?/sprintf(\"%02x\", $1)/ge;
  print hex($ip)
"

如你所见,在 PowerShell 中运行一行命令相当棘手,并且需要一些变通方法。再次推荐你按照第 106 页的“Windows 上的 Bash”部分安装 win-bash,以避免必须实现这些变通方法。

附录 C. Perl1Line.Txt

当我写这本书时,我将所有的单行代码汇编到一个名为perl1line.txt的文件中。本附录就是该文件。当你需要快速查找一个单行代码时,它非常方便。你只需在文本编辑器中打开perl1line.txt并搜索你想要执行的操作。此文件的最新版本可以在* www.catonmat.net/download/perl1line.txt *找到。

C.1 间距

双倍行距一个文件

perl -pe '$\ = "\n"'
perl -pe 'BEGIN { $\ = "\n" }'
perl -pe '$_ .= "\n"'
perl -pe 's/$/\n/'
perl -nE 'say'

双倍行距一个文件,排除空白行

perl -pe '$_ .= "\n" unless /^$/'
perl -pe '$_ .= "\n" if /\S/'

三倍行距一个文件

perl -pe '$\ = "\n\n"'
perl -pe '$_ .= "\n\n"'
perl -pe 's/$/\n\n/'

N-space 一个文件

perl -pe '$_ .= "\n"x7'

每行前添加一个空白行

perl -pe 's/^/\n/'

删除所有空白行

perl -ne 'print unless /^$/'
perl -lne 'print if length'
perl -ne 'print if /\S/'

删除所有连续的空白行,仅保留一行

perl -00 -pe ''
perl -00pe0

压缩/展开所有空白行为 N 个连续的行

perl -00 -pe '$_ .= "\n"x2'

在所有单词之间双倍行距

perl -pe 's/ /  /g'

删除所有单词之间的间距

perl -pe 's/ +//g'
perl -pe 's/\s+//g'

将所有单词之间的间距改为一个空格

perl -pe 's/ +/ /g'

在所有字符之间插入一个空格

perl -lpe 's// /g'

C.2 编号

给文件中的所有行编号

perl -pe '$_ = "$. $_"'
perl -ne 'print "$. $_"'

仅给文件中非空行编号

perl -pe '$_ = ++$x." $_" if /./'
perl -pe '$_ = ++$x." $_" if /\S/'

给文件中的非空行编号并打印(删除空行)

perl -ne 'print ++$x." $_" if /./'

给所有行编号,但仅对非空行打印行号

perl -pe '$_ = "$. $_" if /./'

只给匹配模式的行编号;其他行保持不变

perl -pe '$_ = ++$x." $_" if /regex/'

仅对匹配模式的行进行编号和打印

perl -ne 'print ++$x." $_" if /regex/'

给所有行编号,但仅对匹配模式的行打印行号

perl -pe '$_ = "$. $_" if /regex/'

使用自定义格式给文件中的所有行编号

perl -ne 'printf "%-5d %s", $., $_'

打印文件中的总行数(模拟 wc -l)

perl -lne 'END { print $. }'
perl -le 'print $n = () = <>'
perl -le 'print $n = (() = <>)'
perl -le 'print scalar(() = <>)'
perl -le 'print scalar(@foo = <>)'
perl -ne '}{print $.'

打印文件中非空行的数量

perl -le 'print scalar(grep { /./ } <>)'
perl -le 'print ~~grep{/./}<>'
perl -le 'print~~grep/./,<>'
perl -lE 'say~~grep/./,<>'

打印文件中空白行的数量

perl -lne '$x++ if /^$/; END { print $x+0 }'
perl -lne '$x++ if /^$/; END { print int $x }'
perl -le 'print scalar(grep { /^$/ } <>)'
perl -le 'print ~~grep{ /^$/ } <>'

打印匹配模式的行数(模拟 grep -c)

perl -lne '$x++ if /regex/; END { print $x+0 }'

计算所有行的单词数

perl -pe 's/(\w+)/++$i.".$1"/ge'

给每行的单词编号

perl -pe '$i=0; s/(\w+)/++$i.".$1"/ge'

用单词的数字位置替换所有单词

perl -pe 's/(\w+)/++$i/ge'

C.3 计算

检查一个数字是否是质数

perl -lne '(1x$_) !~ /¹?$|^(11+?)\1+$/ && print "$_ is prime"'

打印每行所有字段的总和

perl -MList::Util=sum -alne 'print sum @F'
perl -MList::Util=sum -F: -alne 'print sum @F'

打印所有行中字段的总和

perl -MList::Util=sum -alne 'push @S,@F; END { print sum @S }'
perl -MList::Util=sum -alne '$s += sum @F; END { print $s }'

打乱每行上的所有字段

perl -MList::Util=shuffle -alne 'print "@{[shuffle @F]}"'
perl -MList::Util=shuffle -alne 'print join " ", shuffle @F'

查找每行中数值最小的元素(最小元素)

perl -MList::Util=min -alne 'print min @F'

查找所有行中数值最小的元素(最小元素)

perl -MList::Util=min -alne '@M = (@M, @F); END { print min @M }'
perl -MList::Util=min -alne '
  $min = min @F;
  $rmin = $min unless defined $rmin && $min > $rmin;
  END { print $rmin }
'
perl -MList::Util=min -alne '$min = min($min // (), @F); END { print $min }'

查找每行中数值最大的元素(最大元素)

perl -MList::Util=max -alne 'print max @F'

查找所有行中数值最大的元素(最大元素)

perl -MList::Util=max -alne '@M = (@M, @F); END { print max @M }'
perl -MList::Util=max -alne '
  $max = max @F;
  $rmax = $max unless defined $rmax && $max < $rmax;
  END { print $rmax }
'
perl -MList::Util=max -alne '$max = max($max // (), @F); END { print $max }'

将每个字段替换为其绝对值

perl -alne 'print "@{[map { abs } @F]}"'

打印每行的字段总数

perl -alne 'print scalar @F'

打印每行的字段总数,后跟该行

perl -alne 'print scalar @F, " $_"'

打印所有行中字段的总数

perl -alne '$t += @F; END { print $t }'

打印与模式匹配的字段的总数

perl -alne 'map { /regex/ && $t++ } @F; END { print $t || 0 }'
perl -alne '$t += /regex/ for @F; END { print $t }'
perl -alne '$t += grep /regex/, @F; END { print $t }'

打印与模式匹配的行的总数

perl -lne '/regex/ && $t++; END { print $t || 0 }'

打印数字π

perl -Mbignum=bpi -le 'print bpi(21)'
perl -Mbignum=PI -le 'print PI'

打印数字 e

perl -Mbignum=bexp -le 'print bexp(1,21)'
perl -Mbignum=e -le 'print e'

打印 UNIX 时间(自 1970 年 1 月 1 日 00:00:00 UTC 以来的秒数)

perl -le 'print time'

打印格林威治标准时间和本地计算机时间

perl -le 'print scalar gmtime'
perl -le 'print scalar localtime'

打印昨天的日期

perl -MPOSIX -le '
  @now = localtime;
  $now[3] -= 1;
  print scalar localtime mktime @now
'

打印 14 个月 9 天 7 秒前的日期

perl -MPOSIX -le '
  @now = localtime;
  $now[0] -= 7;
  $now[3] -= 9;
  $now[4] -= 14;
  print scalar localtime mktime @now
'

计算阶乘

perl -MMath::BigInt -le 'print Math::BigInt->new(5)->bfac()'
perl -le '$f = 1; $f *= $_ for 1..5; print $f'

计算最大公约数

perl -MMath::BigInt=bgcd -le 'print bgcd(@list_of_numbers)'
perl -MMath::BigInt=bgcd -le 'print bgcd(20,60,30)'
perl -MMath::BigInt=bgcd -anle 'print bgcd(@F)'
perl -le '
  $n = 20; $m = 35;
  ($m,$n) = ($n,$m%$n) while $n;
  print $m
'

计算最小公倍数

perl -MMath::BigInt=blcm -le 'print blcm(35,20,8)'
perl -MMath::BigInt=blcm -anle 'print blcm(@F)'
perl -le '
  $a = $n = 20;
  $b = $m = 35;
  ($m,$n) = ($n,$m%$n) while $n;
  print $a*$b/$m
'

生成 10 个 5 到 15 之间的随机数(不包括 15)

perl -le 'print join ",", map { int(rand(15-5))+5 } 1..10'
perl -le '
  $n=10;
  $min=5;
  $max=15;
  $, = " ";
  print map { int(rand($max-$min))+$min } 1..$n;
'

生成一个列表的所有排列

perl -MAlgorithm::Permute -le '
  $l = [1,2,3,4,5];
  $p = Algorithm::Permute->new($l);
  print "@r" while @r = $p->next
'
 perl -MAlgorithm::Permute -le '
  @l = (1,2,3,4,5);
  Algorithm::Permute::permute { print "@l" } @l
'

生成幂集

perl -MList::PowerSet=powerset -le '
  @l = (1,2,3,4,5);
  print "@$_" for @{powerset(@l)}
'

将 IP 地址转换为无符号整数

perl -le '
  $i=3;
  $u += ($_<<8*$i--) for "127.0.0.1" =~ /(\d+)/g;
  print $u
'
 perl -le '
  $ip="127.0.0.1";
  $ip =~ s/(\d+)\.?/sprintf("%02x", $1)/ge;
  print hex($ip)
'
 perl -le 'print unpack("N", 127.0.0.1)'
 perl -MSocket -le 'print unpack("N", inet_aton("127.0.0.1"))'

将无符号整数转换为 IP 地址

perl -MSocket -le 'print inet_ntoa(pack("N", 2130706433))'
perl -le '
  $ip = 2130706433;
  print join ".", map { (($ip>>8*($_))&0xFF) } reverse 0..3
'
perl -le '
  $ip = 2130706433;
  $, = ".";
  print map { (($ip>>8*($_))&0xFF) } reverse 0..3
'
perl -le '
  $ip = 2130706433;
  $, = ".";
  print map { (($ip>>8*($_))&0xFF) } 3,2,1,0
'

C.4 处理数组和字符串

生成并打印字母表

perl -le 'print a..z'
perl -le 'print ("a".."z")'
perl -le '$, = ","; print ("a".."z")'
perl -le '$alphabet = join ",", ("a".."z"); print $alphabet'

生成并打印从“a”到“zz”的所有字符串

perl -le 'print join ",", ("a".."zz")'
perl -le 'print join ",", "aa".."zz"'

创建一个十六进制查找表

@hex = (0..9, "a".."f")
perl -le '
  $num = 255;
  @hex = (0..9, "a".."f");
  while ($num) {
    $s = $hex[($num % 16)].$s;
    $num = int $num/16;
  }
  print $s
'
perl -le 'printf("%x", 255)'
perl -le '$num = "ff"; print hex $num'

生成一个随机的八字符密码

perl -le 'print map { ("a".."z")[rand 26] } 1..8'
perl -le 'print map { ("a".."z", 0..9)[rand 36] } 1..8'

创建一个特定长度的字符串

perl -le 'print "a"x50'
perl -e 'print "a"x1024'
perl -le '@list = (1,2)x20; print "@list"'

从字符串创建一个数组

@months = split ' ', "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"
@months = qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/

从命令行参数创建一个字符串

perl -le 'print "(", (join ",", @ARGV), ")"' val1 val2 val3
perl -le '
  print "INSERT INTO table VALUES (", (join ",", @ARGV), ")"
' val1 val2 val3

查找字符串中字符的数字值

perl -le 'print join ", ", map { ord } split //, "hello world"'
perl -le 'print join ", ", unpack("C*", "hello world")'
perl -le '
  print join ", ", map { sprintf "0x%x", ord $_ } split //, "hello world"
'
perl -le '
  print join ", ", map { sprintf "%o", ord $_ } split //, "hello world"
'
perl -le '
  print join ", ", map { sprintf "%#o", ord $_ } split //, "hello world"
'

将一组数字 ASCII 值转换为字符串

perl -le '
  @ascii = (99, 111, 100, 105, 110, 103);
  print pack("C*", @ascii)
'
perl -le '
  @ascii = (99, 111, 100, 105, 110, 103);
  $str = join "", map chr, @ascii;
  print $str
'
perl -le 'print map chr, 99, 111, 100, 105, 110, 103'
perl -le 'print map chr, @ARGV' 99 111 100 105 110 103

生成一个包含从 1 到 100 的奇数的数组

perl -le '@odd = grep {$_ % 2 == 1} 1..100; print "@odd"'
perl -le '@odd = grep { $_ & 1 } 1..100; print "@odd"'

生成一个包含从 1 到 100 的偶数的数组

perl -le '@even = grep {$_ % 2 == 0} 1..100; print "@even"'

查找字符串的长度

perl -le 'print length "one-liners are great"'

查找数组中元素的数量

perl -le '@array = ("a".."z"); print scalar @array'
perl -le '@array = ("a".."z"); print $#array + 1'
perl -le 'print scalar @ARGV' *.txt
perl -le 'print scalar (@ARGV=<*.txt>)'

C.5 文本转换与替换

对字符串进行 ROT13 加密

perl -le '$str = "bananas"; $str =~ y/A-Za-z/N-ZA-Mn-za-m/; print $str'
perl -lpe 'y/A-Za-z/N-ZA-Mn-za-m/' file
perl -pi.bak -e 'y/A-Za-z/N-ZA-Mn-za-m/' file

对字符串进行 Base64 编码

perl -MMIME::Base64 -e 'print encode_base64("string")'
perl -MMIME::Base64 -0777 -ne 'print encode_base64($_)' file

对字符串进行 Base64 解码

perl -MMIME::Base64 -le 'print decode_base64("base64string")'
perl -MMIME::Base64 -0777 -ne 'print decode_base64($_)' file

对字符串进行 URL 编码

perl -MURI::Escape -le 'print uri_escape("http://example.com")'

对字符串进行 URL 解码

perl -MURI::Escape -le 'print uri_unescape("http%3A%2F%2Fexample.com")'

对字符串进行 HTML 编码

perl -MHTML::Entities -le 'print encode_entities("<html>")'

对字符串进行 HTML 解码

perl -MHTML::Entities -le 'print decode_entities("&lt;html&gt;")'

将所有文本转换为大写

perl -nle 'print uc'
perl -ple '$_ = uc'
perl -nle 'print "\U$_"'

将所有文本转换为小写

perl -nle 'print lc'
perl -nle 'print "\L$_"'

仅将每行的首字母大写

perl -nle 'print ucfirst lc'
perl -nle 'print "\u\L$_"'

反转字母大小写

perl -ple 'y/A-Za-z/a-zA-Z/'

将每一行标题化

perl -ple 's/(\w+)/\u$1/g'

去除每行开头的空白字符(空格、制表符)

perl -ple 's/^[ \t]+//'
perl -ple 's/^\s+//'

去除每行结尾的空白字符(空格、制表符)

perl -ple 's/[ \t]+$//'
perl -ple 's/\s+$//'

去除每行开头和结尾的空白字符(空格、制表符)

perl -ple 's/^[ \t]+|[ \t]+$//g'
perl -ple 's/^\s+|\s+$//g'

将 UNIX 换行符转换为 DOS/Windows 换行符

perl -pe 's|\012|\015\012|'

将 DOS/Windows 换行符转换为 UNIX 换行符

perl -pe 's|\015\012|\012|'

将 UNIX 换行符转换为 Mac 换行符

perl -pe 's|\012|\015|'

在每一行上将“foo”替换为“bar”

perl -pe 's/foo/bar/'
perl -pe 's/foo/bar/g'

在匹配“baz”的行上,将“foo”替换为“bar”

perl -pe '/baz/ && s/foo/bar/'
perl -pe 's/foo/bar/ if /baz/'

以倒序打印段落

perl -00 -e 'print reverse <>' file

打印所有行的倒序

perl -lne 'print scalar reverse $_'
perl -lne 'print scalar reverse'
perl -lpe '$_ = reverse $_'
perl -lpe '$_ = reverse'

以倒序打印列

perl -alne 'print "@{[reverse @F]}"'
perl -F: -alne 'print "@{[reverse @F]}"'
perl -F: -alne '$" = ":"; print "@{[reverse @F]}"'

C.6 有选择地打印和删除行

打印文件的第一行(模拟 head -1)

perl -ne 'print; exit' file
perl -i -ne 'print; exit' file
perl -i.bak -ne 'print; exit' file

打印文件的前 10 行(模拟 head -10)

perl -ne 'print if $. <= 10' file
perl -ne '$. <= 10 && print' file
perl -ne 'print if 1..10' file
perl -ne 'print; exit if $. == 10' file

打印文件的最后一行(模拟 tail -1)

perl -ne '$last = $_; END { print $last }' file
perl -ne 'print if eof' file

打印文件的最后 10 行(模拟 tail -10)

perl -ne 'push @a, $_; @a = @a[@a-10..$#a] if @a>10; END { print @a }' file
perl -ne 'push @a, $_; shift @a if @a>10; END { print @a }' file

仅打印匹配正则表达式的行

perl -ne '/regex/ && print'
perl -ne 'print if /regex/'

仅打印不匹配正则表达式的行

perl -ne '!/regex/ && print'
perl -ne 'print if !/regex/'
perl -ne 'print unless /regex/'
perl -ne '/regex/ || print'

打印匹配正则表达式之前的每一行

perl -ne '/regex/ && $last && print $last; $last = $_'

打印匹配正则表达式之后的每一行

perl -ne 'if ($p) { print; $p = 0 } $p++ if /regex/'
perl -ne '$p && print && ($p = 0); $p++ if /regex/'
perl -ne '$p && print; $p = /regex/'

打印匹配正则表达式 AAA 和 BBB 的所有行,顺序不限

perl -ne '/AAA/ && /BBB/ && print'

打印不匹配正则表达式 AAA 和 BBB 的行

perl -ne '!/AAA/ && !/BBB/ && print'

打印匹配正则表达式 AAA 后跟 BBB 后跟 CCC 的行

perl -ne '/AAA.*BBB.*CCC/ && print'

打印至少 80 个字符长的行

perl -ne 'print if length >= 80'
perl -lne 'print if length >= 80'

打印所有长度小于 80 个字符的行

perl -ne 'print if length() < 80'

仅打印第 13 行

perl -ne '$. == 13 && print && exit'

打印除第 27 行外的所有行

perl -ne '$. != 27 && print'
perl -ne 'print if $. != 27'
perl -ne 'print unless $. == 27'

仅打印第 13 行、第 19 行和第 67 行

perl -ne 'print if $. == 13 || $. == 19 || $. == 67'
perl -ne '
  @lines = (13, 19, 88, 290, 999, 1400, 2000);
  print if grep { $_ == $. } @lines
'

打印第 17 到 30 行的所有内容

perl -ne 'print if $. >= 17 && $. <= 30'
perl -ne 'print if 17..30'

打印两个正则表达式之间的所有行(包括匹配的行)

perl -ne 'print if /regex1/../regex2/'

打印最长的行

perl -ne '
  $l = $_ if length($_) > length($l);
  END { print $l }
'

打印最短的行

perl -ne '
  $s = $_ if $. == 1;
  $s = $_ if length($_) < length($s);
  END { print $s }
'

打印所有包含数字的行

perl -ne 'print if /\d/'

打印所有仅包含数字的行

perl -ne 'print if /^\d+$/'
perl -lne 'print unless /\D/'

打印所有仅包含字母的行

perl -ne 'print if /^[[:alpha:]]+$/

打印每隔一行的内容

perl -ne 'print if $. % 2'

打印每隔一行的内容,从第二行开始

perl -ne 'print if $. % 2 == 0'
perl -ne 'print unless $. % 2'

仅打印所有重复的行一次

perl -ne 'print if ++$a{$_} == 2'

打印所有唯一的行

perl -ne 'print unless $a{$_}++'

C.7 有用的正则表达式

匹配看起来像 IP 地址的内容

/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
/^(\d{1,3}\.){3}\d{1,3}$/
perl -ne 'print if /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/'

测试一个数字是否在 0 到 255 的范围内

/^([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/
perl -le '
  map { $n++ if /^([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ } 0..255;
  END { print $n }
'
perl -le '
  map { $n++ if /^([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$/ } 0..1000;
  END { print $n }
'

匹配 IP 地址

my $ip_part = qr/[0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5]/;
if ($ip =~ /^$ip_part\.$ip_part\.$ip_part\.$ip_part$/) {
  print "valid ip\n";
}
if ($ip =~ /^($ip_part\.){3}$ip_part$/) {
  print "valid ip\n";
}
perl -ne '
  $ip_part = qr|([0-9]|[0-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])|;
  print if /^($ip_part\.){3}$ip_part$/
'

检查一个字符串是否像电子邮件地址

/\S+@\S+\.\S+/
use Email::Valid;
print Email::Valid->address('cats@catonmat.net') ? 'valid email' : 'invalid email';
perl -MEmail::Valid -ne 'print if Email::Valid->address($_)'

检查一个字符串是否是数字

/^\d+$/
/^[+-]?\d+$/
/^[+-]?\d+\.?\d*$/
perl -MRegexp::Common -ne 'print if /$RE{num}{real}/'
perl -MRegexp::Common -ne 'print if /$RE{num}{hex}/'
perl -MRegexp::Common -ne 'print if /$RE{num}{oct}/'
perl -MRegexp::Common -ne 'print if /$RE{num}{bin}/'

检查一个单词在字符串中是否出现两次

/(word).*\1/

将字符串中的所有整数增加一

$str =~ s/(\d+)/$1+1/ge
perl -MRegexp::Common -pe 's/($RE{num}{real})/$1+1/ge'

从 HTTP 头中提取 HTTP 用户代理字符串

/^User-Agent: (.+)$/

匹配可打印的 ASCII 字符

/[ -~]/

提取两个 HTML 标签之间的文本

m|<strong>([^<]*)</strong>|
m|<strong>(.*?)</strong>|

将所有标签替换为

$html =~ s|<(/)?b>|<$1strong>|g

从正则表达式中提取所有匹配项

my @matches = $text =~ /regex/g;
posted @ 2025-11-27 09:16  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报