官术网_书友最值得收藏!

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,就像這個子例程是在本程序中定義的那樣。

主站蜘蛛池模板: 亳州市| 海安县| 哈尔滨市| 桂平市| 晴隆县| 潞西市| 韩城市| 澄江县| 万源市| 兰考县| 辉县市| 克山县| 宝坻区| 宁陕县| 江西省| 集贤县| 富顺县| 淮安市| 梓潼县| 临猗县| 旬阳县| 宜丰县| 大关县| 青海省| 扎鲁特旗| 通城县| 萨嘎县| 静乐县| 进贤县| 龙陵县| 达孜县| 保山市| 琼结县| 油尖旺区| 杭州市| 菏泽市| 汝州市| 磐安县| 庆云县| 岱山县| 闻喜县|