Perl II

ArticleCategory:

Software Development

AuthorImage:

[作者]

TranslationInfo:

original in enGuido Socher

en to gb Xiang Hong

AboutTheAuthor:

Guido是一个老资格的Linux迷和Perl爱好者。 最近他正忙着修房子和在花园里种菜什么的。

Abstract:

前一篇文章Perl I给出了对Perl语言的一个大体的描述。 在这篇文章里我们将学习如何编制我们的第一个有用的Perl程序。

ArticleIllustration:

[Illustration]

ArticleBody:[The article body]

一个程序框架

Perl特别适合用来编制小型的专用程序。 为了加快开发进度,构造一个能提供大多数程序都要用到的功能和结构的基本框架, 绝对是一个好主意。 下面的一段代码提供了基本的命令行选项分析功能以及一个 打印帮助信息的子例程。

!/usr/bin/perl -w
# vim: set sw=8 ts=8 si et:
#
# uncomment strict to make the perl compiler very
# strict about declarations:
#use strict;
# global variables:
use vars qw($opt_h);
use Getopt::Std;
#
&getopts("h")||die "ERROR: No such option. -h for help\n";
&help if ($opt_h);
#
#>>your code<<
#
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
sub help{
print "help message\n";
exit;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__END__

让我们来看看这段代码。"&getopts()"是对库"Getopt::Std"中一个子例程的调用,进行命令行选项分析, 并根据选项内容为相应的全局变量$opt_<选项>赋值。所有的命令行选项都以"-"(减号)开头,并必须位于程序名 和其他参数之间(这是Unix系统的一个约定)。 传给"&getopts"一个特定的字符串(在上面的程序中是"h")将列出所有可接受的选项。 若某选项要求一个参数,则在该选项之后应该有一个冒号。比如说,"&getsopt("d:x:h")"表示本程序接受选项 "-d","-x","-h",其中"-d"和"-x"需要一个参数。 从而"-d something"是一个可接受的命令行,而"-d -x foo"是错误的,因为选项"-d"没有参数。
如果命令行给出了"-h"参数,则变量"$opt_h"被赋值,"&help if ($opt_h);" 将调用子例程help。 语句"sub help{"是对这个子例程的声明。
如果你现在还没有完全了解所有的细节,不要紧,你可以把这段代码当成一个模板,逐步往上添加你希望有的功能。

使用这个模板

我们来利用这个模板写一个小小的16进制/10进制数字转换程序,不妨叫做"numconv";
"numconv -x 30 "将输出10进制数30的16进制形式;
"numconv -d 1A "将输出16进制数1A的10进制形式;
"numconv -h "输出帮助信息。
可以用Perl函数"hex()"完成16进制到10进制的转换,并用"printf()"完成10进制到16进制的转换。 把它们加入我们的模板中,得到下面这个漂亮的程序:

#!/usr/bin/perl -w
# vim: set sw=8 ts=8 si et:
#
# uncomment strict to make the perl compiler very
# strict about declarations:
#use strict;
# global variables:
use vars qw($opt_d $opt_x $opt_h);
use Getopt::Std;
#
&getopts("d:x:h")||die "ERROR: No such option. -h for help\n";
&help if ($opt_h);
if ($opt_d && $opt_x){
    die "ERROR: options -x and -d are mutual exclusive.\n";
}
if ($opt_d){
    printf("decimal: %d\n",hex($opt_d));
}elsif ($opt_x){
    printf("hex: %X\n",$opt_x);
}else{
    # wrong usage -d or -x must be given:
    &help;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
sub help{
    print "convert a number to hex or dec.
USAGE: numconv [-h] -d hexnum
    umconv [-h] -x decnum

OPTIONS: -h this help
EXAMPLE: numconv -d 1af
\n";
    exit;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__END__

单击此处下载"numconv"程序。
下面我们来仔细的分析这个程序。

If-语句

Perl中的if语句有两种形式:
表达式 if (条件);

if (条件) 块1 [[elsif (条件) 块2 ...] else 块3]

其中"块"由花括号{}包围的若干条语句组成。 例如:

printf("hello\n") if ($i);

if ($i == 2){
   printf("i is 2\n");
}elsif ($i == 4){
   printf("i is 4\n");
}else{
   printf("i is neither 2 nor 4\n");
}

就象在C中一样,还可以利用"&&"和"||"操作符的"短路"特性:
printf("hello\n") if ($i);
可以写成:
($i) && printf("hello\n");
我们的模板中对"||"的使用可以被很好的翻译成符合口语习惯的句子:
&getopts("d:x:h")||die "ERROR\n";
"(或者成功的)获得命令行选项,或者退出"。
函数"die"相当于"printf"加"exit"。它将在输出一条信息后终止程序。
&getopts("d:x:h")||die "ERROR\n";
相当于:
die "ERROR\n"; if (! &getopts("d:x:h"));
其中"!"是逻辑非运算符。这也可被重写为:
die "ERROR\n"; unless (&getopts("d:x:h"));
"unless"相当于"if(!..)".

你可以从if语句的这许多种写法中选一种最符合你习惯的。

变量

Perl I中, 我们在使用标量变量(以$开头的变量)之前并没有声明。 它们是在被使用的时候被创建的。 对于小程序来说,这或许让人觉得很方便;但对于大的程序,这个特性很容易导致某些难于发现的错误。 而变量声明可以使被编译器能够检查出某些类型错误。
"use strict;" 强制所有变量都应被声明。
看看下面这个程序:

#!/usr/bin/perl
use strict;
my $i=1;
print "i is $i\n";

这个程序是正确的,并将产生输出:"i is 1"。现在假设我们在敲键盘时错误的把 "i"敲成了"j":

#!/usr/bin/perl
#
$i=1;
print "i is $j\n";

这个程序也能运行得很好,但输出会是:"i is "。 加入语句"use strict;"将使后者无法通过编译。 一旦使用了"strict",所有变量都需要被声明,否则将出现编译错误信息。

#!/usr/bin/perl
use strict;
my $i=1;
print "i is $j\n";

上面的程序将导致如下错误信息,同时定位这个错误变得十分容易:

Global symbol "$j" requires explicit package name at ./vardec line 4.
Execution of ./vardec aborted due to compilation errors.
Exit 255

可以用"my"声明一个(局部)变量;也可以象在我们的模板中那样, 用"use vars qw()"来声明,如:
use vars qw($opt_h);

用"use var"声明的全局变量的有效范围延伸到被包含的库中。
在程序的开头(任何子例程之外)用"my"声明的变量只在当前程序 文件(包括该文件中的所有子例程)中有效。
子例程的局部变量在该子例程内用"my"来声明。

习惯于shell编程的人可能会在声明变量或给变量赋值时丢掉"$"符号。 这是一种错误的做法。无论何时,只要你在使用一个标量变量,"$"符号 都是必须的。

在声明变量时也可以直接赋值。如"my $myvar=10;"声明了变量"$myvar" 同时为其赋初值10。

子例程

在上面的"numconv"程序中我们已经使用了"help"子例程。 子例程的使用可以使程序结构更清晰。
可以在程序的任何地方插入子例程(在调用之前或之后都可以)。 子例程以"sub name(){..."开始,其调用形式是"$retval=&name(...arguments...)"。 子例程的返回值是其最后一条被执行的语句的结果。参数传递通过一个特殊的数组"@_"完成。 我们将在Perl III中详细讨论数组,现在只要知道在子例程内可以用"shift" 从中读出标量变量的值就够了。这里是一个例子

#!/usr/bin/perl
use strict;
my $result;
my $b;
my $a;
$result=&add_and_duplicate(2,3);
print "2*(2+3) is $result\n";

$b=5;$a=10;
$result=&add_and_duplicate($a,$b);
print "2*($a+$b) is $result\n";

# add two numbers and multiply with 2:
sub add_and_duplicate(){
    my $locala=shift;
    my $localb=shift;
    ($localb+$locala)*2;
}

一个实用的程序

我们已经知道了不少关于Perl的基本知识,现在可以写一个有用的程序了。
Perl的设计目标是方便文本文件的处理。 我们的第一个Perl程序将处理一个缩写单词的列表,从中找出重复的项。 这个缩写单词的列表看起来是这样的:
用Perl来做文本处理是很容易的

AC Access Class
AC Air Conditioning
AFC Automatic Frequency Control
AFS Andrew File System
...

可以从这里下载这张。 列表文件的结构是:

怎样读这样的一个文本文件呢?这里是一些按行读入文件的代码:


....
open(FD,"abb.txt")||die "ERROR: can not read file abb.txt\n";
while(){
   #do something
}
close FD;
....

"open"函数需要一个文件描述符和一个文件名作为其参数。文件描述符是一种特殊类型的变量, 你需要依次在"open"函数,某个从文件重读取数据的函数,以及"close"函数中使用这个变量。 从文件中读取数据是由<FD>完成的。<FD>可以被放到一个"while"循环中以实现按行读取。
一般的,Perl中用大写字母序列表示一个文件描述符。
那么,数据上哪儿去了?Perl有不少隐含变量。它们不需要声明,并且总是存在的。"$_"就是其中之一。 在上面的"while"循环中,这个变量保存着最近一次读入的行。 我们来试试:(下载代码):

#!/usr/bin/perl
use strict;
my $i=0;
open(FD,"abb.txt")||die "ERROR: can not read file abb.txt\n";
while(<FD>){
   # increment the line counter. You probably
   # know the ++ from C:
   $i++;
   print "Line $i is $_";
}
close FD;
当前行保存在隐含变量"$_"中。

注意,我们没有写"print "Line $i is $_\n""。 从文本文件中读入的行包含了换行符("\n")。

好,现在我们已经知道如何读文件了,但要完成我们的程序还需要知道:

  1. 怎样从行首读入单词缩写;
  2. 怎样使用Perl中的哈希表。

正则表达式提供了在文本串中搜寻特定模式的灵活方法。在这里,我们要得到 每行中第一个空格之前的字符串,换句话说,我们要寻找的模式是:"行首-->非空格字符序列-->空格"。 用Perl的正则表达式来表示就是:"^\S+\s"。把它放到"m//;"中去, Perl将用这个正则表达式来匹配"$_"变量(这个变量包含当前行,还记得吗?) 其中的"\S+"将与"非空格字符序列"相匹配。如果用括号("()")把"\S+"括起来, "非空格字符序列"将被赋给变量"$1"。
把这些东西加到我们的程序中去:

#!/usr/bin/perl -w
# vim: set sw=8 ts=8 si et:
#
use strict;
# global variables:
use vars qw($opt_h);
my $i=0;
use Getopt::Std;
#
&getopts("h")||die "ERROR: No such option. -h for help.n";
&help if ($opt_h);
#
open(FD,"abb.txt")||die "ERROR: can not read file abb.txt\n";
while(<FD>){
    $i++;
    if (m/^(\S+)\s/){
        # $1 holds now the first word (\S+)
        print "$1 is the abbreviation on line $i\n";
    }else{
        print "Line $i does not start with an abbreviation\n";
    }
}
close FD;
#
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
sub help{
     print "help text\n";
     exit;
}
#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
__END__

如果正则表达式能与当前行匹配,匹配操作符(m/ /)将返回1,我们可以把它用在一个"if" 语句中。这个"if"语句是必要的,因为需要保证变量"$1"中含有有效的数据。

 

哈希表

现在已经可以读文件并从中获得单词缩写了。剩下的事情是,我们还需要某种方法来判断 某个缩写是不是已经读过了。我们需要一种新的数据类型:哈希表。 哈希表是一种以字符串作为索引的数组。表示整个哈希表的变量以"%"开头, 同时可以通过"$变量名{"索引字符串"}"来引用表中的某个元素。 这里的"$"符号跟标量变量前面的"$"是一样的,因为哈希表中的一个元素实际上 就是一个普通的标量变量。
一个例子:

#!/usr/bin/perl -w
my %htab;
my $index;
# load the hash with data:
$htab{"something"}="value of something";
$htab{"somethingelse"}=42;
# get the data back:
$index="something";
print "%htab at index \"$index\" is $htab{$index}\n";
$index="somethingelse";
print "%htab at index \"$index\" is $htab{$index}\n";

运行这个程序,输出是:

%htab at index "something" is value of something
%htab at index "somethingelse" is 42

好了,我们完整的程序是:

 1  #!/usr/bin/perl -w
 2  # vim: set sw=4 ts=4 si et:
 3  # 
 4  use strict;
 5  # global variables:
 6  use vars qw($opt_h);
 7  my %htab;
 8  use Getopt::Std;
 9  #
10  &getopts("h")||die "ERROR: No such option. -h for help.n";
11  &help if ($opt_h);
12  #
13  open(FD,"abb.txt")||die "ERROR: can not read file abb.txt\n"; 
14  print "Abbreviations with several meanings in file abb.txt:\n";
15  while(<FD>){ 
16      if (m/^(\S+)\s/){
17          # we use the first word as index to the hash:
18          if ($htab{$1}){
19              # again this abbrev:
20              if ($htab{$1} eq "_repeated_"){
21                  print; # same as print "$_";
22              }else{
23                  # this is the first duplicate we print first
24                  # occurance of this abbreviation:
25                  print $htab{$1};
26                  # print the abbreviation line that we are currently reading:
27                  print;
28                  # mark as repeated (= appears at least twice)
29                  $htab{$1}="_repeated_";
30              }
31          }else{
32              # the first time we load the whole line:
33              $htab{$1}=$_;
34          }
35      }
36  } 
37  close FD; 
38  #
39  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
40  sub help{
41          print "finddup -- Find abbreviations with several meanins in the
42  file abb.txt. The lines in this file must have the format:
43  abrev meaning
44  \n";
45          exit;
46  }
47  #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
48  __END__ 
单击这里下载这个程序。

最后,让我们再来看看这个程序是怎么工作的:
逐行的读入一个文件,用一个名为"%htab" 的哈希表保存数据(33行),该哈希表的索引就是单词缩写。在为哈希表的某个元素赋值之前, 检查该元素是不是已经被赋值了,如果是,区分两种可能性:

  1. 是该缩写的第一次重复
  2. 已经不是第一次重复了
为区别这两种情况,在出现首次重复之后为哈希表的相应元素赋值"_repeated_"(29行)。

你最好下载这个程序自己试试。

小结

在这篇文章中你已经学到了Perl的一些细节,但我们并没有讨论到Perl的所有数据类型。 你可能想知道有没有什么办法可以不象上面那样,把文件名"abb.txt"硬编码到程序中去。 实际上,你已经学会了如何通过使用命令行选项来做到这一点(例如:finddup -f abb.txt)。
试着修改这个程序!
下一篇文章将讨论读取命令行的一般方法以及新的数据类型:数组。