我会认真回答。我不知道有任何程序可以将 shell 脚本翻译成 Perl,而且我怀疑任何解释器模块都会提供性能优势。所以我将概述我将如何去做。
现在,您希望尽可能多地重用您的代码。在这种情况下,我建议选择该代码的片段,编写该代码的 Perl 版本,然后从主脚本调用 Perl 脚本。这将使您能够以小步骤进行转换,断言转换后的部分正在工作,并逐渐提高您的 Perl 知识。
由于您可以从 Perl 脚本调用外部程序,因此您甚至可以用 Perl 替换一些更大的逻辑,并从 Perl 调用更小的 shell 脚本(或其他命令)来做一些您还不愿意转换的事情。因此,您将有一个 shell 脚本调用一个 perl 脚本调用另一个 shell 脚本。而且,事实上,我用我自己的第一个 Perl 脚本就是这样做的。
当然,选择好要转换的内容很重要。我将在下面解释 shell 脚本中常见的模式有多少是用 Perl 编写的,以便您可以在脚本中识别它们,并通过尽可能多的剪切和粘贴来创建替换。
首先,Perl 脚本和 Shell 脚本都是代码+函数。即,任何不是函数声明的东西都按照遇到的顺序执行。不过,您不需要在使用前声明函数。这意味着可以保留脚本的总体布局,尽管将内容保存在内存中(如整个文件或它的处理形式)的能力可以简化任务。
一个 Perl 脚本,在 Unix 中,开头是这样的:
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
#other libraries
(rest of the code)
显然,第一行指向用于运行脚本的命令,就像普通的 shell 一样。以下两个“使用”行使语言更加严格,这应该会减少您遇到的错误数量,因为您不太了解该语言(或明显做错了什么)。第三个 use 行导入“Data”模块的“Dumper”函数。它对于调试目的很有用。如果您想知道数组或哈希表的值,只需打印 Dumper(whatever)。
还要注意,cmets 就像 shell 一样,行以“#”开头。
现在,您调用外部程序并通过管道传输或从它们传输。例如:
open THIS, "cat $ARGV[0] |";
这将运行 cat,传递“$ARGV[0]”,这将是 shell 上的 $1——传递给它的第一个参数。其结果将通过“THIS”传送到您的 Perl 脚本中,您可以使用它从中读取结果,稍后我将展示。
您可以使用“|”在行首或行尾,表示模式“pipe to”或“pipe from”,并指定要运行的命令,也可以在开头使用“>”或“>>”,打开一个用于写入的文件,带或不带截断,“”用于读取和写入。请注意,后者将首先截断文件。
“open”的另一种语法可以避免文件名称中包含此类字符的问题,它将打开模式作为第二个参数:
open THIS, "-|", "cat $ARGV[0]";
这将做同样的事情。模式“-|”代表“管道来自”,“|-”代表“管道到”。其余模式可以照原样使用 (>, >>, <, +>, +<)。虽然还有更多要打开的内容,但对于大多数事情来说应该足够了。
但您应该尽可能避免调用外部程序。你可以直接打开文件,比如open THIS, "$ARGV[0]";,性能会更好。
那么,您可以删除哪些外部程序?好吧,几乎所有东西。但让我们保持基础:cat、grep、cut、head、tail、uniq、wc、sort。
猫
嗯,关于这个,没什么好说的。请记住,如果可能的话,只读取一次文件并将其保存在内存中。如果文件很大,当然不会这样做,但几乎总有办法避免多次读取文件。
无论如何,cat 的基本语法是:
my $filename = "whatever";
open FILE, "$filename" or die "Could not open $filename!\n";
while(<FILE>) {
print $_;
}
close FILE;
这会打开一个文件,并打印它的所有内容(“while(<FILE>)”将循环到 EOF,将每一行分配给“$_”),然后再次关闭它。
如果我想将输出定向到另一个文件,我可以这样做:
my $filename = "whatever";
my $anotherfile = "another";
open (FILE, "$filename") || die "Could not open $filename!\n";
open OUT, ">", "$anotherfile" or die "Could not open $anotherfile for writing!\n";
while(<FILE>) {
print OUT $_;
}
close FILE;
这会将行打印到由“OUT”指示的文件中。您也可以在适当的地方使用STDIN、STDOUT 和STDERR,而无需先打开它们。实际上,“print”默认为STDOUT,“die”默认为“STDERR”。
还要注意“or die ...”和“|| die ...”。运算符or 和|| 表示只有第一个返回false(即空字符串、空引用、0 等)时才会执行以下命令。 die 命令停止脚本并显示错误消息。
“or”和“||”之间的主要区别在于优先级。如果在上面的示例中将“or”替换为“||”,它将无法按预期工作,因为该行将被解释为:
open FILE, ("$filename" || die "Could not open $filename!\n");
这根本不是预期的。由于“or”的优先级较低,因此可以正常工作。在使用“||”的行中,open 的参数在括号之间传递,从而可以使用“||”。
唉,有些东西和猫差不多:
while(<>) {
print $_;
}
这将打印命令行中的所有文件,或通过 STDIN 传递的任何内容。
GREP
那么,我们的“grep”脚本将如何工作?我将假设“grep -E”,因为在 Perl 中这比简单的 grep 更容易。无论如何:
my $pattern = $ARGV[0];
shift @ARGV;
while(<>) {
print $_ if /$pattern/o;
}
传递给 $patttern 的 "o" 指示 Perl 只编译该模式一次,从而加快速度。不是“条件允许的东西”的风格。这意味着它只会在条件为真时执行“某事”。最后,单独的“/$pattern/”与“$_ =~ m/$pattern/”相同,这意味着将 $_ 与指示的正则表达式模式进行比较。如果你想要标准的grep 行为,即只是子串匹配,你可以这样写:
print $_ if $_ =~ "$pattern";
剪切
通常,您最好使用正则表达式组来获得确切的字符串而不是剪切。例如,你会用“sed”做什么。无论如何,这里有两种复制剪切的方法:
while(<>) {
my @array = split ",";
print $array[3], "\n";
}
这将为您提供每行的第四列,使用“,”作为分隔符。注意@array 和$array[3]。 @ sigil 意味着“数组”应该被视为一个数组。它将接收由当前处理行中的每一列组成的数组。接下来,$ 印记意味着array[3] 是一个标量值。它将返回您要求的 列。
不过,这不是一个好的实现,因为“split”会扫描整个字符串。我曾经通过不使用 split 将一个过程从 30 分钟缩短到 2 秒——不过,这些行相当大。无论如何,如果预期行很大,而您想要的列很低,则以下内容具有优越的性能:
while(<>) {
my ($column) = /^(?:[^,]*,){3}([^,]*),/;
print $column, "\n";
}
这利用正则表达式来获取所需的信息,而且仅此而已。
如果你想要位置列,你可以使用:
while(<>) {
print substr($_, 5, 10), "\n";
}
将从第六个字符开始打印 10 个字符(同样,0 表示第一个字符)。
头部
这个很简单:
my $printlines = abs(shift);
my $lines = 0;
my $current;
while(<>) {
if($ARGV ne $current) {
$lines = 0;
$current = $ARGV;
}
print "$_" if $lines < $printlines;
$lines++;
}
这里要注意的事项。我使用“ne”来比较字符串。现在,$ARGV 将始终指向当前正在读取的文件,因此我会跟踪它们以在读取新文件后重新开始计数。还要注意“if”的更传统语法以及后置语法。
我还使用简化的语法来获取要打印的行数。当您单独使用“shift”时,它将假定为“shift @ARGV”。另外请注意,除了修改@ARGV 之外,shift 还会返回移出它的元素。
与 shell 一样,数字和字符串之间没有区别——您只需使用它即可。即使像“2”+“2”这样的东西也可以。事实上,Perl 更加宽容,乐于将任何非数字都视为 0,因此您可能要小心。
不过,这个脚本效率很低,因为它读取所有文件,而不仅仅是所需的行。让我们对其进行改进,并在此过程中查看几个重要的关键字:
my $printlines = abs(shift);
my @files;
if(scalar(@ARGV) == 0) {
@files = ("-");
} else {
@files = @ARGV;
}
for my $file (@files) {
next unless -f $file && -r $file;
open FILE, "<", $file or next;
my $lines = 0;
while(<FILE>) {
last if $lines == $printlines;
print "$_";
$lines++;
}
close FILE;
}
关键字“next”和“last”非常有用。首先,"next" 将告诉 Perl 回到循环条件,如果适用则获取下一个元素。在这里,我们使用它来跳过文件,除非它确实是文件(不是目录)并且可读。如果我们无法打开文件,它也会跳过。
然后“last”用于立即跳出循环。一旦达到所需的行数,我们就使用它来停止读取文件。确实我们读了一行太多,但是在那个位置有“last”清楚地表明它后面的行不会被执行。
还有“重做”,它会回到循环的开头,但不会重新评估条件,也不会获取下一个元素。
尾巴
我会在这里做一个小技巧。
my $skiplines = abs(shift);
my @lines;
my $current = "";
while(<>) {
if($ARGV ne $current) {
print @lines;
undef @lines;
$current = $ARGV;
}
push @lines, $_;
shift @lines if $#lines == $skiplines;
}
print @lines;
好的,我将“push”(将一个值附加到数组)与“shift”(从数组的开头获取一些内容)结合起来。如果你想要一个堆栈,你可以使用 push/pop 或 shift/unshift。混合它们,你就有了一个队列。我用$#lines 将我的队列最多保留10 个元素,这将为我提供数组中最后一个元素的索引。您还可以使用scalar(@lines) 获取@lines 中的元素数量。
UNIQ
现在,uniq 只消除重复的连续行,根据您目前所见,这应该很容易。所以我会全部消除:
my $current = "";
my %lines;
while(<>) {
if($ARGV ne $current) {
undef %lines;
$current = $ARGV;
}
print $_ unless defined($lines{$_});
$lines{$_} = "";
}
现在我将整个文件保存在内存中,位于%lines 中。使用% 印记表明这是一个哈希表。我将这些行用作键,并且不存储任何值——因为我对这些值不感兴趣。我用“defined($lines{$_})”检查键存在的位置,它将测试与该键关联的值是否已定义;关键字 "unless" 的作用与 "if" 类似,但效果相反,所以它只在未定义行时打印一行。
还要注意语法$lines{$_} = "" 作为在哈希表中存储内容的一种方式。注意{} 用于哈希表,而不是[] 用于数组。
厕所
这实际上会用到很多我们见过的东西:
my $current;
my %lines;
my %words;
my %chars;
while(<>) {
$lines{"$ARGV"}++;
$chars{"$ARGV"} += length($_);
$words{"$ARGV"} += scalar(grep {$_ ne ""} split /\s/);
}
for my $file (keys %lines) {
print "$lines{$file} $words{$file} $chars{$file} $file\n";
}
三个新事物。两个是“+=”运算符,这应该是显而易见的,以及“for”表达式。基本上,“for”会将数组的每个元素分配给指定的变量。 “我的”是用来声明变量的,尽管如果之前声明它是不需要的。我可以在这些括号内有一个@array 变量。 "keys %lines" 表达式将返回作为哈希表 "%lines" 的键(文件名)的数组。其余的应该是显而易见的。
我实际上添加的只是修改答案的第三件事是“grep”。这里的格式是:
grep { code } array
它将为数组的每个元素运行“代码”,将元素作为“$_”传递。然后 grep 将返回代码计算结果为“true”的所有元素(不是 0,不是“”等)。这样可以避免计算由连续空格产生的空字符串。
与“grep”类似,还有“map”,这里我就不演示了。它不会过滤,而是返回一个由每个元素的“代码”结果形成的数组。
排序
最后,排序。这个也很简单:
my @lines;
my $current = "";
while(<>) {
if($ARGV ne $current) {
print sort @lines;
undef @lines;
$current = $ARGV;
}
push @lines, $_;
}
print sort @lines;
在这里,“排序”将对数组进行排序。注意 sort 可以接收一个函数来定义排序标准。例如,如果我想对数字进行排序,我可以这样做:
my @lines;
my $current = "";
while(<>) {
if($ARGV ne $current) {
print sort @lines;
undef @lines;
$current = $ARGV;
}
push @lines, $_;
}
print sort {$a <=> $b} @lines;
这里“$a”和“$b”接收要比较的元素。 "<=>" 返回 -1、0 或 1,具体取决于数字是小于、等于还是大于另一个。对于字符串,“cmp”做同样的事情。
处理文件、目录和其他东西
至于其余的,基本的数学表达式应该很容易理解。您可以通过这种方式测试有关文件的某些条件:
for my $file (@ARGV) {
print "$file is a file\n" if -f "$file";
print "$file is a directory\n" if -d "$file";
print "I can read $file\n" if -r "$file";
print "I can write to $file\n" if -w "$file";
}
我不想在这里详尽,还有很多其他这样的测试。我也可以做“glob”模式,比如shell的“*”和“?”,像这样:
for my $file (glob("*")) {
print $file;
print "*" if -x "$file" && ! -d "$file";
print "/" if -d "$file";
print "\t";
}
如果你把它和“chdir”结合起来,你也可以模拟“find”:
sub list_dir($$) {
my ($dir, $prefix) = @_;
my $newprefix = $prefix;
if ($prefix eq "") {
$newprefix = $dir;
} else {
$newprefix .= "/$dir";
}
chdir $dir;
for my $file (glob("*")) {
print "$prefix/" if $prefix ne "";
print "$dir/$file\n";
list_dir($file, $newprefix) if -d "$file";
}
chdir "..";
}
list_dir(".", "");
在这里,我们终于看到了一个函数。使用以下语法声明函数:
sub name (params) { code }
严格来说,“(params)”是可选的。我使用的声明参数“($$)”意味着我收到了两个标量参数。我也可以在其中包含“@”或“%”。数组“@_”传递了所有参数。 “my ($dir, $prefix) = @_”这一行只是将该数组的前两个元素分配给变量$dir 和$prefix 的一种简单方法。
这个函数不返回任何东西(它实际上是一个过程),但是你可以让函数返回值,只需向它添加“return something;”,并让它返回“某物”。
其余的应该很明显了。
混合一切
现在我将展示一个更复杂的示例。我会展示一些糟糕的代码来解释它有什么问题,然后再展示更好的代码。
对于第一个示例,我有两个文件,names.txt 文件,其中包含姓名和电话号码,systems.txt,其中包含系统和负责它们的名称。他们在这里:
names.txt
John Doe, (555) 1234-4321
Jane Doe, (555) 5555-5555
The Boss, (666) 5555-5555
systems.txt
Sales, Jane Doe
Inventory, John Doe
Payment, That Guy
然后,如果该人负责该系统,我想打印第一个文件,并将系统附加到该人的姓名后。第一个版本可能如下所示:
#!/usr/bin/perl
use strict;
use warnings;
open FILE, "names.txt";
while(<FILE>) {
my ($name) = /^([^,]*),/;
my $system = get_system($name);
print $_ . ", $system\n";
}
close FILE;
sub get_system($) {
my ($name) = @_;
my $system = "";
open FILE, "systems.txt";
while(<FILE>) {
next unless /$name/o;
($system) = /([^,]*)/;
}
close FILE;
return $system;
}
不过,此代码不起作用。 Perl 会抱怨该函数使用得太早而无法检查原型,但这只是一个警告。它将在第 8 行(第一个 while 循环)上给出错误,抱怨关闭文件句柄上的 readline。这里发生的是“FILE”是全局的,所以函数get_system正在改变它。让我们重写它,修复两个问题:
#!/usr/bin/perl
use strict;
use warnings;
sub get_system($) {
my ($name) = @_;
my $system = "";
open my $filehandle, "systems.txt";
while(<$filehandle>) {
next unless /$name/o;
($system) = /([^,]*)/;
}
close $filehandle;
return $system;
}
open FILE, "names.txt";
while(<FILE>) {
my ($name) = /^([^,]*),/;
my $system = get_system($name);
print $_ . ", $system\n";
}
close FILE;
这不会给出任何错误或警告,也不会起作用。它只返回系统,但不返回姓名和电话号码!发生了什么?好吧,发生的事情是我们在调用get_system 之后引用了“$_”,但是,通过读取文件,get_system 覆盖了$_ 的值!
为避免这种情况,我们将在 get_system 中设置 $_ 本地化。这将给它一个本地范围,然后从get_system返回原始值:
#!/usr/bin/perl
use strict;
use warnings;
sub get_system($) {
my ($name) = @_;
my $system = "";
local $_;
open my $filehandle, "systems.txt";
while(<$filehandle>) {
next unless /$name/o;
($system) = /([^,]*)/;
}
close $filehandle;
return $system;
}
open FILE, "names.txt";
while(<FILE>) {
my ($name) = /^([^,]*),/;
my $system = get_system($name);
print $_ . ", $system\n";
}
close FILE;
这仍然行不通!它在名称和系统之间打印一个换行符。好吧,Perl 读取的行包括它可能有的任何换行符。有一个简洁的命令可以从字符串中删除换行符“chomp”,我们将使用它来解决这个问题。由于并非每个名称都有系统,因此我们也可以避免在这种情况下打印逗号:
#!/usr/bin/perl
use strict;
use warnings;
sub get_system($) {
my ($name) = @_;
my $system = "";
local $_;
open my $filehandle, "systems.txt";
while(<$filehandle>) {
next unless /$name/o;
($system) = /([^,]*)/;
}
close $filehandle;
return $system;
}
open FILE, "names.txt";
while(<FILE>) {
my ($name) = /^([^,]*),/;
my $system = get_system($name);
chomp;
print $_;
print ", $system" if $system ne "";
print "\n";
}
close FILE;
这行得通,但它也恰好效率低下。我们为名称文件中的每一行读取整个系统文件。为避免这种情况,我们将从系统中读取所有数据一次,然后使用它来处理名称。
现在,有时文件太大了,您无法将其读入内存。发生这种情况时,您应该尝试将处理它所需的任何 other 文件读入内存,以便您可以一次完成每个文件的所有操作。无论如何,这是它的第一个优化版本:
#!/usr/bin/perl
use strict;
use warnings;
our %systems;
open SYSTEMS, "systems.txt";
while(<SYSTEMS>) {
my ($system, $name) = /([^,]*),(.*)/;
$systems{$name} = $system;
}
close SYSTEMS;
open NAMES, "names.txt";
while(<NAMES>) {
my ($name) = /^([^,]*),/;
chomp;
print $_;
print ", $systems{$name}" if defined $systems{$name};
print "\n";
}
close NAMES;
不幸的是,它不起作用。从来没有出现过系统!发生了什么?好吧,让我们看看“%systems”包含什么,使用Data::Dumper:
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
our %systems;
open SYSTEMS, "systems.txt";
while(<SYSTEMS>) {
my ($system, $name) = /([^,]*),(.*)/;
$systems{$name} = $system;
}
close SYSTEMS;
print Dumper(%systems);
open NAMES, "names.txt";
while(<NAMES>) {
my ($name) = /^([^,]*),/;
chomp;
print $_;
print ", $systems{$name}" if defined $systems{$name};
print "\n";
}
close NAMES;
输出将是这样的:
$VAR1 = ' Jane Doe';
$VAR2 = 'Sales';
$VAR3 = ' That Guy';
$VAR4 = 'Payment';
$VAR5 = ' John Doe';
$VAR6 = 'Inventory';
John Doe, (555) 1234-4321
Jane Doe, (555) 5555-5555
The Boss, (666) 5555-5555
$VAR1/$VAR2/etc 是 Dumper 显示哈希表的方式。奇数是键,后面的偶数是值。现在我们可以看到%systems 中的每个名字都有一个前面的空格!愚蠢的正则表达式错误,让我们修复它:
#!/usr/bin/perl
use strict;
use warnings;
our %systems;
open SYSTEMS, "systems.txt";
while(<SYSTEMS>) {
my ($system, $name) = /^\s*([^,]*?)\s*,\s*(.*?)\s*$/;
$systems{$name} = $system;
}
close SYSTEMS;
open NAMES, "names.txt";
while(<NAMES>) {
my ($name) = /^\s*([^,]*?)\s*,/;
chomp;
print $_;
print ", $systems{$name}" if defined $systems{$name};
print "\n";
}
close NAMES;
因此,我们在这里积极地删除名称和系统开头或结尾的所有空格。还有其他方法可以形成该正则表达式,但这不是重点。这个脚本还有一个问题,如果你的“names.txt”和/或“systems.txt”文件最后有一个空行,你就会看到这个问题。警告如下所示:
Use of uninitialized value in hash element at ./exemplo3e.pl line 10, <SYSTEMS> line 4.
Use of uninitialized value in hash element at ./exemplo3e.pl line 10, <SYSTEMS> line 4.
John Doe, (555) 1234-4321, Inventory
Jane Doe, (555) 5555-5555, Sales
The Boss, (666) 5555-5555
Use of uninitialized value in hash element at ./exemplo3e.pl line 19, <NAMES> line 4.
这里发生的情况是,在处理空行时,“$name”变量中没有任何内容。有很多方法可以解决这个问题,但我选择以下方法:
#!/usr/bin/perl
use strict;
use warnings;
our %systems;
open SYSTEMS, "systems.txt" or die "Could not open systems.txt!";
while(<SYSTEMS>) {
my ($system, $name) = /^\s*([^,]+?)\s*,\s*(.+?)\s*$/;
$systems{$name} = $system if defined $name;
}
close SYSTEMS;
open NAMES, "names.txt" or die "Could not open names.txt!";
while(<NAMES>) {
my ($name) = /^\s*([^,]+?)\s*,/;
chomp;
print $_;
print ", $systems{$name}" if defined($name) && defined($systems{$name});
print "\n";
}
close NAMES;
现在正则表达式的名称和系统至少需要一个字符,我们在使用前测试是否定义了“$name”。
结论
那么,这些是翻译 shell 脚本的基本工具。你可以用 Perl 做更多更多,但这不是你的问题,它也不适合这里。
正如一些重要主题的基本概述,
可能会被黑客攻击的 Perl 脚本需要使用 -T 选项运行,这样 Perl 就会抱怨任何未正确处理的易受攻击的输入。
有一些库,称为模块,用于数据库访问、XML&cia 处理、Telnet、HTTP 和其他协议。事实上,有很多模块可以在CPAN 找到。
正如其他人所说,如果您使用 AWK 或 SED,您可以使用 A2P 和 S2P 将它们翻译成 Perl。
Perl 可以以面向对象的方式编写。
Perl 有多个版本。在撰写本文时,稳定版是 5.8.8,并且有 5.10.0 可用。还有一个 Perl 6 正在开发中,但经验告诉大家不要急于等待它。
有一本关于 Perl 的免费、优秀、实用、硬而快速的书,名为 Learning Perl The Hard Way。它的风格与这个答案非常相似。从这里出发可能是个好地方。
我希望这会有所帮助。
免责声明
我不是要教 Perl,您将至少需要一些参考资料。有一些关于良好 Perl 习惯的指南,例如在脚本开头使用“use strict;”和“use warnings;”,以减少对糟糕代码的宽容,或者在打印行使用 STDOUT 和 STDERR,以指出正确的输出管道。
这是我同意的,但我认为它会偏离显示常见 shell 脚本实用程序模式的基本目标。