《高阶Perl》——第2章 分配表 2.1 配置文件处理

简介: 本节书摘来自华章计算机《高阶Perl》一书中的第2章,第2.1节,作者(美)Mark Jason Dominus,译 滕家海,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

第2章

分 配 表

第1章介绍了如何用别的函数参数化函数的行为使函数更加灵活。例如,并没有把每次移动盘子就输出一条消息硬编码到hanoi()函数里,而是让其调用一个从外部传入的辅助函数。通过提供一个合适的辅助函数,可以使hanoi()输出一系列说明,或检查它自己的行动,或生成一个图形显示,而不必重新编写基本的算法。类似地,可以从total_size()函数的计算文件大小的行为中提取出目录遍历行为,得到一个更有价值和普遍适用的dir_walk()函数,它可以做许多不同的事情。

为了从hanoi()与dir_walk()提取出行为,使用了代码引用。把别的函数作为参数传递给hanoi()与dir_walk()函数,有效地把辅助函数当成数据块。代码引用使这些成为可能。

现在先不讲递归,而叙述代码引用的另一种用法。

2.1 配置文件处理

假设我们有一个应用要读取一个如下格式的配置文件:

VERBOSITY       8
CHDIR           /usr/local/app
LOGFILE         log
...             ...

要读取这个配置文件并根据每个指示采取适当的行动。例如,对于VERBOSITY指示,只是设置一个全局变量。而对于LOGFILE指示,则要立即重定向程序的诊断消息到指定的文件。对于CHDIR,也许可以让程序chdir指定的目录以使随后的文件操作与新的目录相关联。这意味着,在之前的例子里LOGFILE是/usr/local/app/log,而不是用户在程序运行时恰好所在的目录下的log文件。

许多程序员会遇到这个问题并会立即想象到一个含有巨大if-else分支的函数,如下:

sub read_config {
  my ($filename) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    if ($directive eq 'CHDIR') {
      chdir($rest) or die "Couldn't chdir to '$rest': $!; aborting";
    } elsif ($directive eq 'LOGFILE') {
      open STDERR, ">>", $rest
        or die "Couldn't open log file '$rest': $!; aborting";
    } elsif ($directive eq 'VERBOSITY') {
      $VERBOSITY = $rest;
    } elsif ($directive eq ...) {
      ...
    } ...
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

这个函数分为两部分。第一部分打开文件并每次从中读取一行。它把每行分成$directive部分(第一个单词)和$rest部分(剩余的部分)。$rest部分包含了指示的参数,如提供给LOGFILE指示的要打开的日志文件名。函数的第二部分是一棵大的if-else树,它检查$directive变量,查看它是哪个指示,如果指示不可识别,则中断程序。

这类函数可以变得非常庞大,因为在if-else树中有许多选项。每次有人想增加一个指示,他就要改变函数增加一个elsif分句。if-else树的分枝的内容相互之间没有很多事情要做,除了它们都是可配置的琐碎事实。这样的函数违背了编程的一条重要法则:相关的东西应该放在一起;不相关的东西应该分开。

依照此法则为这个函数提出了一个不同的结构:读取和解析文件的部分应该与配置的指示被识别后的执行动作分开。此外,实现各种不相关的指示的代码不应该一起挤进单个函数。

2.1.1 表驱动配置

可以把打开、读取和解析配置文件的代码与实现不同指示的不相关的代码分开。像这样把程序分成两半将可以更加灵活地修改每部分,也把代码与指示分开了。

有read_config()的一个替代版本:

### Code Library: rdconfig-tabular
sub read_config {
  my ($filename, $actions) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
      $actions->{$directive}->($rest);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

和之前完全一样地打开、读取和解析配置文件。但不再依赖巨大的if-else分支了。而这版read_config接受一个额外的参数,$actions,它是一个行动表,read_config()每读取一个配置的指示,它将执行这些行动之一。这个表就称为分配表(dispatch table),因为它包含了read_config()读文件时将要把控制分配到的函数。变量$rest的意义和之前相同,但现在作为一个参数传递给合适的行为函数。

一个典型的分配表如下:

$dispatch_table =
{ CHDIR      => \&change_dir,
  LOGFILE    => \&open_log_file,
  VERBOSITY  => \&set_verbosity,
  ...        => ...,
};

分配表是一个散列,它的键(通常称为标签(tag))是指示的名称,它的值是行为(action),指向当识别出合适的指示名时调用的子例程。行为函数期望接受变量$rest作为一个参数,典型的行为如下:

sub change_dir {
  my ($dir) = @_;
  chdir($dir)
    or die "Couldn't chdir to '$dir': $!; aborting";
}

sub open_log_file {
  open STDERR, ">>", $_[0]
    or die "Couldn't open log file '$_[0]': $!; aborting";
}

sub set_verbosity {
  $VERBOSITY = shift
}

如果行为很小,就可以直接把它们放到分配表里:

$dispatch_table =
  { CHDIR      => sub { my ($dir) = @_;
                       chdir($dir) or
                         die "Couldn't chdir to '$dir': $!; aborting";
                      },
    LOGFILE    => sub { open STDERR, ">>", $_[0] or
                          die "Couldn't open log file '$_[0]': $!; aborting";
                      },
    VERBOSITY  => sub { $VERBOSITY = shift },
    ...        => ...,
};

通过转变为一个分配表,消除了巨大的if-else树,但是到头来还是得到了一个只小了一点的表。这看起来不太成功。但是表带来了几个好处。

2.1.2 分配表的优势

分配表是数据,而不是代码,所以它可以在运行时改变。你可以在你想的任何时候插入新的指示到表里。假设表含有:

'DEFINE' => \&define_config_directive,

其中,define_config_directive()是:

### Code Library: def-conf-dir
sub define_config_directive {
  my $rest = shift;
  $rest =~ s/^\s+//;
  my ($new_directive, $def_txt) = split /\s+/, $rest, 2;

  if (exists $CONFIG_DIRECTIVE_TABLE{$new_directive}) {
    warn "$new_directive already defined; skipping.\n";
    return;
  }

  my $def = eval "sub { $def_txt }";
  if (not defined $def) {
    warn "Could not compile definition for '$new_directive': $@; skipping.\n";
    return;
  }

  $CONFIG_DIRECTIVE_TABLE{$new_directive} = $def;
}

配置器现在接受这样的指示:

DEFINE HOME       chdir('/usr/local/app');

define_config_directive()把HOME放入$new_directive并把chdir('/usr/local/app');放入$def_txt。它用eval把定义文本编译成一个子例程,然后把新的子例程装入一个主配置表,%CONFIG_DIRECTIVE_TABLE,以HOME为键。如果事实上%CONFIG_DIRECTIVE_TABLE是一开始就传递给read_config()的分配表,那么read_config()将会看到新的定义,如果在输入文件的下一行看到指示HOME,就将把一个行为关联到HOME。现在一个配置文件如下:

DEFINE HOME       chdir('/usr/local/app');
CHDIR /some/directory
...
HOME

在...里的指示是在目录/some/directory里被执行。当处理器到达HOME时,它就返回到它的家目录。也可以定义一个相同的但更健壮的版本:

DEFINE PUSHDIR  use Cwd; push @dirs, cwd(); chdir($_[0])
DEFINE POPDIR   chdir(pop @dirs)

PUSHDIR dir用标准Cwd模块提供的cwd()函数指出当前目录的名称。它把当前目录的名称保存在变量@dirs里,然后改变到目录dir。POPDIR撤销最后一个PUSHDIR的影响:

PUSHDIR /tmp
A
PUSHDIR /usr/local/app
B
POPDIR
C
POPDIR

程序改变到/tmp,执行指示A。然后改变到/usr/local/app并执行指示B。随后的POPDIR使程序回到/tmp,在那里执行指示C,最后第二个POPDIR使程序回到它开始的地方。

为了使DEFINE能改变配置表,将不得不把它存入一个全局变量。如果明确地把表传递给define_config_directive也许更好。为此需要对read_config做一点小小的改变:

### Code Library: rdconfig-tablearg
sub read_config {
  my ($filename, $actions) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
      $actions->{$directive}->($rest, $actions);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

现在define_config_directive如下:

### Code Library: def-cdir-tablearg
sub define_config_directive {
  my ($rest, $dispatch_table) = @_;
  $rest =~ s/^\s+//;
  my ($new_directive, $def_txt) = split /\s+/, $rest, 2;

  if (exists $dispatch_table->{$new_directive}) {
    warn "$new_directive already defined; skipping.\n";
    return;
  }

  my $def = eval "sub { $def_txt }";
  if (not defined $def) {
    warn "Could not compile definition for '$new_directive': $@; skipping.\n";
    return;
  }

  $dispatch_table->{$new_directive} = $def;
}

有了这个改变,就可以增加一个确实有用的配置指示了:

DEFINE INCLUDE    read_config(@_);

它安装一个新的条目到分配表里,如下:

INCLUDE => sub { read_config(@_) }

现在,当在配置文件里写:

INCLUDE extra.conf

主函数read_config()将执行行为,传递给它两个参数。第一个参数是从配置文件里得到的$rest,在这个例子里是文件名extra.conf。第二个参数还是分配表。将把这两个参数直接传递给read_config的递归调用。read_config将读取extra.conf,当它结束时就会把控制交给read_config的主调用,后者将继续处理主要的配置文件,从刚才离开的地方继续。

为了递归调用能正确工作,read_config()必须是可重入的。破坏可重入性最简单的方法是使用全局变量,如使用一个全局文件句柄代替词法文件句柄。如果使用了一个全局文件句柄,递归调用read_config()将会用同样被主调用使用的句柄打开extra.conf,这将会关闭主配置文件。当递归调用返回时,read_config()将无法读取主文件的剩余部分,因为它的文件句柄已经关闭了。

INCLUDE这个定义非常简单也非常实用。但它也是巧妙的,也许写read_config的时候都没有意识到。“read_config不需要是可重入的”说起来简单。然而,如果已经写了不可重入的read_config,那么有用的INCLUDE定义将不会起作用。在这里可以学到一个重要的经验:默认使函数是可重入的,因为有时递归调用带来的好处将是一个惊喜。

可重入的函数展现了比不可重入的函数更简单和更可预见的行为。它们更加灵活因为它们可以递归地调用。INCLUDE例子表明无法总预见到所有的想递归地执行一个函数的理由。更好也更安全的是尽可能使所有函数是可重入的。

分配表与在read_config()里硬编码相比较,另一个优势是可以使用同一个read_config函数处理两个不相关并且有完全不同指示的文件,只要每次传递一个不同的分配表给read_config()。可以通过传递一个简装的分配表给read_config()而使程序处于“初学者模式”。或者可以重复利用read_config()处理另一个带有相同基本语法的文件,只要传递给它一个带有一套不同的指示的表即可。在2.1.4节有这样的一个例子。

2.1.3 分配表策略

在PUSHDIR与POPDIR实现中,行为函数使用了一个全局变量,@dirs, 维护压入的目录的栈。这效果不好。可以通过让read_config()支持一个用户形参(user parameter)克服它,使系统更灵活。这是一个参数,由read_config()的主调者提供,一字不变地传递给行为:

### Code Library: rdconfig-uparam
sub read_config {
  my ($filename, $actions, $user_param) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
     $actions->{$directive}->($rest, $user_param, $actions);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

这消除了全局变量,因为现在可以像这样定义PUSHDIR和POPDIR了:

DEFINE PUSHDIR  use Cwd; push @{$_[1]}, cwd(); chdir($_[0])
DEFINE POPDIR   chdir(pop @{$_[1]})

形参$_[1]指向被传递给read_config()的用户形参参数。如果read_config()这样调用:

read_config($filename, $dispatch_table, \@dirs);

那么PUSHDIR和POPDIR将用数组@dirs作为它们的栈,如果它这样调用:

read_config($filename, $dispatch_table, []);

那么它们将使用一个崭新的、匿名的数组作为栈。

向一个行为回调传递一个要执行的行为的标签名称常常是有用的。为此,可以改变read_config():

### Code Library: rdconfig-tagarg
sub read_config {
  my ($filename, $actions, $user_param) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    my ($directive, $rest) = split /\s+/, $_, 2;
    if (exists $actions->{$directive}) {
      $actions->{$directive}->($directive, $rest, $actions, $user_param);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

为什么这是有用的?参考为VERBOSITY指示定义的行为:

VERBOSITY => sub { $VERBOSITY = shift },

容易想象会有一些配置指示遵循这个通用模式:

VERBOSITY => sub { $VERBOSITY = shift },
TABLESIZE => sub { $TABLESIZE = shift },
PERLPATH  => sub { $PERLPATH = shift },
... etc ...

把这三个类似的行为合并成单个做这三件工作的函数。为此,函数需要知道指示的名称以便设置合适的全局变量:

VERBOSITY => \&set_var,
TABLESIZE => \&set_var,
PERLPATH  => \&set_var,
... etc ...

sub set_var {
  my ($var, $val) = @_;
  $$var = $val;
}

或者,如果你不喜欢一堆松散的全局变量,你可以把配置信息保存到一个散列里,然后传递这个散列的引用作为用户形参:

sub set_var {
  my ($var, $val, undef, $config_hash) = @_;
  $config_hash->{$var} = $val;
}

在这个例子里,节省的不多,因为行为如此简单。然而可能有几个配置指示需要共享一个更复杂的函数。这里有一个稍微复杂些的例子:

sub open_input_file {
  my ($handle, $filename) = @_;
  unless (open $handle, $filename) {
    warn "Couldn't open $handle file '$filename': $!; ignoring.\n";
  }
}

这个open_input_file()函数可以被许多配置指示分享。例如,假设一个程序有三个输入文件:一个历史文件、一个临时文件和一个模式文件。希望这三个文件的位置都可以在配置文件里配置,这需要在分配表里有三个条目。但是三个条目都可以共享相同的open_input_file()函数:

...
HISTORY  => \&open_input_file,
TEMPLATE => \&open_input_file,
PATTERN  => \&open_input_file,
...

现在假设配置文件认为:

HISTORY         /usr/local/app/history
TEMPLATE        /usr/local/app/templates/main.tmpl
PATTERN         /home/bill/app/patterns/default.pat

read_config()将看到第一行并分配给open_input_file()函数,传递给它的参数列表是('HISTORY','/usr/local/app/history')。open_input_file()将参数HISTORY

作为文件句柄名,并把HISTORY文件句柄打开到文件/usr/local/app/history。第二行,

read_config()将再次分配给open_input_file(),这次传递给它('TEMPLATE',
'/usr/local/app/templates/main.tmpl')。这次,open_input_file()将打开TEMPLATE

句柄而不是HISTORY句柄。

2.1.4 默认行为

例子中的read_config()函数一遇到无法识别的指示就会崩溃。这种行为是硬编码在其中的。如果分配表自身携带了对一个无法识别的指示要做什么的信息,那会更好。增加这个功能很简单:

### Code Library: rdconfig-default
sub read_config {
  my ($filename, $actions, $userparam) = @_;
  open my($CF), $filename or return; # Failure
  while (<$CF>) {
    chomp;
    my ($directive, $rest) = split /\s+/, $_, 2;
    my $action = $actions->{$directive} || $actions->{_DEFAULT_};
    if ($action) {
      $action->($directive, $rest, $actions, $userparam);
    } else {
      die "Unrecognized directive $directive on line $. of $filename; aborting";
    }
  }
  return 1; # Success
}

这里的函数在行为表里寻找指定的指示,如果没有,它就寻找_DEFAULT_行为,仅当分配表里没有指定的默认行为时崩溃。这里有一个典型的_DEFAULT_行为:

sub no_such_directive {
  my ($directive) = @_;
  warn "Unrecognized directive $directive at line $.; ignoring.\n";
}

由于把指示的名称作为第一个参数传递给行为函数,因此默认的行为知道调用无法识别的指示代表什么。由于no_such_directive()函数也得到了传递的整个分配表,因此它可以抽取到真实的指示名称并通过模式匹配指出可能的含义。这里no_such_directive()用一个假想的score_match()函数判断哪个表条目良好地匹配无法识别的指示:

sub no_such_directive {
  my ($bad, $rest, $table) = @_;
  my ($best_match, $best_score);
  for my $good (keys %$table) {
    my $score = score_match($bad, $good);
    if ($score > $best_score) {
      $best_score = $score;
      $best_match = $good;
    }
  }
  warn "Unrecognized directive $bad at line $.;\n";
  warn "\t(perhaps you meant $best_match?)\n";
}

现在拥有的系统只含有少量代码,但它是极其灵活的。假设程序还要读取一系列用户ID与电子邮件地址,格式如下:

fred            fred@example.com
bill            bvoehno@plover.com
warez           warez-admin@plover.com
...             ...

可以复用read_config()并让它读取和解析这个文件,通过提供合适的分配表:

$address_actions =
  { _DEFAULT_ => sub { my ($id, $addr, $act, $aref) = @_;
                       push @$aref, [$id, $addr];
                     },
  };

read_config($ADDRESS_FILE, $address_actions, \@address_array);

这里已经给了read_config()一个非常小的分配表,它只有一个_DEFAULT_条目。read_config()对地址文件里的每一行都将调用这个默认的条目一次,传递给它“指示名称”(实际上即用户ID)与地址(即$rest的值)。默认的行为将获得这些信息并增加到@address_array,程序可以在以后使用它。

相关文章
|
9天前
|
C语言
转载 - gcc/ld 动态连接库和静态连接库使用方法
本文介绍了如何在GCC中实现部分程序静态链接、部分动态链接。使用`-Wl`标志传递链接器参数,`-Bstatic`强制链接静态库,`-Bdynamic`强制链接动态库。
15 0
|
29天前
|
运维 Linux Apache
LAMP架构调优(三)——模块的安装与调用
LAMP架构调优(三)——模块的安装与调用
9 0
|
10月前
无法找到可用的配置文件来构建预处理器
无法找到可用的配置文件来构建预处理器
59 0
|
Python
一日一技:如何让自己的工具函数在Python全局可用?
一日一技:如何让自己的工具函数在Python全局可用?
217 0
一日一技:如何让自己的工具函数在Python全局可用?
|
数据挖掘
无事来学学--Kettle中应用,统计,映射组件的使用
应用组件 转换里面的第五个分类。应用都是一些工具类。
217 0
|
JSON 数据格式 索引
ES7学习笔记(五)动态映射
通常情况下,我们使用ES建立索引的步骤是,先创建索引,然后定义索引中的字段以及映射的类型,然后再向索引中导入数据。而动态映射是ES中一个非常重要的概念,你可以直接向文档中导入一条数据,与此同时,索引、字段、字段类型都会自动创建,无需你做其他的操作。
1851 0

热门文章

最新文章