 | 级别: 初级 Teodor Zlatanov (tzz@iglou.com), 程序员, Gold Software Systems
2000 年 10 月 09 日 Perl 专家 Ted Zlatanov 从控制流和配置处理开始,继续研究其 cfperl 项目(用 Perl 编写的 cfengine 解释器)的自顶向下开发。
本章出自 Ted 著的书籍
At the Helm,该书已经在 Web 上的
developerWorks专栏
更佳编程之路中连载发表。
请阅读 Ted 的
以前发表的章节。
在前一篇专栏文章
更佳编程之路:第 6 章 开发 cfperl,从头开始
中,我们讨论了着手进行新项目的一些预备步骤,包括研究、选择许可证和决定基本体系结构。
完成了这些步骤之后,我准备开始编码!
编写 cfperl 解释器的前几个步骤是编写主循环,确定控制流以及采用与 cfengine 配置样式(尽可能)相似的配置样式。
因为 cfperl 是一个小项目,所以自顶向下方法是可行的。我将主循环放在首位,
接着按顺序开发解释器周期的每个部分。此外,为了我和他人的利益,我在脚本的结束部分以 POD 形式对主循环编写了文档。
控制流由我选择的解析模块确定。使用
Parse::RecDescent 允许我进行 2 级解析:首先使用通用解析器,
然后(通过使用以节为键的文法)使用适合于具体情况的文法或使用缺省文法。
主循环
主 cfperl 循环如下所示:
- 模块
- 常量
- 初始化、全局变量、文法
- cfrun 循环
实际上,在模块之前,要做两件事情。第一件事是导言(程序标识、GPL 许可证)。
然后,声明
$VERSION 变量,并将它初始化成与 CVS 修订本相同。请注意
$Revision: 1.9 $和
$Id: c10.xml,v 1.9 2002/06/09 23:27:55 lifelogs Exp $
的使用,它们是在检入时由 CVS 自动填充的。这里引入了
$VERSION,
因为(从概念上讲)它是元变量:不是由 cfperl 本身直接使用,但反映了有关 cfperl 的状态信息。
清单 1. cfperl 导言
#!/usr/bin/perl -w
# cfperl.pl: cfengine parser in Perl
# by tzz@iglou.com $Id: c10.xml,v 1.9 2002/06/09 23:27:55 lifelogs Exp $
# ... license omitted ...
my $VERSION = sprintf('%d.%02d', (q$Revision: 1.9 $ =~ /\d+/g));
|
接下来是模块节。我使用由 Emacs 提供的可折叠方式;
# {{{和
# }}} 字符串
分别指示节的开始和结束。
清单 2. cfperl 模块
# {{{ modules
use Data::Dumper;
use English;
use strict;
use POSIX;
use Parse::RecDescent;
use Carp;
use File::Basename;
use AppConfig qw/:expand :argcount/;
use IO::File;
use Sys::Hostname;
# }}}
|
接下来是常量节。我用符号名称为每个专门处理的 cfengine 配置节命名,
这些符号名称可以在整个 cfperl 源代码中使用,而不仅限于字符串常量。另外,“any”类的名称在这里都被定义成常量,
并设置缺省全局配置文件。常量使源代码的维护更为方便;因此请始终尽量使用常量而不是变量。
清单 3. cfperl 常量
# {{{ constants
use constant GLOBAL_CONFIG_FILE => '/etc/cfengine/cfengine.conf';
use constant ANY_CLASS => 'any';
use constant GROUPS_SECTION => 'groups';
use constant IMPORT_SECTION => 'import';
use constant CONTROL_SECTION => 'control';
use constant DEFAULT_SECTION => 'default';
use constant CRON_SECTION => 'cron';
# }}}
|
接下来是初始化、全局变量和文法部分。因为我们将在以后的章节中研究文法,所以我在这里的代码样本中省略它们。
自动刷新输出和使
Data::Dumper 模块输出可读是辅助调试的重要设置。
我们将在
配置选项一节中说明 AppConfig 选项。拥有全局
$config 对象将大大有助于管理配置选项。
如果全局变量是标量,则将它们设置成缺省值(避免不明显错误的最佳实践是始终初始化变量 — 但决不要假设它们已
被初始化)。cfrun 队列、次序以及类散列将在以后章节中说明。
因为它们可以在整个 cfperl 中使用,所以将它们定义为全局性的,以更便于访问。
清单 4. cfperl 初始化和全局变量
# {{{ initialization settings
$| = 1; # auto-flush the output
$Data::Dumper::Terse = 1; # produce human-readable Data::Dumper output
$Data::Dumper::Indent = 0; # produce human-readable Data::Dumper output
my $config = AppConfig->new();
$config->define(
# see the section on configuration options for the full AppConfig definitions
);
# }}}
# {{{ globals
my $current_section = 'control';# the current section while parsing
my $current_classes = 'any'; # the current classes while parsing, starts out as 'any'
my @cfrun_queue; # the cfrun queue (cfrun atoms, see add_line)
my %classes; # the list of defined classes
my @cfrun_order; # the defined order of execution
# }}}
|
在进一步初始化已定义的类和处理配置选项(我们将在
配置选项一节中说明配置选项)之后,
根据给予 cfperl 的 cfengine 配置调用
process_line()和
cfrun() 函数。
注:
-exec(或
-e)选项将根据加上
-e 的参数先运行
process_line()和
cfrun()。
清单 5. cfperl 主循环
my $input_line;
while ($input_line = <$config_file>)
{
chomp $input_line;
process_line($input_line);
}
cfrun();
|
控制流
在 cfperl 本身的 POD 节中简要地概述了 cfperl 控制流。
清单 6. cfperl 控制流文档
First, a top-level parser is applied, preprocessing everything into a
cfrun queue (a queue of actions, tagged with a section and some classes).
Second, the 'import', 'control' and 'groups' sections are parsed (first
the imports, then the control and group statements). The actionsequence
(called a cfqueue) is defined.
Third, the remaining statements (everything not processed in the second
step) are processed.
|
处理这些步骤的函数是:
- 顶级解析器:
parse_line()
- 导入:
load_file()
- 控制和分组:
dispatch()
- 所有其它节:
dispatch()
除了顶级解析外,所有这些步骤都在
cfrun() 内部完成。
实际上,
cfrun()是真正解释器,而
parse_line() 是用于挑选出节和导入的准备阶段。
导入必须在解释之前进行,因为其内容会影响解释。
注:控制流的文档在程序本身内部!
它以相当简单的术语说明,有 cfengine 方面经验的人应该会理解。
命令行开关和其它配置选项
应用程序配置有许多 CPAN 模块。我选取了
AppConfig,因为我曾经使用过它,而且因为它可以模拟 cfengine 配置开关的行为。
每个开关都被赋予一个别名。例如,
-define和
-D 的意义相同,
这需要将
-v用作
-debug 的快捷方式。我本来还可以使用大小写来区分它们。
help(
-h)和 input(
-i)选项不带参数。debug(
-v)、fileread(
-f,将其命名为 fileread,因为
AppConfig 有一个内部
file()方法)和 exec(
-e)都是具有一个参数的选项。define(
-D)是唯一列表选项,
所以用户可以写成“
-D Alpha -D Beta”,从而同时定义 Alpha 和 Beta。
我使用
AppConfig args() 方法来读取 @ARGV 中的参数。
清单 7. cfperl 命令行开关定义
my $config = AppConfig->new();
$config->define(
'HELP' => { ARGCOUNT => ARGCOUNT_NONE,
DEFAULT => 0,
ALIAS => 'h'},
'DEBUG' => { ARGCOUNT => ARGCOUNT_ONE,
DEFAULT => 0,
ALIAS => 'v'},
'DEFINE' => { ARGCOUNT => ARGCOUNT_LIST,
ALIAS => 'D'},
'FILEREAD' => { ARGCOUNT => ARGCOUNT_ONE,
DEFAULT => 0,
ALIAS => 'f' },
'EXEC' => { ARGCOUNT => ARGCOUNT_ONE,
DEFAULT => 0,
ALIAS => 'e' },
'INPUT' => { ARGCOUNT => ARGCOUNT_NONE,
DEFAULT => 0,
ALIAS => 'i' },
);
# now read the command-line options from @ARGV
out(0, "main: Invalid options passed, ignoring") unless $config->args();
|
在主循环开始时,事情变得有点儿棘手了。我们必须处理所有的命令行开关。首先,使用
-D 选项定义所有给定的类。
清单 8. cfperl 命令行开关处理:-define 选项
foreach my $class (@{$config->DEFINE})
{
out(1, "Defining class $class on user request");
$classes{$class} = 1;
}
|
现在,处理输入文件。我们将
$config_file 定义为 IO::File
对象(由于许多原因,它处理文件数据要比标准的 Perl FILE 句柄可靠得多)。
如果
-f选项指定了一个
可读文件,那么我们将
$config_file 设置成该文件。
否则,如果给定
-i选项,则将
$config_file 设置成标准的 cfperl 输入。
否则,如果给定
-e 选项,那么我们只要处理该选项的参数,运行快速
cfrun(),然后退出。最后,如果所有情况都失败,我们使用在常量节中定义的
GLOBAL_CONFIG_FILE。
cfperl 用户可以使用的复杂选项模拟了可与 cfengine 一起使用的选项,
但它们不一样,也不该一样。cfperl 不打算替换 cfengine,只是为它添加功能而已。
复制 cfengine 的所有行为和命令行开关是不必要的而且很困难。
清单 9. cfperl 命令行开关处理:文件处理
my $config_file = new IO::File;
if (-r $config->FILEREAD) # we can read the -f argument
{
$config_file->open('< ' . $config->FILEREAD);
}
elsif ($config->INPUT) # -i means read configuration interactively
{
$config_file->fdopen(fileno(STDIN),"r");
}
elsif ($config->EXEC) # just run the one line
{
process_line($config->EXEC) && cfrun();
exit;
}
else # none of the above, use GLOBAL_CONFIG_FILE
{
$config_file->open('< ' . GLOBAL_CONFIG_FILE);
}
exit unless $config_file->opened();
# we continue to a parse_line/cfrun loop done on $config_file
|
下次,我们将完成 cfperl 的代码 — 到时候见。
参考资料
关于作者  | 
|  | Teodor Zlatanov 于 1999 年从美国波士顿大学(Boston University)毕业,获得计算机工程硕士学位。他从 1992 年起就从事程序员的工作,使用了 Perl、Java、C 和 C++。他的兴趣是文本解析、三层客户机-服务器数据库体系结构、UNIX 系统管理、CORBA 和项目管理方面的开放源码工作。可以通过
tzz@iglou.com与 Teodor 联系。
|
对本文的评价
|  |