1.5 完成命令行参数
Perl程序没有类似C语言的main(“主”)函数,Perl程序中,一开始就可视为主函数。1.4节,我们使用几十行代码(见代码1-3),处理了命令行参数。在程序结构上,代码1-3是值得改进的。Perl提供了子例程(subroutine),类似其他语言中的函数。本节,我们就把1.4节的实例,改造成子例程的实现方式,看看代码结构是不是更清晰了。
代码1-4 ch01/read_argument_v4.pl
1 #!/usr/local/bin/perl 2 3 my %rule_of_opt = ( 4 '-s' => { 5 'perl_type' => 'scalar', 6 }, 7 '-a' => { 8 'perl_type' => 'array', 9 } 10 ); 11 my (%value_of_opt) ; 12 handle_argv( \@ARGV, \%rule_of_opt, \%value_of_opt ); 13 print_argv( \%value_of_opt ); 14 15 exit 0; 16 17 ### sub 18 19 sub print_and_exit { 20 print @_, "\n"; 21 exit 1; 22 } # print_and_exit 23 24 sub read_argv { 25 my ($aref, $hv) = @_; 26 my ($opt); 27 for my $arg ( @$aref ) { 28 if ( $arg =~ /^-/ ) { 29 $opt = $arg; 30 if ( exists $hv->{$opt} ) { 31 print_and_exit( "Repeated option: $arg" ); 32 } 33 else { 34 @{ $hv->{$opt} } = (); 35 } 36 } 37 elsif ( defined $opt ) { 38 push @{ $hv->{$opt} }, $arg; 39 } 40 else { 41 print_and_exit( "Un-support option: $arg" ); 42 } 43 } 44 } # read_argv 45 46 sub check_argv_perl_type { 47 my ($hr, $hv) = @_; 48 for my $opt ( keys %$hv ) { 49 if ( exists $hr->{$opt} ) { 50 if ( ${$hr->{$opt}}{'perl_type'} eq 'scalar') { 51 if ( @{ $hv->{$opt} } != 1 ) { 52 print_and_exit( "Error: only one parameter is expected to '$opt'" ); 53 } 54 } 55 elsif ( ${$hr->{$opt}}{'perl_type'} eq 'array') { 56 if ( @{ $hv->{$opt} } < 1 ) { 57 print_and_exit( "Error: one or more parameter is expected to '$opt'" ); 58 } 59 } 60 else { 61 print_and_exit( "Error: unknown 'perl_type' of '$opt'" ); 62 } 63 } 64 else { 65 print_and_exit( "Un-support option: '$opt'" ); 66 } 67 } 68 } # check_argv_perl_type 69 70 sub handle_argv { 71 my ($aref, $hr, $hv) = @_; 72 read_argv($aref, $hv); 73 check_argv_perl_type($hr, $hv); 74 } # handle_argv 75 76 sub print_argv { 77 my ($hv) = @_; 78 for my $opt ( keys %$hv ) { 79 print "$opt =>"; 80 for my $pv ( @{ $hv->{$opt} } ) { 81 print " $pv"; 82 } 83 print "\n"; 84 } 85 } # print_argv
如果我们这样执行:
./read_argument_v4.pl -s a1 -a b1 b2 b3
那么输出如下所示:(也许你得到的两行输出的上下次序不同)
-s => a1 -a => b1 b2 b3
如果我们这样执行:
./read_argument_v4.pl -s a1 a2 -a b1 b2 b3
那么输出如下所示:
Error: only one parameter is expected to '-s'
我们制作了5个子例程,这使整个程序的结构更加简洁清晰。代码1-4的功能与代码1-3的功能完全一样。子例程handle_argv调用了两个子例程read_argv和check_argv_perl_type。子例程read_argv负责读取参数,子例程check_argv_perl_type负责检查参数的类型。子例程print_and_exit只输出错误信息,然后退出程序。输出参数也由子例程print_argv完成。
命令行参数都存储在@{ $value_of_opt{$opt} }数组中。
子例程中的代码的基本结构与代码1-3中的一样,代码也几乎一样,不同点是rule_of_opt都变成了$hr–>,value_of_opt都变成了$ha–>,@ARGV都变成了@$aref。
第12行,我们调用了子例程handle_argv,并向其传递了3个参数(\@ARGV、\%rule_of_opt和\%value_of_opt),这三个参数都是变量名之前有一个反斜杠“\”,这表示一个指向其后内容的引用。引用可视为指向某块内容的内存地址。
之后两节,我们将分别介绍引用和子例程,也包含@_。
1.5.1 引用
引用(reference)是一种标量,相当于C语言中的指针,使用起来比指针更方便、更安全。在大多数情况下,引用可以视为内存中的地址。引用可以指向任何数据类型,包括标量、数组或者散列等,还可以指向子例程。
要创建引用,使用反斜杠“\”。
$sref = \$str; $aref = \@ARGV; $href = \%ENV;
要解析引用,根据引用所指向的数据类型,使用对应的符号,如下所示:
$$sref (即$str) @$aref (即@ARGV) %$href (即%ENV)
可以自行使用大括号,增强可读性,如@{$aref}。
“引用”本质上是一个标量,它引用(或指向)其他数据结构的初始地址。在调用子例程时,通常使用引用来传递复杂的数据结构,节省需要复制的数据量。
现在补充更多的有关引用的细节。
代码1-5 ch01/ch1_ref.pl
1 #!/usr/local/bin/perl 2 3 # Scalar 4 my $str = "hello" ; 5 my $sref = \$str ; 6 $$sref = "HELLO" ; 7 print $$sref, "\n"; 8 9 # Array 10 my @lines = ( "a", "b", "c" ) ; 11 my $aref = \@lines ; 12 for my $str ( @$aref ) { 13 print $str, "\n"; 14 } 15 push @$aref, "d"; 16 $aref->[0] = "A"; 17 for my $str ( @$aref ) { 18 print $str, "\n"; 19 } 20 21 # Hash 22 my %cof = ( 23 'China' => 'Beijing', 24 'England' => 'London', 25 'Japan' => 'Tokyo', 26 ); 27 my $href = \%cof ; 28 for my $k ( keys %$href ) { 29 print "$k : $href->{$k}\n" ; 30 } 31 $href->{'USA'} = 'WDC' ; 32 for my $k ( keys %$href ) { 33 print "$k : $href->{$k}\n" ; 34 } 35 36 exit 0;
运行后输出:
HELLO a b c A b c d Japan : Tokyo England : London China : Beijing USA : WDC England : London China : Beijing Japan : Tokyo
上面的程序分成了3段,分别示范了3类变量(标量、数组和散列)以及对应的引用。后面我们简称此类“指向某种变量的引用”为“引用”。要创建引用,需要在变量之前增添一个反斜杠(\)。要通过引用来获取变量本身,需要在引用之前增添一个与变量类型对应的符号(标量用$,数组用@,散列用%)。要通过引用来获取数组或散列的某个元素,需要在引用后面紧跟->(短划线紧跟大于符号),然后是[](数组)或者{}(散列)。
1.5.2 子例程
子例程就像其他语言中的函数,是可以被调用的一组代码。它可以使程序更凝练,即便是只执行一次的子例程也可以使程序结构更清晰,方便阅读或者修改。
子例程由关键字sub定义。最常用的定义方式是,由关键字sub开始,后面是子例程的名称,最后是大括号包围的子例程代码。
sub sub_name { <code here> }
调用子例程时,一般采用如下形式:
sub_name(parameters);
子例程可以在程序的任意位置定义,本书中,一般将子例程定义放在程序主体的尾部,即在exit语句之后。这样的优点是读者一打开程序,就能看到程序的主体结构,不会陷入许多子例程的细节中。
子例程每次被调用时,都有一个专属的数组@_(@符号后面紧跟下划线),它会存储某次调用时被传入的参数。它除了名称特殊以外,用法与其他普通数组一样。
sub print_by_line { for my $str (@_) { print $str, "\n"; } } print_by_line("a", "b", "c");
运行上述代码会输出:
a b c
子例程的返回值通常是代码块中最后一句语句的返回值,我们一般不依赖这个特性,而会在代码块最后写上return语句来显式地返回某个值,该值既可以是一个标量,也可以是一个列表(数组)。如果仅写return,则返回未定义(undef)值。当然,在代码块的其他位置也可以使用return。
传递给子例程的参数是按值传递的,即复制了一份参数值。
my $num = 2; sub times_three { $_[0] = $_[0] * 3; print "value: $_[0]\n"; } times_three($num); print "num is: $num\n";
运行上述代码会输出:
value: 6 num is: 2
如果传递的参数是引用,虽然引用也被复制了一份,但是引用相当于内存中的地址,所以对引用的操作,会改变其指向的变量。
子例程的参数如果包含多个数,那么子例程实际获得的参数是这些数组合并组成的一个列表。
sub add_all { my $sum = 0; for my $n ( @_ ) { $sum = $sum + $n; } return $sum; } my @nums_1 = (1, 2, 3); my @nums_2 = (4, 5); my @nums_3 = (6, 7); add_all( @nums_1, @nums_2, @nums_3 );
add_all获得的参数是依次排列的三个数组,相当于一个有7个元素的列表再传入@_数组。
子例程也支持递归调用。
1.5.3 模块
1.5.2节中实现了几个处理命令行参数的子例程。可以预见的是,之后还会编写不同的程序,也会用到这些子例程。这些子例程如何共享给其他程序使用呢?Perl提供了模块(module),使得不同的程序可以共用某段代码。一个模块一般是一个文件,或者以一个文件作为接口的多个文件组成。文件名就是模块名,文件名的后缀是.pm(Perl Module)。
我们把代码1-4中的子例程做成模块。模块名可取为My_perl_module_v1,依照Perl的惯例,模块名的首字母是大写字母。
代码1-6 perl_module/My_perl_module_v1.pm
1 package My_perl_module_v1; 2 3 sub print_and_exit { 6 } # print_and_exit 7 8 sub read_argv { ... 28 } # read_argv 29 30 sub check_argv_perl_type { ... 52 } # check_argv_perl_type 53 54 sub Handle_argv { ... 58 } # Handle_argv 59 60 1;
我们把代码1-4中的4个子例程代码(除了输出参数的子例程print_argv),原封不动地复制到新的文件My_perl_module_v1.pm,然后在第一行写上package My_perl_module_v1,这表示把这个文件打包成模块My_perl_module_v1,这个模块名必须与文件名完全一致。在文件的结束位置,添加一行1;(数字1),表示整个文件的返回状态,这是Perl的语法要求。好了,只添加了两行代码,一个模块就已经完成制作。最后,为了区别于在程序中定义的子例程,我们把将被程序直接调用的子例程handle_argv改为首字母大写的形式Handle_argv,以区别于在(主)程序内部定义的子例程。
首先,需要告诉程序,我们的模块(文件)的位置。有一种方法,是把这个模块文件,放在Perl程序默认会搜寻模块的位置。如果我们在命令行中执行:
perl -e "use something"
然后程序会告诉我们,找不到这个叫作“something”的模块,那么,它曾经找过哪些位置呢?它会告诉我们:
Can't locate something.pm in @INC (you may need to install the something module) (@INC contains: /usr/local/Cellar/perl/5.28.0/lib/perl5/site_perl/5.28.0/darwin-thread-multi-2level /usr/local/Cellar/perl/5.28.0/lib/perl5/site_perl/5.28.0 /usr/local/Cellar/perl/5.28.0/lib/perl5/5.28.0/darwin-thread-multi-2level /usr/local/Cellar/perl/5.28.0/lib/perl5/5.28.0 /usr/local/lib/perl5/site_perl/5.28.0/darwin-thread-multi-2level /usr/local/lib/perl5/site_perl/5.28.0) at -e line 1.
数组@INC包含了程序会寻找模块的位置,如果我们把My_perl_module_v1.pm放到其中任意一个位置,那么我们就可以像使用内建的模块一样使用My_perl_module_v1了:
use My_perl_module_v1;
不过,更常见的是另一种方法—使用一个专门的路径,放置自制的Perl模块。比如../perl_module/My_perl_module_v1.pm。那么我们的用法如代码1-7所示。
代码1-7 ch01/read_argument_v5.pl
1 #!/usr/local/bin/perl 2 3 use lib "../perl_module"; 4 use My_perl_module_v1; 5 6 my %rule_of_opt = ( 7 '-s' => { 8 'perl_type' => 'scalar', 9 }, 10 '-a' => { 11 'perl_type' => 'array', 12 } 13 ); 14 my (%value_of_opt) ; 15 My_perl_module_v1::Handle_argv( \@ARGV, \%rule_of_opt, \%value_of_opt ); 16 print_argv( \%value_of_opt ); 17 18 exit 0; 19 ### sub 20 sub print_argv { (此处省略了多行) 29 } # print_argv
第3行,use lib <directory> 语句告诉程序自制模块所在的目录。
第4行,use <module_name>语句使用模块。
第15行,调用模块中定义的子例程,使用<module_name>::<sub_route>形式完成此操作,请注意,中间有两个冒号。后面的参数列表与非模块形式一样。
好了,我们完成了最简的模块复用。运行代码1-7,其结果与代码1-4的结果一致。
这样仍然有一些不便利,就是每次调用某个子例程时,需要输入模块名和两个冒号。能不能省略呢?答案是可以的。我们制作第二个模块,叫作My_perl_module_v2.pm。
代码1-8 perl_module/My_perl_module_v2.pm
1 package My_perl_module_v2; 2 3 use parent qw(Exporter); 4 our @EXPORT = qw(Handle_argv);
第4行后面省略的内容与My_perl_module_v1.pm(代码1-6)一样。
第1行,模块的名称为My_perl_module_v2。
第3行,使用了一个pragma(某类特殊模块):parent。qw(Exporter)是一个列表。使parent模块中的Exporter在当前程序(My_perl_module_v2.pm)中生效。更详细的内容,请参见9.3.3节。
第4行,使用指令our声明了一个数组@EXPORT,这个数组的名字是固定的,即@EXPORT。our类似于my,也是声明变量,更多详情请参见9.2.9节。这个数组的元素就是本模块对外的“出口”,也就是说,它使调用本模块的程序可以见到Handle_argv,而不必显式地调用My_perl_module_v2::Handle_argv。于是,我们的程序可以如代码1-9所示调用这个模块中的子例程了。
代码1-9 ch01/read_argument_v6.pl
1 #!/usr/local/bin/perl 2 3 use lib "../perl_module"; 4 use My_perl_module_v2; 5 6 my %rule_of_opt = ( 7 '-s' => { 8 'perl_type' => 'scalar', 9 }, 10 '-a' => { 11 'perl_type' => 'array', 12 } 13 ); 14 my (%value_of_opt) ; 15 Handle_argv( \@ARGV, \%rule_of_opt, \%value_of_opt ); 16 print_argv( \%value_of_opt ); 17 18 exit 0; 19 ### sub 20 sub print_argv { (此处省略了多行) 29 } # print_argv
第4行,换了一个模块名My_perl_module_v2。
第15行,直接调用模块My_perl_module_v2中的子例程Handle_argv,就像这个子例程是在本程序中定义的那样。