AppConfig 是在简单情况下用 Perl 配置应用程序的一种有用工具,但是有些时候,在命令行处理和配置文件解析方面需要更多功能。不是使用 XML 或 YAML 这类数据格式,您可以通过稍许额外的努力,改变 AppConfig 使其能够处理复杂的命令行开关以创建多层散列。
基于 Perl 配置应用程序的方法包括,从 XML 或 YAML 到 Getopt 模块,到配置文件中的 Perl 代码。这些方法的结果就是,Perl 程序员选择这些方法中的一种,同时忽视其他的。
相反,程序员应该努力平衡代码的复杂性和用户的需要。一般而言,最简单、最自然的配置是最难支持的,因为自然语言简直太有组织性了。很多用户,特别是 Unix 用户,已经习惯于相信特定的配置文件格式是“自然的”而且易于学习的,但是事实上,那些格式经过了相当好的定制并且能力极好,对新手来说却是晦涩难懂的。例如,比较 Unix 的 /etc/passwd 文件格式:
tzz:x:500:29:Ted Zlatanov,,,:/home/tzz:/bin/tcsh
和 fetchmail 的配置文件格式:
# Configuration created Mon Mar 13 14:42:23 2000 by fetchmailconf
set postmaster "postmaster"
set bouncemail
set properties ""
set daemon 30
poll mail.server.net with proto POP3
user tzz there with password PASSWORD is tzz here options warnings 3600
初级程序员经常遗漏的一点是,配置支持是很难;因而您应该花费一些时间在这上面。另外,考虑到程序的配置经常会迫使您系统化它可以做什么以及用户如何影响它的作用。这是非常有益的想法,所以我强烈推荐程序员通过释放配置文件格式开始编写他们的程序。(当然,紧接着系统化程序配置的下一个任务就是编写文档 —— 是的,完全正确,就是在开始编码之前。但这是另一个主题。)
AppConfig ── 一个卓越的代码块模块,充满了配置应用程序这一通用问题的实用和有益的方法 ── 为功能和简单性之间带来了极好的平衡,另一个好处是一个统一的命令行和配置文件解析器。
我曾经在以前的一个两部分的系列文章中讨论过 AppConfig(参阅 参考资料),因而我们不会在本文中介绍该模块的基础知识。您应该熟悉 Perl 和 多层配置,这也意味着这篇文章可能仅有益于这些 新 Perl 程序员,如果他们愿意花时间理解讨论过的基本主题的话(参考资料部分的链接将会提供帮助)。
关于 AppConfig 与其他模块的比较。我并不偏爱任意特定的 Perl 模块,包括 AppConfig。我只是使用适合的模块。因此,如果 AppConfig 对于某一任务来说并非最佳工具,我将不会试图碰钉子。您同样也不应该。
观察配置
编写配置所必要的基本技能是简化和提炼用户的需求,直到它们可以通过一些直接的方式表达。例如,“删除这个文件”是 Unix 下 rm 命令的目的。Unix 用户知道的常用选项是 -r 和 -f;一个是递归地删除一个目录(对于空目录,这将比键入特有的目录删除命令即 rmdir 要快很多),另一个选项强制删除而不需要用户对它希望的操作进行确认。
用户可以将这些选项表达为“删除这个,我就是这个意思”和“删除从这儿开始的所有文件。”注意,并没有一个选项能够使 rm 完成诸如创建一个链接这样不寻常的操作。当然,最重要的选项是需要被删除的文件列表;该列表被假定为命令行中不以 - 开头的任何东西(实际的规则会稍微复杂一些)。一个常见的恶作剧是创建名为 -rf 的文件以及另一个名为 * 的文件,这样,当这个可怜的用户尝试删除它们的时候,他将会删除所有的文件(因为 * 将会在 rm 看到它之前被 shell 扩展)。
rm 命令的这些细微差别显示了,即使是一个简单的、设计精美的程序,也带有可能带给用户危险的微妙的选项。是的, * 被扩展是 shell 的缺陷。是的, -i 开关对新用户来说是非常必要的。要点就是选项带来了复杂性;程序(和程序员)必须为处理这些复杂性做好准备。
这样,我提出了一个简单的规则,即 Zlatanov 配置定律:您只能包括这三点中的两个 ── 简单代码,简单配置,或者简单操作。本文将会向您显示如何在这三者间实现平衡,但是这并不能改变用户体验的底层复杂性。
什么是复杂的分层配置?
复杂的分层配置是这样的配置,它们:
- 需要额外的处理。
- 具有多于一层深度的嵌套配置(直到可以被 AppConfig 散列处理的一层)。
- 符合上述两点。
注意,这样做的时候,我们将遇到 AppConfig 的 bleeding edge(有风险的新功能)。有很多使用 AppConfig 的理由(并不仅仅是因为其配置文件和命令行选项的简单性),但是如果您在一个星期六的下午发现自己的嵌套配置有八层深,那么该考察 XML、YAML或者其他为这个目的而设计的便携式数据格式了。
我们这里将要描述的方法打算作为您转移到诸如 XML 这样的强大数据格式之前的中间步骤。(XML 并不能处理命令行开关,因而您必须使用不同的方式处理它们,也许会是 Getopt,这个模块将会给您的程序带来复杂性。)
哪些配置需要额外的处理呢?通常是那些在程序开始之前不能够完全理解的配置。例如,-f 开关通常用于告诉程序读取一个配置文件,这将因为该文件可能覆盖或者补充命令行上给出的选项而使处理变复杂。
分层配置是指那些包含了复杂结构条目的配置。例如,程序也许需要告诉用户列表,他们可以使用它了,以及每个人所应该得到的特权(sudo 程序就是这样程序的一个示例 ── sudo 必须了解用户和组,他们在哪些机器上被赋予了使用 sudo 的特权,以及他们可以利用哪些特权)。所以配置实体为 user-or-group.machine(s).privilege(s) 或者三层,可能在每个子层带有多个实体(sudo 还支持类似 NOPASSWD 的选项,这是主配置实体的侧面附加物,因而并未添加层次)。
AppConfig 功能
AppConfig 最重要的功能就是,它将命令行选项和配置文件选项统一到一种数据结构中。尽管这会导致复杂化。
在众所周知的 CPAN 范围之外,AppConfig 可以做额外的处理,但是您必须决定事件发生的顺序。为了避免惊讶,从命令行开始,然后读取通过命令行开关指定的文件。这意味着配置文件中的选项将会覆盖命令行中的选项。按照相反的方式处理(使用命令行文件开关读取文件,然后处理其他命令行开关)也是可能的。我们会示范这两种方法。
对于分层配置,AppConfig 需要安装有定制函数,以解释不止一层深的散列。您必须小心不要太过扩展;多于三到四层就会变得简直无法处理(但是再一次强调,如果您的配置的层次多于三层,那么 AppConfig 将的确不再是适合的工具)。我们将为这个方法提供一个示例。
命令行选项及文件
首先是解析命令行选项,只需调用 args() 方法即可。
清单 1. 通过调用 args() 开始解析命令行选项及文件
#!/usr/bin/perl -w
use strict;
use AppConfig qw/:argcount/;
my $config = AppConfig->new();
$config->define(
CONFIG_FILE =>
{ ARGCOUNT => ARGCOUNT_ONE, ALIAS => 'F' },
DEBUG =>
{ ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'H' },
);
$config->args();
print "Debug is ", ($config->DEBUG())? 'on':'off', " before reading the file\n";
$config->file($config->CONFIG_FILE())
if $config->CONFIG_FILE();
print "Debug is ", ($config->DEBUG())? 'on':'off', " after reading the file\n";
|
args() 方法消费 @ARGV 中被认为是开关的内容。例如:
- 如果
@ARGV 包含 -a 及 hello,那么 args() 也许消费,也许不消费 hello。
- 如果
-a 是一个布尔开关,那么 hello 将不会被消费。
- 如果
-a 是一个带有参数(比如 ARGCOUNT_LIST、ARGCOUNT_HASH、ARGCOUNT_ONE)的开关,那么 hello 将会被消费。
- 如果
-a 是一个散列开关,那么 hello 仅会创建一个带有 undef 值的 hello 键。
一旦命令行选项告诉我们需要读入某个文件之后,只需要调用 file() 方法。如果该文件没有找到或者不可读,AppConfig 将会默认地打印一个警告信息,程序还是会继续运行。对用户来说,这样做通常是正确的,但是如果您希望错误选项更加严格,您可以激活 PEDANTIC 选项,手册中有该选项的详细解释。
文件在命令行选项之前
在命令行开关之前处理配置文件需要一定的技巧。您可能想到先保存 @ARGV,然后在读取配置文件之后恢复它,并且再次调用 args()。这将为任何命令行给出的列表创建两份实体。
假定选项是 -call 呼叫某人(一个列表),并在命令行指定了 -call joe。如果您调用了 args() 两次,您将会呼叫 Joe 两次。如果您从列表中消除重复,就可能破坏两次呼叫 Joe 的意图!
在我看来,最简单的解决方案是手工解析 @ARGV,如下所示。
清单 2. 手工解析 @ARGV
#!/usr/bin/perl -w
use strict;
use AppConfig qw/:argcount/;
my $config = AppConfig->new();
$config->define(
CONFIG_FILE =>
{ ARGCOUNT => ARGCOUNT_ONE, ALIAS => 'F' },
DEBUG =>
{ ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'H' },
);
my $file_switch_found = 0;
foreach my $arg (@ARGV)
{
# note that this should come FIRST
# or it will pick up $file_switch_found from the case below
if ($file_switch_found)
{
$config->CONFIG_FILE($arg);
$file_switch_found = 0;
}
# and this should come SECOND
if (lc $arg eq '-f' || lc $arg eq '-config_file')
{
$file_switch_found = 1;
}
}
print "Debug is ", ($config->DEBUG())? 'on':'off', " before reading the file\n";
$config->file($config->CONFIG_FILE())
if $config->CONFIG_FILE();
print "Debug is ", ($config->DEBUG())? 'on':'off', " after reading the file\n";
$config->args();
print "Debug is ", ($config->DEBUG())? 'on':'off', " after processing the arguments\n";
|
除了更难于维护的事实之外,这个方法还会给用户带来更大的迷惑。但是它是可行的。多个 -f 开关将导致最后一个文件被读取。
一个技巧在于 @ARGV 循环,如果您在检查 -f 开关之后又检查了 $file_switch_found ,它将在处理 -f 开关的过程中自动打开。(注意,这个循环避免了知道它在 @ARGV 数组中的位置 ── 这在 Perl 中是清洁代码的基本步骤。)
一旦您开始跟踪数组偏移,您的代码就会变得更加复杂,以至于任何时候您都应该避免通过索引迭代通过一个数组。
解释多层散列
通常,如果您在 AppConfig 中使用 -hashoption a=b=c,它将立即为关键字 "a" 赋值 "b=c"。我将示范如何创建带有任意嵌套层级的深层嵌套的散列(但是再一次警告您,不要在这个方法中走得过深,如果不使用更加适当的方法,比如 XML 或 YAML 的话)。
另外,让我们直接通过 $state->{VARIABLE} 散列修改 AppConf 的状态。根据 AppConfig 作者的需要,这也许在 AppConfig 1.56 之后的版本中不能使用,但是作者未必会做出这样的改变。
清单 3. 解释多层散列
#!/usr/bin/perl -w
use strict;
use AppConfig qw/:argcount/;
use Data::Dumper;
my $config = AppConfig->new();
$config->define(
NESTED =>
{
ARGCOUNT => ARGCOUNT_HASH,
ALIAS => 'N',
ACTION => \&nesting_action
},
);
$config->args();
print "Nested option is ", Dumper($config->NESTED());
sub nesting_action
{
my $state = shift @_;
my $vname = shift @_;
my $value = shift @_;
# AppConfig can handle this
return 1 unless ($value =~ m/=.*=/);
my @matches = ($value =~ m/(("[^"]+"|[^=]+))=?/g)
if $value;
my @m;
my $m = scalar @matches;
foreach (0..($m/2-1))
{
my $lost = shift @matches;
my $found = shift @matches;
$found =~ s/^"(.*)"$/$1/;
push @m, $found;
}
@matches = @m;
# we will always match this
my $firstkey = shift @matches;
# nothing to do if we can't get a first key
return unless defined $firstkey;
# now, put in the parsed value
$state->{VARIABLE}->{$vname}->{$firstkey} = {};
my $hash = $state->{VARIABLE}->{$vname}->{$firstkey};
while (scalar @matches > 2)
{
my $key = shift @matches;
$hash->{$key} = {};
$hash = $hash->{$key};
}
# note we could have zero @matches, so check first
if (scalar @matches > 1)
{
# we use pop and not shift because of the order of evaluation
$hash->{pop @matches} = pop @matches;
}
}
|
当然所有有趣的代码都在 nesting_action() 函数中。
首先,获得所有与一看起来非常复杂的正则表达式匹配的结果。有兴趣的话,还可以在值中加入 "=" 字符。在这样的情况下,用户必须在值的周围加上引号,而在值的中间不能使用引号(尽管它们可以在值中被它们自己所使用)。也许您惊讶于解析为何如此简单,我并不考虑过于复杂而缺少趣味或者教育性的代码。
如果在用户提供的值中存在不多于一个 "=" 字符,就让 AppConfig 来处理它。其他操作只会使代码复杂化,而并无益处。
在 Perl 6 中,正则表达式将会更简单,处理也会更加容易,因为这个版本在匹配分组和数据提取的分组之间作了区分。这儿,得到两个值中的每一个,并且循环提取一个值。此外,还应除去两边的引号(如果有的话)。最终的结果在 @matches 数组中。
当调用这个动作的时候,值已经存储在 $state->{VARIABLE} 散列中。使用第一个键可以覆盖它,然后循环通过中间键,以插入更多的子层(注意,我们将最后的子层留到最终)。
最终,当 @matches 数组中仅存两个条目的时候,将它们作为键值对插入当前的子层中。
在结尾使用 pop() 而不是 shift() ,是因为计算的顺序。我还在两个独立的行中调用了 shift() 两次,并将结果存储在临时变量中,但是我想这是更清晰、更高级、更难维护的解决方案。
结束语
AppConfig 是在简单情况下用 Perl 配置应用程序的一种有用工具,但是有些时候,在命令行处理和配置文件解析方面需要更多功能。不是使用 XML 或 YAML 这类数据格式,您可以通过稍许额外的努力,使用 AppConfig 来达到更加高级的目标。这对于您需要处理复杂的命令行开关以创建多层散列时尤为有用。
一定要抵抗 Perl 可以使您的配置文件简单化的诱惑。这是一个危险而且脆弱的解决方案 ── 一个用户错误或者是不安全的许可都可能危急您的软件的安全。做一些额外的工作并验证您的输入,总比运行未知的和不能信赖的代码要好一些。
如果您发现 AppConfig 有帮助,您还应该去 CPAN 中看一下 AppConfig::Std 模块;它为 AppConfig 扩展了一些常用和有益的选项。
(http://www.fanqiang.com)