如何将一个Shell脚本翻译成Perl?

17

我有一个很长的shell脚本。现在我的老板要求我必须用Perl重写它。 是否有办法编写Perl脚本并在其中使用现有的shell代码,类似于Inline::C? 是否有类似于Inline::Shell的东西?我看过inline模块,但它只支持语言。


7
使用假想的 inline::bash,岂不是有些忽略了重写的意义吗?我能想到这样重写的唯一理由就是为了在组织中标准化语言。在 bash 脚本周围包上一个微小的 Perl 包装器根本无法实现这一点。 - Quentin
1
我的意思是重用现有的shell代码,并在我的新迁移的perl脚本中添加一些新的perl代码。 - not-exactly-a-unixhater
4
我赞同David的观点:不要与Perl对抗,而是使用它!开始转换你的脚本,这可能并不那么难...然后将结果与你的bash脚本进行比较。 - lexu
3
问题的核心在于:我的老板让我用Perl重写一个Bash脚本,我如何在不使用Perl的情况下完成这个任务?这并不是一个好问题。 - innaM
2
他并不是试图避免使用Perl。他只是想重复利用已经存在的工作来开始工作。 - brian d foy
显示剩余7条评论
6个回答

83
我会认真回答。我不知道任何将shell脚本转换为Perl的程序,并且我怀疑任何解释器模块都不会提供性能优势。因此,我将概述我如何进行操作。
现在,你希望尽可能多地重用代码。在这种情况下,我建议选择一些代码片段,编写一个对应的Perl版本,然后从主脚本调用Perl脚本。这将使您可以分步进行转换,确保已转换部分正常工作,并逐渐提高您的Perl知识水平。
由于您可以从Perl脚本中调用外部程序,因此您甚至可以使用Perl替换一些较大的逻辑,并从Perl调用较小的shell脚本(或其他命令)来执行您尚未感到舒适的某些操作。因此,您将拥有一个调用perl脚本的shell脚本,再调用另一个shell脚本的perl脚本。实际上,我就是用这种方式处理了自己的第一个Perl脚本。
当然,选择要转换的内容非常重要。下面我将解释,在shell脚本中常见的许多模式在Perl中如何编写,以便您可以在脚本中识别它们,并通过尽可能多的复制粘贴创建替代品。
首先,Perl脚本和Shell脚本都是代码+函数。也就是说,除了函数声明之外的任何内容都将按照遇到的顺序执行。你不需要在使用之前声明函数。这意味着可以保留脚本的一般布局,尽管保持事物在内存中(例如整个文件或其处理形式)使得简化任务成为可能。
在Unix上,Perl脚本以类似以下的方式开始:
#!/usr/bin/perl

use strict;
use warnings;

use Data::Dumper;
#other libraries

(rest of the code)

显然,第一行指向要用于运行脚本的命令,就像普通shell一样。接下来的两个"use" 行使语言更加严格,这应该会减少因为您不熟悉语言而遇到的错误数量(或简单地做错了什么)。第三个"use" 行导入了 "Data" 模块的 "Dumper" 函数。它对于调试非常有用。如果您想知道数组或哈希表的值,只需打印Dumper(whatever)。
请注意,注释与shell的相同,以“#”开头。
现在,您可以调用外部程序并将其连接管道。例如:
open THIS, "cat $ARGV[0] |";

那将运行cat,通过"$ARGV[0]"传递参数,这将是shell中的$1——传递给它的第一个参数。其结果将通过"THIS"管道传输到您的Perl脚本中,您可以使用它来从中读取,稍后我会展示如何操作。
您可以在行首或行尾使用"|",表示"管道到"或"管道自"模式,并指定要运行的命令,还可以在开头使用">"或">>",以带有或不带有截断的方式打开文件进行写入,使用"<"明确表示为读取文件(默认),或者使用"+<"和"+>"进行读写。请注意,后者将首先截断文件。
另一种用于"open"的语法,可以避免文件名中包含这些字符的问题,即将打开模式作为第二个参数:
open THIS, "-|", "cat $ARGV[0]";

这样做会产生同样的效果。模式“-|”代表“管道自”,而“|-”代表“管道到”。其余的模式可以像以前一样使用(>,>>,<,+>,+<)。虽然还有更多关于打开文件的内容,但对于大多数事情来说,这应该足够了。
但是,您应该尽可能避免调用外部程序。例如,您可以通过直接打开文件 open THIS, "$ARGV[0]"; 来实现,从而获得更好的性能。
那么,您可以省略哪些外部程序呢?好吧,几乎所有的都可以。但让我们先从基础知识开始:cat、grep、cut、head、tail、uniq、wc、sort。 CAT 嗯,对于这个命令没有太多可说的。只需记住,如果可能,请仅读取文件一次并将其保存在内存中。当然,如果文件很大,您就无法做到这一点,但几乎总有办法避免多次读取文件。
无论如何,cat 的基本语法如下:
my $filename = "whatever";
open FILE, "$filename" or die "Could not open $filename!\n";
while(<FILE>) {
  print $_;
}
close FILE;

这将打开一个文件,并打印出它的所有内容("while(<FILE>)" 会循环直到文件结尾,将每一行赋值给 "$_"),然后再关闭文件。

如果我想将输出指向另一个文件,可以这样做:

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"指示的文件中。您还可以在适当的位置使用STDINSTDOUTSTDERR,而无需先打开它们。实际上,"print"默认为STDOUT,"die"默认为"STDERR"。
请注意"or die ..."和"|| die ..."。运算符or||表示仅在第一个命令返回false(即空字符串、null引用、0等)时才执行以下命令。die命令会停止带有错误消息的脚本。
"or"和"||"的主要区别是优先级。如果在上面的示例中将"or"替换为"||",则不会按预期工作,因为该行将被解释为:
open FILE, ("$filename" || die "Could not open $filename!\n");

这与预期的完全不同。由于"or"的优先级较低,因此它起作用了。在使用"||"的那一行中,将open的参数放在括号中,使得可以使用"||"。

可惜,有一些与cat非常相似的东西:

while(<>) {
  print $_;
}

这将打印出命令行中的所有文件或通过STDIN传递的任何内容。

GREP

那么,我们的“grep”脚本如何工作呢?我会假设使用“grep -E”,因为在Perl中比简单的grep更容易。无论如何:

my $pattern = $ARGV[0];
shift @ARGV;
while(<>) {
        print $_ if /$pattern/o;
}

在 $pattern 中传递的 "o" 指示 Perl 仅编译该模式一次,从而提高速度。不要使用 "something if cond" 风格。它意味着只有当条件为真时才会执行 "something"。最后,"/$pattern/" 单独使用与 "$_ =~ m/$pattern/" 相同,这意味着将 $_ 与指定的正则表达式模式进行比较。如果您想要标准的 grep 行为,即仅进行子字符串匹配,可以编写以下内容:

print $_ if $_ =~ "$pattern";

剪切

通常,使用正则表达式组来获取精确字符串比使用"cut"命令更好。例如,你可以使用"sed"命令。无论如何,下面有两种重现"cut"命令的方法:

while(<>) {
  my @array = split ",";
  print $array[3], "\n";
}

那将会获取每一行的第四列,使用“,”作为分隔符。请注意@array$array[3]@标记意味着“array”应该被视为数组。它将接收由当前处理行中的每一列组成的数组。接下来,$标记意味着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”清楚地显示其后面的行将不会被执行。
还有“redo”,它将返回循环的开始,但不重新评估条件,也不获取下一个元素。 结尾 我要在这里做个小把戏。
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{$_} = "" 的语法用于在哈希表中存储某些内容。请注意使用{}表示哈希表,而不是使用[]表示数组。

WC

实际上,这将使用我们已经学过的很多东西:

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"会将数组的每个元素分配给指定的变量。 "my"用于声明变量,但如果之前已经声明,则不需要使用它。我可以在那些括号中使用@array变量。"keys %lines"表达式将作为数组返回哈希表"%lines"中存在的键(文件名)。其余部分应该很明显。

第三件事,实际上是在修改答案时添加的"grep"。格式如下:

grep { code } array

它将针对数组的每个元素运行"code",将该元素作为"$_"传递。然后grep将返回所有评估为"true"(不是0,不是""等)的元素。这避免了由连续空格导致的空字符串计数。
类似于"grep",还有"map",我在这里不会演示。它不是过滤,而是返回由每个元素的"code"结果形成的数组。
最后是排序。这很容易:
my @lines;
my $current = "";
while(<>) {
  if($ARGV ne $current) {
    print sort @lines;
    undef @lines;
    $current = $ARGV;
  }
  push @lines, $_;
}
print sort @lines;

在这里,“sort”将对数组进行排序。请注意,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;"使它返回"value",从而使它返回值。

其余的应该很明显了。

混合运用

现在我将展示一个更复杂的例子。我将展示一些糟糕的代码来解释其中的问题,然后展示更好的代码。

对于这个第一个例子,我有两个文件,names.txt文件,里面有名称和电话号码,system.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循环)报错,抱怨文件句柄已关闭的读取操作。这里发生的情况是"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;

这个代码可以工作,但效率极低。我们为了读取名字文件中的每一行,都要读取整个系统文件。为了避免这种情况,我们将先读取系统文件中所有的数据,然后再使用这些数据处理名字文件。

有时候一个文件非常大,无法将其全部读入内存。如果遇到这种情况,你应该尝试将任何需要处理的其他文件读入内存,以便每个文件只需进行一次单独的通行。以下是第一个优化版本的代码:

#!/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;

很遗憾,它不起作用。 没有系统出现! 发生了什么? 好吧,让我们通过使用 Data::Dumper 来查看 "%systems" 包含的内容:

#!/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/etcDumper显示哈希表的方式。奇数是键,相邻的偶数是值。现在我们可以看到%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,可以使用A2PS2P将它们转换为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脚本实用程序模式的基本目标。

10
我必须点赞,仅仅因为它展示了如此全面和努力的内容! - Brian Agnew
10
这是对一个不那么好的问题的绝妙回答。 - Mark Biek
3
尽可能使用 open() 函数的三个参数形式。http://perldoc.perl.org/functions/open.html - Brad Gilbert
您可能还想为命令/子程序提供一些链接。 - Brad Gilbert
1
@Daniel:出于同样的原因(健壮性),使用“open my $file,…”而不是“open FILE,…”以及在使用之前将$_本地化是一个好主意。然后,您的各种实用程序(cat、grep等)可以成为不会践踏全局状态的子例程。 - j_random_hacker
显示剩余3条评论

5

我很惊讶还没有人提到在核心Perl中包含的Shell模块,它可以让你使用函数调用语法来执行外部命令。例如(改编自概要):

use Shell qw(cat ps cp);
$passwd = cat '</etc/passwd';
@pslines = ps '-ww';
cp "/etc/passwd", "/tmp/passwd";

只要使用括号,你甚至可以在use行中没有提到的$PATH中调用其他程序,例如:

gcc('-o', 'foo', 'foo.c');

请注意,Shell会将子进程的标准输出收集并作为字符串或数组返回。这简化了脚本编写,但不是最有效的方法,并且可能会在您依赖于命令的未缓冲输出时出现问题。
模块文档提到了一些缺点,例如无法使用相同的语法调用shell内部命令(例如cd)。实际上,他们建议不要在生产系统中使用该模块!但它肯定可以成为一个有用的支撑,直到您将代码移植到“正确”的Perl上。

5
我不知道你的shell脚本里有什么内容,但是别忘了有像a2p(awk转perl)和s2p(sed转perl)这样的工具,或许还有其他的。值得四处看看。
由于Perl的强大功能,你可能会发现它并不需要很大的工作量。你可能一直在用各种bash特性和实用程序来做某些事情,而这些事情在Perl中可以自然地完成。
像任何迁移项目一样,最好先准备一些已经测试过的回归测试,以便在两个解决方案上运行,如果你没有这些,我建议首先生成这些测试。

1
不行。但我无法相信它会那么难。 - Brian Agnew

4
内联shell被称为system。如果您有自定义函数要公开给Perl使用,那么就没有办法了。但是,您可以在与运行Perl程序相同的环境中运行短小的shell代码段。您还可以逐步用Perl替换shell脚本的部分内容。开始编写一个模块来复制shell脚本功能,并将Perly位插入shell脚本中,直到最终大部分都是Perl。
没有shell到Perl的翻译器。曾经有一个长期运行的笑话,说你可以将你的csh脚本发送电子邮件给一个csh-to-Perl翻译器,但这实际上只是Tom Christainsen在早期90年代向您展示Perl的酷之处。Randal Schwartz上传了一个sh-to-Perl的翻译器,但您必须检查上传日期:那是愚人节。他的脚本只是简单地将所有内容包装在system中。
无论您做什么,请不要丢失原始的shell脚本。 :)

0

我认为学习Perl并尝试使用Perl而不是shell是为了更大的利益。我曾经借助于Notepad++的“替换”功能进行过一次转换。

然而,当我试图创建一个可以执行shell脚本的Perl包装器时,我遇到了与最初提出的问题类似的问题。

针对我的情况,我编写了下面的代码,它能够正常工作。

希望这能有所帮助。

#!perl
use strict;
use Data::Dumper;
use Cwd;

#Variables read from shell
our %VAR;

open SH, "<$ARGV[0]" or die "Error while trying to read $ARGV[0] ($!)\n";
my @SH=<SH>;
close SH;

sh2perl(@SH);


#Subroutine to execute shell from Perl (read from array)
sub sh2perl {
    #Variables
    my %case; #To store data from conditional block of "case"
    my %if; #To store data from conditional block of "if"

    foreach my $line (@_) {
        #Remove blanks at the beginning and EOL character
        $line=~s/^\s*//;
        chomp $line;

        #Comments and blank lines
        if ($line=~/^(#.*|\s*)$/) {
            #Do nothing
        }

        #Conditional block - Case
        elsif ($line=~/case.*in/..$line=~/esac/) {
            if ($line=~/case\s*(.*?)\s*\in/) {
                $case{'var'}=transform($1);
            } elsif ($line=~/esac/) {
                delete $case{'curr_pattern'};
                #Run conditional block
                my $case;
                map { $case=$_ if $case{'var'}=~/$_/ } @{$case{'list_patterns'}};
                $case ? sh2perl(@{$case{'patterns'}->{$case}}) : sh2perl(@{$case{'patterns'}->{"*"}});
            } elsif ($line=~/^\s*(.*?)\s*\)/) {
                $case{'curr_pattern'}=$1;
                push(@{$case{'list_patterns'}}, $case{'curr_pattern'}) unless ($line=~m%\*\)%)
            } else {
                push(@{$case{'patterns'}->{ $case{'curr_pattern'} }}, $line);
            }
        }

        #Conditional block - if
        elsif ($line=~/^if/..$line=~/^fi/) {
            if ($line=~/if\s*\[\s*(.*\S)\s*\];/) {
                $if{'condition'}=transform($1);
                $if{'curr_cond'}="TRUE";
            } elsif ($line=~/fi/) {
                delete $if{'curr_cond'};
                #Run conditional block
                $if{'condition'} ? sh2perl(@{$if{'TRUE'}}) : sh2perl(@{$if{'FALSE'}});
            } elsif ($line=~/^else/) {
                $if{'curr_cond'}="FALSE";
            } else {
                push(@{$if{ $if{'curr_cond'} }}, $line);
            }
        }

        #echo
        elsif($line=~/^echo\s+"?(.*?[^"])"?\s*$/) {
            my $str=$1;
            #echo with redirection
            if ($str=~m%[>\|]%) { 
                eval { system(transform($line)) };
                if ($@) { warn "Error while evaluating $line: $@\n"; }
            #print new line
            } elsif ($line=~/^echo ""$/) {
                print "\n";
            #default
            } else {
                print transform($str),"\n";
            }
        }

        #cd
        elsif($line=~/^\s*cd\s+(.*)/) {
            chdir $1;
        }

        #export
        elsif($line=~/^export\s+((\w+).*)/) {
            my ($var,$exported)=($2,$1);
            if ($exported=~/^(\w+)\s*=\s*(.*)/) {
                while($exported=~/(\w+)\s*=\s*"?(.*?\S)"?\s*(;(?:\s*export\s+)?|$)/g) { $VAR{$1}=transform($2); }
            }
            # export($var,$VAR{$var});
            $ENV{$var}=$VAR{$var};
            print "Exported variable $var = $VAR{$var}\n";
        }


        #Variable assignment
        elsif ($line=~/^(\w+)\s*=\s*(.*)$/) {
            $1 eq "" or $VAR{$1}=""; #Empty variable
            while($line=~/(\w+)\s*=\s*"?(.*?\S)"?\s*(;|$)/g) {
                $VAR{$1}=transform($2);
            }
        }

        #Source
        elsif ($line=~/^source\s*(.*\.sh)/) {
            open SOURCE, "<$1" or die "Error while trying to open $1 ($!)\n";
            my @SOURCE=<SOURCE>;
            close SOURCE;
            sh2perl(@SOURCE);
        }


        #Default (assuming running command)
        else {
            eval { map { system(transform($_)) } split(";",$line); };
            if ($@) { warn "Error while doing system on \"$line\": $@\n"; }
        }

    }
}


sub transform {
    my $src=$_[0];

    #Variables $1 and similar
    $src=~s/\$(\d+)/$ARGV[$1-1]/ge;

    #Commands stored in variables "$(<cmd>)"
    eval {
        while ($src=~m%\$\((.*)\)%g) {
            my ($cmd,$new_cmd)=($1,$1);
            my $curr_dir=getcwd;
            $new_cmd=~s/pwd/echo $curr_dir/g;
            $src=~s%\$\($cmd\)%`$new_cmd`%e;
            chomp $src;
        }
    };
    if ($@) { warn "Wrong assessment for variable $_[0]:\n=> $@\n"; return "ERROR"; }

    #Other variables
    $src=~s/\$(\w+)/$VAR{$1}/g;

    #Backsticks
    $src=~s/`(.*)`/`$1`/e;

    #Conditions
    $src=~s/"(.*?)"\s*==\s*"(.*?)"/"$1" eq "$2" ? 1 : 0/e;
    $src=~s/"(.*?)"\s*!=\s*"(.*?)"/"$1" ne "$2" ? 1 : 0/e;
    $src=~s/(\S+)\s*==\s*(\S+)/$1 == $2 ? 1 : 0/e;
    $src=~s/(\S+)\s*!=\s*(\S+)/$1 != $2 ? 1 : 0/e;

    #Return Result
    return $src;
}

但是从快速浏览中我发现有相当多的问题:它将无法正确解析嵌套块命令,例如 if ... fi,以及包含多个反引号对的命令(从第一个 ``` 到最后一个都将被传递给 shell)。我怀疑在引用方面存在无数差异,尽管我还没有检查过。 - j_random_hacker

-15
你可以使用以下方式开始编写你的“Perl”脚本:
#!/bin/bash

假设bash已安装在该位置,perl将自动调用bash解释器来运行它。

编辑:或者操作系统会拦截调用并阻止其到达Perl。我很难找到有关实际工作原理的文档。欢迎对文档进行评论。


6
那不叫做Perl脚本。She-bang处理不是由Perl解释器处理,而是由内核处理。 - EFraim
1
which 命令读取脚本的第一行以确定它是哪种类型的脚本,因此使用 she bang 脚本 bin 位置。 - not-exactly-a-unixhater
1
真的吗?即使您使用以下命令调用脚本:perl myscript.sh? - Quentin
2
@David:不是的。如果你执行perl myscript.sh,那么接下来的内容将会被传递给在shebang行中指定的解释器。 - Quentin
6
Perl 会读取 shebang 并执行其中的内容。你可以尝试一下。 - jrockway
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接