IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope: Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Linux  >

更佳编程之路: 第七章 -- 顶级控制流和配置

开始编码

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

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 循环如下所示:

  1. 模块
  2. 常量
  3. 初始化、全局变量、文法
  4. 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

Teodor Zlatanov 于 1999 年从美国波士顿大学(Boston University)毕业,获得计算机工程硕士学位。他从 1992 年起就从事程序员的工作,使用了 Perl、Java、C 和 C++。他的兴趣是文本解析、三层客户机-服务器数据库体系结构、UNIX 系统管理、CORBA 和项目管理方面的开放源码工作。可以通过 tzz@iglou.com与 Teodor 联系。




对本文的评价










回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款