Perl / Unix 一行脚本对决,第一部分


shell(如Bash)提供了内置命令和脚本功能,可以轻松解决和自动化各种任务。grep、sed、Awk、sort、find或parallel等外部命令可以组合在一起协同工作。有时,您可以使用Perl作为单一替代品或特定用例的补充。

Perl是满足文本处理需求的最高效、最便携的选项。Perl具有功能丰富的正则表达式引擎、内置函数、广泛的生态系统,并且相当便携。然而,与专用工具相比,Perl可能性能较慢,并且可能更冗长。

一行脚本还是脚本?

为了对数字信号处理(DSP)芯片进行汇编级测试,我需要为多个地址范围复制相同的场景。当时我对Linux命令行的了解有限,不知道如何使用sed或Awk。我使用Vim和Perl来处理各种文本处理需求。

我不知道Perl的一行脚本选项,所以我每次需要为多个文件进行替换时,都会修改脚本。有一次,我甚至将文件作为Vim缓冲区打开,并应用bufdo命令,看看这会使我的工作流程更简单。如果我知道Perl的一行脚本,我可以轻松利用find和Bash通配符使我的生活变得更简单,例如

$ perl -i -pe 's/0xABCD;/0x1234;/; s/0xDEAD;/0xBEEF;/' *.tests

-i选项会将更改写回源文件。如果需要,我可以传递一个参数来创建原始文件的备份。例如,-i.bkp将为传递作为输入文件的ip.txt创建备份ip.txt.bkp。我还可以将备份放在另一个现有目录中。通配符*将被扩展为原始文件名

$ mkdir backups
$ perl -i'backups/*' -pe 's/SEARCH/REPLACE/g' *.txt

强大的正则表达式功能

Perl的正则表达式比工具程序使用的基本或扩展正则表达式更强大。我经常使用的常见功能是非贪婪和占有性量词、前瞻、/e标志、子表达式调用和(*SKIP)(*FAIL)。以下是多年来我回答StackOverflow帖子中的某些示例。

跳过一些匹配项

这个问题需要将avr-asm转换为arm-gnu注释。起始文件看起来像这样

ABC r1,';'
ABC r1,";" ; comment
  ;;;

我需要将;更改为@,但单引号或双引号内的;不应受到影响。我可以匹配选择支的第一分支中的引号内的;,并使用(*SKIP)(*F)来不替换这些

$ perl -pe 's/(?:\x27;\x27|";")(*SKIP)(*F)|;/@/' ip.txt
ABC r1,';'
ABC r1,";" @ comment
  @;;

我经常使用(*SKIP)(*F),所以我希望它有一个更短的语法,例如(*SF)

使用递增值替换字符串

我可以使用递增值替换字符串。替换中的/e允许我将替换侧视为Perl代码。该代码评估的结果是替换内容。这可能是一个我递增的变量

$ echo 'a | a | a | a | a | a | a | a' | perl -pe 's/ *\| */$i++/ge'
a0a1a2a3a4a5a6a

反转子字符串

我还使用/e技巧反转与模式匹配的文本

$ echo 'romarana789:qwerty12543' | perl -pe 's/\d+$/reverse $&/e'
romarana789:qwerty34521

进行一些算术运算

将另一个/e改为/ee意味着有两轮Perl代码。我将替换侧评估为字符串,然后将其评估为Perl代码。在文本文件中的算术替换中,我需要找到简单的算术,如25100+10,并将其替换为其算术结果

id=25100+10
xyz=1+
abc=123456
conf_string=LMN,J,IP,25100+1,0,3,1

我可以通过匹配数字并在替换侧进行一些Perl操作,用一行/e做到这一点

$ perl -pe 's/(\d+)\+(\d+)/$1+$2/ge' ip.txt
id=25110
xyz=1+
abc=123456
conf_string=LMN,J,IP,25101,0,3,1

但是,而不是分别匹配数字,我可以匹配整个表达式。匹配结果是 $&,所以第一个 /e 将其插值到 25100+10。第二轮运行的是 Perl,这是加法。

$ perl -pe 's/\d+\+\d+/$&/gee' ip.txt
id=25110
xyz=1+
abc=123456
conf_string=LMN,J,IP,25101,0,3,1

这也会使处理一组运算符变得更容易。

$ echo '2+3 10-3 8*8 11/5' | perl -pe 's|\d+[+/*-]\d+|$&|gee'
5 7 64 2.2

处理换行符。

我想取消这个文本的格式。

Hello there.
It will rain to-
day. Have a safe
and pleasant jou-
rney.

与 sed 和 Awk 不同,您可以选择在 Perl 中保留记录分隔符。这使得解决这个问题变得更容易。

$ perl -pe 's/-\n//' msg.txt
Hello there.
It will rain today. Have a safe
and pleasant journey.

请参阅 删除破折号并将换行符替换为空格,了解类似的问题,并比较 Perl 解决方案与 sed/Awk。

多行固定字符串替换。

使用 Perl 内置功能转义正则表达式元字符更简单。结合将整个输入文件作为单个字符串“吸入”,我可以轻松执行多行固定字符串替换。考虑以下示例输入。

This is a multiline
sample input with lots
of special characters
like . () * [] $ {}
^ + ? \ and ' and so on.

假设你有一个包含你希望匹配的行的文件。

like . () * [] $ {}
^ + ? \ and ' and so on.

还有一个包含替换字符串的文件。

---------------------
$& = $1 + $2 / 3 \ 4
=====================

以下是使用 Perl 实现此方法的一种方法。

$ perl -0777 -ne '$#ARGV==1 ? $s=$_ : $#ARGV==0 ? $r=$_ :
                  print s/\Q$s/$r/gr' search.txt replace.txt ip.txt
This is a multiline
sample input with lots
of special characters
---------------------
$& = $1 + $2 / 3 \ 4
=====================

请注意,在上述解决方案中,search.txtreplace.txt 的内容也由 Perl 命令处理。避免使用 shell 变量来保存它们的内容,因为尾随换行符和 ASCII NUL 字符需要特别注意。

Awk 和 sed 没有等效选项来“吸入”整个输入文件内容。Sed 是图灵完备的,Awk 是一种编程语言,因此如果您愿意,可以为其编写代码,除了您需要用于转义元字符的代码之外。

更好的正则表达式支持。

一些其他正则表达式库可能存在与它们实现方式相关的问题。例如,GNU 版本可能存在一些其他实现可能不存在的错误。您使用的版本可能会产生不同的结果。然而,Perl 在任何地方都有相同的错误。

后向引用。

我发现了一个关于 glibc 中的后向引用的问题,并已在 grep 中进行了报告。这个问题在至少 GNU 实现的 grep 和 sed 中可以看到。据我所知,没有任何 Awk 实现支持正则表达式定义中的后向引用。

我想获取具有两个连续重复字符出现的单词。这个例子需要一些时间,但没有输出。

$ grep -xiE '([a-z]*([a-z])\2[a-z]*){2}' /usr/share/dict/words

当展开嵌套或使用 PCRE 时,它确实工作。

$ grep -xiE '[a-z]*([a-z])\1[a-z]*([a-z])\2[a-z]*' /usr/share/dict/words
Abbott
Annabelle
...

$ grep -xiP '([a-z]*([a-z])\2[a-z]*){2}' /usr/share/dict/words
Abbott
Annabelle
...

以下是 Perl,这是原始的正则表达式。

$ perl -ne 'print if /^([a-z]*([a-z])\2[a-z]*){2}$/i' /usr/share/dict/words
Abbott
Annabelle
...

单词边界。

为什么这个 sed 命令不替换倒数第三个“and”? 展示了当涉及到单词边界和组重复时另一个有趣的错误。这个错误在 Linux 上使用 glibc 的正则表达式功能(如)的任何东西中都可以看到。

这会错误地匹配,因为在“cocoa”的中间没有单词边界。

$ sed --version
sed (GNU sed) 4.8
$ echo 'cocoa' | sed -nE '/(\bco){2}/p'
cocoa

没有量词,就没有问题,也没有匹配。

$ echo 'cocoa' | sed -nE '/\bco\bco/p'
$ echo 'cocoa' | perl -ne 'print if /(\bco){2}/'

以下是 GNU sed 的另一个示例。它修改了这一行,因为它认为它在“with”之后找到了“it”作为一个单独的词两次,但第二个实际上在“sit”的中间。

$ echo 'it line with it here sit too' | sed -E 's/with(.*\bit\b){2}/XYZ/'
it line XYZ too

将模式更改为消除量词,它就可以正常工作了。

$ echo 'it line with it here sit too' | sed -E 's/with.*\bit\b.*\bit\b/XYZ/'
it line with it here sit too
$ echo 'it line with it here sit too it a' | sed -E 's/with.*\bit\b.*\bit\b/XYZ/'
it line XYZ a

# Perl doesn't need such workarounds
$ echo 'it line with it here sit too' | perl -pe 's/with(.*\bit\b){2}/XYZ/'
it line with it here sit too
$ echo 'it line with it here sit too it a' | perl -pe 's/with(.*\bit\b){2}/XYZ/'
it line XYZ a

敬请期待。

在第二部分中,我将深入了解 XML、JSON 和 CSV,届时我将有更多内容。

其他阅读内容。


图片来自 Flickr 上的 Dim Sum! (CC BY-NC-ND 2.0)

标签

苏迪普·阿加瓦尔

苏迪普·阿加瓦尔沉迷于编写书籍和阅读小说(主要是奇幻和科幻)。

浏览他们的文章

反馈

这篇文章有什么问题吗?请在GitHub上打开一个问题或拉取请求来帮助我们。