original in enGuido Socher
en to gb Xiang Hong
Guido是一个老资格的Linux迷和Perl爱好者。 最近他正忙着修房子和在花园里种菜什么的。
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"程序。
下面我们来仔细的分析这个程序。
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中, 我们在使用标量变量(以$开头的变量)之前并没有声明。 它们是在被使用的时候被创建的。 对于小程序来说,这或许让人觉得很方便;但对于大的程序,这个特性很容易导致某些难于发现的错误。 而变量声明可以使被编译器能够检查出某些类型错误。
|
#!/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程序将处理一个缩写单词的列表,从中找出重复的项。
这个缩写单词的列表看起来是这样的:
|
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")。
好,现在我们已经知道如何读文件了,但要完成我们的程序还需要知道:
正则表达式提供了在文本串中搜寻特定模式的灵活方法。在这里,我们要得到
每行中第一个空格之前的字符串,换句话说,我们要寻找的模式是:"行首-->非空格字符序列-->空格"。
用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行),该哈希表的索引就是单词缩写。在为哈希表的某个元素赋值之前,
检查该元素是不是已经被赋值了,如果是,区分两种可能性:
你最好下载这个程序自己试试。