功能丰富的Perl: 用 Perl 进行应用程序配置

来源:岁月联盟 编辑:zhu 时间:2009-03-05

  程序(从低级的列目录程序到 Web 浏览器)的首要需求之一是:它应该是可配置的。事实证明,基于文件的可配置性和命令行选项的组合是针对可配置性需要的长期而又灵活的解决方案。Perl 程序通常采用这种方法,尽管它们往往还包括一个配置文件和命令行解析例程。

  我们将在本文中使用的命令行解析有一点复杂。因此,为了避免进一步的混淆,如果您正在进行的解析等级高于简单参数,我建议您使用 Parse::RecDescent(或等价的解析模块)。有关复杂命令行解析的讨论,请参阅我关于说英语的 Perl 程序的 前一篇文章。

  在开始之前,请确保您已经在系统上安装了 Perl 5.005(或更新版本)和 CPAN AppConfig 模块。您还需要 Persistent::MySQL 或适用于您的特定数据库的 Persistent 类。这些都可以在 CPAN 中获取(请参阅本文后面的 参考资料)。

  简单的方法:自己动手(DIY)

  理论上(并在有适当工具的情况下!)任何人都可以构建配置解析器,对吗?举例来说, Perl Cookbook展示了一个提供良好开端的快速实现。那么,如果您从此类实现开始的话,编写一个配置文件解析器有多难呢?

  实际上相当困难,因为此类项目涉及如下几个比较复杂的问题:

  配置文件中的空白行和注释

  错误的行(象拼错的关键字)以及哪些内容不可或缺而哪些内容可以忽略的问题

  您必须自己编写解析器的可能性,因为您可能需要多种不同数据结构(例如,布尔型、标量、数组和散列)

  多个配置文件

  变量缺省值

  将命令行选项与文件配置集成并控制它们的交互方式

  用另一种 DIY 配置文件格式培训用户(这通常有些类似于:“只要别一行中只有‘=’号,这就有效。哦,还有注释由‘#’开始,但它们必须独立于其它行。别忘了对关键字使用大写,对值用小写。回来!回来!我还没有告诉您关于强制关键字的事情呢!”)

  重新编写或复制可能有错误的配置代码而不是重用模块

  使配置成为具有一致接口的对象而不是通常的关键字的 DIY 随机散列

  您已经害怕了?这就是我们使用 AppConfig 的原因。它可以处理所有这些问题。很清楚的一点是,您不应该使用 DIY。

  求助于 AppConfig

  尽管 Andy Wardley 编写的 AppConfig CPAN 模块有助于解决上面列出的所有问题,但它不是万能的。它不可能魔术般地改进您的程序。有时需要重新编写以使用 AppConfig。(也存在一点学习曲线,本文试图帮助您降低学习的难度。)

  显然,如果您不确定应该使用 DIY 还是 AppConfig,那么应该根据您的经验和正在编写的内容作出决定。但我相信,AppConfig 不能做得象 DIY 一样好或更好这种情况是非常少的。

  关于 AppConfig 能为您做些什么,以下将逐点进行说明(按照前一节的问题列表):

  配置文件中的空白行和注释:AppConfig 可以识别空白行和注释,并将忽略它们。

  错误的行(象拼错的关键字)中哪些内容是不可或缺的,哪些内容是可以忽略的:可以设置 AppConfig 的灵敏度,以忽略错误设置或异常中止程序。如果可以使用其它的拼写,则关键字可以有别名(譬如在国际化设置中)。

  编写自己的解析器,因为您需要不同的数据结构(布尔型、标量、数组)和散列:AppConfig 处理所有这些数据结构,但它这样做时不用嵌套。如果您需要嵌套的散列或数组,则需要自己动手或帮助一下 AppConfig。

  多个配置文件:AppConfig 将根据需要处理任意数量的配置文件,依次从每个文件中装入设置。您还可以帮助 AppConfig 重新设置数组和散列,以便插入到堆栈底部的值不必出现在堆栈顶部。

  变量缺省值:AppConfig 提供变量缺省值。“-variable”语法将配置文件中的变量重新设置成其缺省状态。

  控制命令行选项并将它们与文件配置集成:AppConfig 提供对 Getopt::Std 和 Getopt::Long 命令行选项解析的支持。解析可以在读取配置文件之前或之后执行。

  用另一种 DIY 配置文件格式培训用户:AppConfig 使用标准、灵活的格式。“KEYWORD 值”和/或“KEYWORD=值”对于标量而言都是可接受的。因为数组是由元素构成的,所以“ARR=1”的后面跟着“ARR=2”将产生一个具有元素 1 和 2 的 ARR 数组。您还可以将布尔选项指定为“bool”、“nobool”、“!bool”、“bool on”、“bool off”、“bool yes”(但是“bool no”会产生错误)、“bool=1”、“bool=0”。(显然,“声名狼籍的”符号逻辑发明者 George Boole 博士 — 请参阅 参考资料 — 会觉得这些选项很亲切。)散列选项被指定为“KEYWORD PARAMETER = 值”,其中的值将成为带有关键字 PARAMETER 的散列项。

  重新编写或复制可能有错误的配置代码,而不是重用模块:AppConfig 对这一点相当稳定,并且到该模块的接口不大可能更改。它还已经通过了数千名其他程序员的测试,所以为什么不使用它呢?

  使配置成为具有一致接口的对象,而不是使用通常的 DIY 随机散列:一致的 API 从主程序中抽取配置处理程序,并用简化与处理程序的连接(在本例中处理程序是 AppConfig)。这种方法引入的错误也更少,因为它只使用方法,而不直接使用数据结构。

  既然我们已经看到了很多选用 AppConfig 的好理由,那么,让我们看一下完整的带注释的 AppConfig 用法示例。目前,我们将省略许多比较高级的特性(将在下一节中讨论)。可以从命令行用“-varname value”设置标量、布尔型和数组变量,用“-varname key=value”设置散列变量。此处用的配置文件是 config.pl,以下是示例:

# blank lines are ignored
    
# set a boolean
debug
    
# set a scalar
name=E.T.
    
# set an array
hosts = dbhost
hosts = backuphost
    
# reset the hosts array
-hosts
    
# add new values to hosts
hosts = firewall
hosts = farewell
    
# set a hash
phone joe = 222-333-4444
phone marge = 555-666-7777

  AppConfig 高级用法

  AppConfig 可以在几个级别上进行变量扩展,这取决于 EXPAND 设置。有关更多详细信息请参阅 AppConfig 文档。

# expand all variables, globally
my $config = AppConfig->new({ GLOBAL => { EXPAND => EXPAND_ALL } });
    
# expand just HOME_DIR as UID, so "~username" will work as in the shell
$config->define('HOME_DIR => { ARGCOUNT => ARGCOUNT_ONE, EXPAND
    
=> EXPAND_UID });

  INI 样式的节是 AppConfig 的另一个特性,您会发现它很有用。通过在配置文件中使用 [节](它本身占一行),您可以列出在文件结束前使用的所有关键字,也可以使用节名加上下划线‘_’列出本节和下一节的所有关键字。例如:

[file]
location = /tmp
type = txt
name = accounts.txt
[database]
host = wyrm
user = slayer
password = amethyst

  等价于:

file_location = /tmp
file_type = txt
file_name = accounts.txt
database_host = wyrm
database_user = slayer
database_password = amethyst

  可以用 varlist() 函数检查 AppConfig 配置对象。下面的代码打印了 AppConfig 对象中每个变量的内容。注:varlist() 可能有点麻烦,因为它必须采用正则表达式(空字符串是绝对无效的)。

use Data::Dumper; # for hash and array references
    
my %varlist = $config->varlist('.*');
foreach my $varname (keys %varlist)
{
print "Variable name $varname, value = ", Dumper $config->get($varname), "n";
}

  AppConfig 中有一个 Getopt::Long 接口,它允许访问 Getopt::Long 模块的所有功能。下面的代码定义了 Getopt::Long 的变量参数,调用该段代码以解析来自命令行的参数。无效值会引起错误。

$config->define("help|h|!"); # define a boolean
$config->define("code|c|=i"); # define a scalar integer
$config->define("list|l|=f@"); # define a array of floating point values only
$config->define("uids|u|=f%"); # define a hash of floating point values only
    
$config->getopt(); # instead of args(), to use the Getopt::Long options

  AppConfig 中还可以进行变量验证。这意味着通过引用正则表达式(或者甚至是一段代码),变量会拒绝将其值设置为某些恶意的值或完全无意义值的尝试。

        # the username validation succeeds only when it is exactly "joe"
# the password validation succeeds when it contains "joe" or "joE"
$config->define(
        'USERNAME' => { ARGCOUNT => ARGCOUNT_ONE,
                VALIDATE => sub # subroutine validation
                {
             my $varname = shift @_;
             my $value = shift @_;
             print "$varname = $valuen";
             return ($value eq "joe");
            }
             },
        'PASSWORD' => { ARGCOUNT => ARGCOUNT_ONE,
        VALIDATE => "jo[Ee]" # regex validation
           }
        );

  AppConfig 使自动触发操作成为可能,因此每次变量的值更改时,该操作就会执行。注:对 AppConfig 的引用也被传送到子例程,所以单个更改会触发其它变量更改。

        $config->define(
        'USERNAME' => { ARGCOUNT => ARGCOUNT_ONE,
                ACTION => sub # autoaction
                 {
            my $config = shift @_;
            my $varname = shift @_;
            my $value = shift @_;
            print "$varname = $valuen";
           }
             }
        );

  AppConfig 限制

  AppConfig 不处理变量中嵌入的代码。在我看来,配置文件中无论如何都不应该存在代码,并且允许用户执行任意代码是个坏主意。但是,AppConfig 不提供对变量的自动求值,尽管确实可以协调与变量相关联的验证和自动操作子例程来执行这项任务。如果您确实感到对此有强烈的需求,那么挑出有疑问的变量并自己针对它们运行 eval()(以下面所阐述的方法)。不用说,除非您完全希望将对您的程序的这一级别的控制权赋予用户,否则就 不要这样做。

foreach my $varname ('username', 'password')
{
$config->set($varname, eval $config->get($varname));
}

  AppConfig 中 INI 样式的节不是很重要。它们定义代码节,但在使用之前必须预先知道这些节。最好先把节设计好,以便它们创建嵌套在父对象中的新 AppConfig 对象,但这是个次要问题。

  在简单测试中,使用 AppConfig 似乎不影响装入和执行速度。它是个相当小的模块,其大小/速度代价通常可以忽略不计。当然,如果您的程序对时间很敏感,那么您应该针对使用和不使用 AppConfig 的情况分别对它计时,然后自己决定是否值得使用该模块。

  AppConfig 的复杂程度和学习曲线之所以比您原来预期的要低很多,很大程度上是因为出色的 API。这里有使人混淆的地方(尤其对新程序员更是如此),但总的来说,对于任何原先具有 Perl 经验的人,这不是很重要的问题。

  AppConfig 的解析限制在于其可用性。如果您需要命令行选项的高级解析,请参阅有关说英语的 Perl 程序的 前一篇文章。(这一限制与 AppConfig 不能进行上下文敏感的解析有特殊的关系。)

  用 AppConfig 和 Persistent::DBI 将配置上载到数据库

  我建议在阅读本节之前,阅读我关于 用 Persistent 模块保存数据的文章。您还应该对 Perl 引用和 SQL 数据库有一定理解。我特定的代码示例使用了 MySQL 数据库以及相应的 Persistent 模块。如果您正在使用另一种数据库(例如 Postgres 或 Oracle),则应该寻找其它 Persistent 模块。

  在配置可用于数据库环境之前,必须先设计数据库模式。换句话说,在开始编写用来保存和恢复数据的代码 之前,您需要确定希望保存什么数据。这个示例将在不同表中存储布尔值、标量、数组和散列。

  这未必是最佳途径。您也可以将一个表用于所有数据类型,或按用途划分表。我的示例仅是实现持久配置的许多方法之一。它肯定不是仅有的方法。

  我在此处提供的模式可能对于大多数用途都足够了。它确实对于值和键长度有一些限制,但可以在代码中轻松地调整那些限制。但是,创建数组和散列元素标识的方法可能引起问题。在这些情况下没有完美的解决方案,只有解决问题的不同方法。将任意的结构化数据存储到关系数据库总是麻烦的。

 

  我们将使用 AppConfig::State 中的 _argcount() 方法。有关这个方法的更详细信息,请阅读 AppConfig::State 的手册页。简单来说,如果我们知道变量名,该方法就可以告诉我们正在处理什么类型的变量。

  在我的代码示例中,我使用了 MySQL 数据库和相应的 Persistent 模块: persistent-config.pl。

  结束语

  只要做少量工作,就可以使 AppConfig 和 Persistent 类很好地合作。前一节中展示的持久配置脚本可以处理大多数具有短键和变量名的配置,但还有可改进的余地。在按自己的喜好编写完脚本后,就可以在网络的 任何地方启动它,并让它从中央主机载入当前配置。至少,进行改进将教会您关于数据库配置的知识,并有助于您以网络为中心的新方式研究应用程序。

  代码重用通常是模块唯一最大的好处,尤其对 AppConfig 更是这样。对于 DIY(自己动手)方法产生了错误和延迟的情况,AppConfig 提供了有效的单一解决方案,该方案很可能可以满足大多数配置需求。

  “AppConfig 限制”一节所列出的限制非常少。当然,在选用 AppConfig 之前,您应该确定对于您的项目什么才是最适合的。最好牢记如何将此处提供的信息应用于您的特定项目,并研究 AppConfig 手册页。

  现在干什么呢?您应该从 AppConfig 中只采用您所需要的。而不要尝试让它为您做所有的工作。将一半程序放到一个配置文件中看来挺有趣,但如果这样做,不久以后用户就会咆哮着冲进您的办公室。请使您的配置文件有逻辑性而又简单。编写程序接受的配置语法的详细说明,包括 AppConfig 提供的非常周到的命令行选项。