1.4 继续改进命令行参数

1.3节中,我们对参数的假设是过于严苛的,目的是降低代码的复杂程度。

不同的选项可能有不同数量的参数值,有的只有一个,有的可能有多个。之前也提到过,我们有意不支持开关类(只有选项,而没有对应的参数值)的选项。

本节,我们的目标是:

1)根据每个选项的不同要求,合理地存储每个选项的参数值。

2)重复的选项被视为错误。

3)如果用户输入了错误的参数,程序会输出相应的提示信息,并提前结束程序。

下面我们看看实际代码:

代码1-3 ch01/read_argument_v3.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 
12 my ($opt, %value_of_opt) ;
13 for my $arg ( @ARGV ) {
14   if ( $arg =~ /^-/ ) {
15     $opt = $arg;
16     if ( exists $value_of_opt{$opt} ) {
17       print "Repeated option: $arg\n";
18       exit 1;
19     }
20     else {
21       @{ $value_of_opt{$opt} } = ();
22     }
23   }
24   elsif ( defined $opt ) {
25     push @{ $value_of_opt{$opt} }, $arg;
26   }
27   else {
28     print "Un-support option: $arg\n";
29     exit 1;
30   }
31 }
32 
33 for my $opt ( keys %value_of_opt ) {
34   if ( exists $rule_of_opt{$opt} ) {
35     if ( ${$rule_of_opt{$opt}}{'perl_type'} eq 'scalar') {
36       if ( @{ $value_of_opt{$opt} } != 1 ) {
37         print "Error: only one parameter is expected to '$opt'\n";
38         exit 1;
39       }
40     }
41     elsif ( ${$rule_of_opt{$opt}}{'perl_type'} eq 'array') {
42       if ( @{ $value_of_opt{$opt} } < 1 ) {
43         print "Error: one or more parameter is expected to '$opt'\n";
44         exit 1;
45       }
46     }
47     else {
48       print "Error: unknown 'perl_type' of '$opt'\n";
49       exit 1;
50     }
51   }
52   else {
53     print "Un-support option: '$opt'\n";
54     exit 1;
55   }
56 }
57 
58 for my $opt ( keys %value_of_opt ) {
59   print "$opt =>";
60   for my $pv ( @{ $value_of_opt{$opt} } ) {
61     print " $pv";
62   }
63   print "\n";
64 }
65 
66 exit 0;

这个程序分成4个部分:第1部分定义散列来描述选项的规则。第2部分读取命令行参数。第3部分根据规则检查命令行参数所在的散列。第4部分输出命令行参数。

第3~10行,我们声明并初始化了一个散列%rule_of_opt。它有两个键-s和-a,每个键对应的不是单纯的标量,而是一个嵌套的散列结构,由于这个散列结构没有明确的名称,我们也称它为匿名散列。这两个匿名散列各有一个键perl_type,且分别对应了一个字符串,分别是scalar和array。

第13~31行是一个for循环结构,它从@ARGV依次读取参数,把参数值赋值给标量$arg。

第14~23行的if分支,来判断$arg的类别。如果$arg是以短划线开头,紧跟数字、字母、下划线的任意组合,那么就认为$arg是一个选项,并赋值给另一个标量($opt)存储。

第16行中的exists判断某个键是否存在于散列中。如果$key是散列%one_hash的一个键,则exists $one_hash{$key}返回真,否则返回假。如果第16行的$opt此前已经存在于%value_of_opt中,则表明现在是第2次读取了,重复的选项是一种错误。

第21行,@{ $value_of_opt{$opt} }是一个数组,还记得我们之前介绍的Perl三种变量的起始字符吗?@开头的是数组,$value_of_opt{$opt}是散列%value_of_opt中的键$opt之所指。所以@{ $value_of_opt{$opt} }就是散列%value_of_opt中的键$opt指向的一个数组。我们把这个数组初始化成一个空的数组。

第24~26行,defined函数根据变量是否有值返回真假,如果有值,则返回真,否则返回假。如果$arg不符合第14行的if的条件要求,那么来到了elsif分支,此时需要判断,此前是否已经定义该选项了。如果已经定义,则把当前的$arg添加到该数组@{ $value_of_opt{$opt} }的尾部。

第27~30行,如果上述两个分支判断都失败,则来到了这个else分支,输出错信息,并终止程序,返回状态1。什么情况下,会执行到这个分支呢?是的,可能你也想到了,就是程序的第一个参数不是选项时。例如:

./read_argument_v3.pl non-option -a something

程序会输出:

Un-support option: non-option

第33~56行,遍历并检查已经存储在散列%value_of_opt中的选项和对应的参数值。在这个for循环中,包含了3层if/else判断。

第34行,判断选项($opt)是否存在于散列%rule_of_opt。如果不存在,就来到了第52~55行,输出信息,然后结束程序。如果存在,则继续下一层的判断。

第35~50行,判断散列%rule_of_opt中的键$opt,所对应的perl_type是scalar还是array。如果都不是,则来到了47~50行,输出信息,然后结束程序。eq操作符用于比对字符串,如果左右两边的字符串相同,则返回“真”,否则返回“假”。

第35~40行,处理${$rule_of_opt{$opt}}{'perl_type'}等于scalar的情形。处在if的条件中的@{ $value_of_opt{$opt} } != 1(见第36行)是Perl特有的灵活语法。“!=”是“不等于”的意思,它造就了一个“标量环境”,即假设左右两边都是标量。那么处在标量环境中的数组,意味着什么呢?它的意义很直观,就是该数组的元素数量。如果数组@{ $value_of_opt{$opt} }的元素的个数不等于1,则此表达式返回“真”,否则返回“假”。

第41~46行,处理${$rule_of_opt{$opt}}{'perl_type'}等于array的情形,检查此数组的元素数量,如果元素数量小于1,则被认为不符合程序规则,输出提示信息,然后终止程序。

第58~64行,输出所有选项和对应的参数值。

第66行,是程序的末尾,显式地结束程序,并设置返回值为0(零)。

下面将详细介绍本实例中出现的数据结构:数组的散列,散列的散列。

1.4.1 数组的散列

基本的散列如下:

%basic_hash = (
  "keyA" => "valueA",
  "keyB" => "valueB",
);

数组的散列则如下:

%array_of = (
  "keyA" =>  ["a1", "a2", …], 
  "keyB" =>  ["b1", "b2", …],
);

散列的每一个键指向的不再是单一的标量,而是一个数组。这个数组(不带标志符@)的名称是$array_of{key},完整的(带标志符@的)名称是@$array_of{key},这样编写,Perl就知道这是一个散列的键指向的数组。为了使查看代码时更明确,我们可以加上一组不改变其意义的{},甚至添加空格,如@{ $array_of{key} },这样更清晰一些。那么就可以像普通数组一样取用该数组的元素,如${ $array_of{key} }[0]。

代码1-3中,为了初始化此数组使用以下代码(第21行):

@{ $value_of_opt{$opt} } = ();

就像初始化普通数组一样。如果想要在初始化散列时就设置这些数组的元素,那么我们可以这么写:

%array_of = (
  "keyA" => ["A1", "A2",],
);

此处的中括号表示一个匿名数组的引用,$array_of{"keyA"}实际上是一个指向右侧匿名数组的引用。有关引用的详细知识,会在后续章节进行介绍。

1.4.2 散列的散列

散列的散列:

%hash_of = (
  "keyA" => { "k1" => "valueA1",
              "k2" => "valueA2",
               },
  "keyB" => { "k1" => "valueB1",
              "k2" => "valueB2",
               },
);

$hash_of{"keyA"}指向一个散列,这种用法与数组的散列类似,此时$hash_of{"keyA"}是散列名(不带标志符%),完整的散列名是%{ $hash_of{"keyA"} },除了名称有点复杂以外,其余用法与普通散列一样。要取用此散列的值,使用${ $hash_of{"keyA"} }{"k1"} = "valueA1"。同理,如果你不会混淆,也可以写为$$hash_of{"keyA"}{"k1"}

散列和数组都可以依照以上规则进行任意深度的嵌套。