编写 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 的代码 — 到时候见。