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,就像这个子例程是在本程序中定义的那样。