- Perl語言IC設計實踐
- 滕家海編著
- 3514字
- 2022-02-08 17:38:00
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,就像這個子例程是在本程序中定義的那樣。