顶级 cfperl 解析器在每个配置行被赋予 意义
之前对其进行处理。这一特征很重要。顶级解析器获取文本行形式的
数据。它的工作就是从该数据中提取
信息。例如,顶级解析器将除去行中所有注释和所有空行。所有经过处理并因而有意义(对于 cfperl 解释器来说)的数据就被封装用于第二级解析器。当顶级解析器已经除去条件执行、空行和其它(与编辑)无关的信息后,编写第二级解析器(例如,解释编辑命令的解析器)就容易多了。
顶级 cfperl 解析器是解析 cfengine 节、复合类(用或 ["|"] 和与 ["."] 符号隔开的类序列,用于构建布尔语句)和配置操作行(将由第二级解析器解析的任何内容)的场所。在本文中将逐一说明这些项。请记住:cfperl 是正处在开发之中的项目,并且您在 cfperl 网站上所看到的 cfperl 版本可能有别于本文中所使用的静态版本(请参阅 参考资料以获取到该网站和 cfperl.pl 下载的链接)。
要理解以下各节,您必须知道 Parse::RecDescent 模块的基本知识。如果您需要这一非常有用的模块的背景知识, 参考资料一节中有相关的早期文章的链接。在 参考资料中您还会发现到样本 cfengine 配置的链接,该样本配置对于理解 cfperl 的用途和结构而言是不可或缺的。
另一个要注意的地方是,例如,我避免用命名“:”(冒号)的方式来实际命名“#”字符。“#”字符至少有 10 个名称,最常用的是“磅”和“散列”,但是使用任何一种特定名称都必定会把一半的读者搞糊涂。
文法基础知识
顶级(全局)解析器是在变量 $parse_global 中进行定义的。它是个全局对象,与第二级解析器(都在 %parsers 散列中进行引用)截然不同。为何不用 $parsers{GLOBAL} 来代替单独的变量呢?这是因为顶级解析器和所有的第二级解析器都不同且彼此无关,因此我觉得无论以何种方式将它们组合在一起都会使程序结构变得混乱。
由 Parse::RecDescent 进行 适当
解释的 Parse::RecDescent 文法定义构成了该文法。因此, $parse_global 是一个 Parse::RecDescent 对象(解析器),它带有与该文法中定义的规则相对应的方法。
input() 方法及其相应的文法规则至关重要。这是我想从顶级解析器获得的唯一方法,因为它把其它所有规则都合而为一了。为了清晰起见,建议您在文法中只使用一个顶级规则,除非您需要在几个不相交的文法之间共享文法元素定义。如果是那样的话,请务必记录下您的决策,以便偶尔阅读的人能立即明白其中的道理。
清单 1. 完整的顶级文法
my $parse_global = new Parse::RecDescent (q{
# note that the section has to come after the class
# in the parsing order (it matches a class)
input: blank | comment | class_with_line | class | section | line_action
comment: /^\s*/ '#' { 1; }
blank: /^\s*$/ { 1; }
section: /\w+/ ':' { $::current_section = $item[1]; $::current_classes = 'any'; 1; }
class: compound_class '::' { $::current_classes = $item{compound_class}; 1; }
class_with_line: compound_class '::' nonempty_line { $::current_classes =
$item{compound_class};
{
section => $::current_section,
classes => $::current_classes,
line => $item{nonempty_line}
};
}
line_action: line {
{
section => $::current_section,
classes => $::current_classes,
line => $item{line}
};
}
nonempty_line: /\S.*/
line: /.*/
compound_class: /[-!.|\w]+/
});
顶级解析器的简单元素
顶级解析器的基本元素是:行、非空行、注释和空行。
行本身是个简单元素;它可以是“任何您所喜欢”的像正则表达式 /.*/ 之类的东西。那么它为何有用呢?行是具有较复杂规则的元素。只有在得知输入并非空行或注释时才使用它,因此实际上行是有价值的信息。
复合类是有效类名的任意序列。根据 cfengine 规则,字符集是混杂了任意数目布尔逻辑谓词 /!.|/ 的 /[-\w]+/ 。复合类的实际意义(“true”或“false”取决于其中的各个类和它们的逻辑组合方式)将在执行 cfperl 命令时决定。
非空行是以非空格字符 开头
的任意行。它可以包含其后的所有内容,但只有存在非空格字符才使之成为非空行。
注释行是任何以“#”字符(前面可以有也可以没有空格)开头的行。“#”字符后面的内容被认为是注释。请注意,顶级文法此时不会除去行尾的注释。并非不可以这么做,但是这会使文法变得非常复杂,因此我将它留到开发过程的后期才进行处理。乍一看,似乎很容易理解“#”字符属于字符串的情况,但事实上这并非易事。
空行是从头到尾只有空格字符的所有行。
节和类
在定义了基本元素之后,文法定义了节和类规则。节是后面跟着“:”(冒号)的一些单词字符(我们没有静态地列出节的名称,以便于在 cfperl 运行时能方便地添加新的节)。冒号后面的任何内容都将丢弃。节的关键操作是将全局节变量 $current_section 设置成节名并将全局的当前类变量 $current_classes 复位为“any”。请注意,在“input”规则中,节跟在复合类后面。那是因为节和复合类相匹配,它的键标记是“::”序列(两个冒号)。
可以用两种方式检测类:通过类自己,在这种情况下,它们是后面跟着“::”(两个冒号)的复合类名,或者在它们后面跟着即将执行的行。无论哪一种情况,全局变量 $current_classes 都被设置成复合类名。
顶级解析器的用途是产生“cfrun 原子”。这些原子是在 cfrun() 函数中进行解释的。cfrun 原子被生成为匿名的散列(为将来的“节”、“类”和“行”之外的元素留出空间)。匿名散列从文法其它部分的全局变量集获取当前的节和复合类。因此,文法的行为象在状态之间移动的有限状态机,以产生 cfrun 原子。
最后是 input()
实际的 input() 规则出奇的精巧,目的是结束“世界饥荒”并开创国际合作的新纪元。当然这并不真是这么回事,但是它的确很有用! input() 规则用特定的顺序列出了备用的输入可能性。例如,可以将类解释成节,那样类就必须出现在节之前。如果带有行的类没有出现在类之前,则可以将它解释成类(而行也被删除)。因此,顺序很重要,您应当理解它被指定为那种方式的原因。
这看起来可能很明了,但是 input() 规则实际上是文法中最难正确编写的部分,因为它要求正确排列所有备用输入可能性的顺序,而且要求清楚程序所能获取的所有可能输入。事实上, input() 规则是顶级文法中的顶级入口点,这使得它成为 cfperl 程序本身的关键之一。使用 参考资料一节中的样本配置,试用一下 cfperl 程序的本地副本,尝试更改 input() 顺序并查看它如何改变 cfperl 的行为。
复合类
cfengine(cfperl 实现其规则)中的复合类是用逻辑运算符“.”、“|”和“!”(点,竖线或惊叹号)隔开的简单类名。例如,简单类有“solaris”或“linux”(在 Solaris 或 Linux 机器上时分别被 cfperl 定义成 true)或“Hr_00”(在凌晨 00:00 到 00:59 之间被 cfperl 定义成 true)。复合类合并了简单类;例如,如果 solaris 和 Hr_00 都是 true,那么“solaris.Hr_00”将会是 true,而“solaris|linux”在 Solaris 和 Linux 机器上都会是 true。
惊叹号“!”对其后的简单类进行“非”运算,因此“!linux”只有在非 Linux 机器上才为 true。此外,圆括号可以组合逻辑表达式,而“||”是“|”的别名。正如您所看到的那样,复合类相当复杂,几乎不可能在不用 Parse::RecDescent 的情况下解释它们。
还要牢记一点,根据命令的结果,可以在 cfperl 执行期间定义或不定义类。因此,只在 cfperl 执行开始的时候解释复合类一次是行不通的。必须实时进行解释。
复合类解析器
复合类解析器十分复杂,因为它实现完整的逻辑语言解释器并返回逻辑表达式的 求值树
。求值树是在 cfperl 的 allowed_cfrun_atom() 函数中通过 eval_tree() 函数进行解释和求值的。
解析器将 Parse::RecDescent 自动操作(不需要特定操作就可将自动操作附加到所有项)定义成子例程数组和该项自身的数组。这是个恒等函数,目前尚未被使用。
清单 2. 完整的复合类文法
{
local $::RD_AUTOACTION = q{ [ sub{ $_[1]; }, $item[1] ] };
$parsers{CLASS_HIERARCHY} = new Parse::RecDescent(q{
input : disj
disj : conj /\|+/ disj { [ sub{ $_[1] || $_[2] }, $item{conj}, $item{disj} ] }
| conj
conj : unary /\.+/ conj { [ sub{ $_[1] && $_[2] }, $item{unary}, $item{conj ] }
| unary
unary : inversion
| '!(' input ')' { [ sub{ ! $_[1] }, $item{input} ] }
| '(' input ')' { [ sub{ $_[1] }, $item{input} ] }
| atom
inversion : '!'/\w+/ { [ sub{
my $classes = shift;
my $atom = shift;
# print "not_atom($atom) ";
return !exists $classes->{$atom};
}, $item[2] ] }
atom : /\w+/ { [ sub{
my $classes = shift;
my $atom = shift;
# print "atom($atom) ";
return exists $classes->{$atom};
}, $item[1] ] }
});
}
复合类原子
复合类文法中有两个基本级别原子:一个是简单类名的原子( atom ),另一个是反转的简单类名的原子( inversion )。每个原子都返回一个函数,该函数将参数作为对已定义类的散列和原子名本身的引用。 atom 函数检查原子是否存在于类散列中(因此,估计在 Solaris 机器上“solaris”的求值结果为 1),而 inversion 函数则执行相反的操作。
请注意, inversion 只指定简单类名的反转。复合类的反转是在“unary”规则中完成的。
复合类逻辑
复合类解析器最复杂的部分在于逻辑合取和析取的解释。析取是逻辑“或(OR)”,而合取是逻辑“与(AND)”。这听起来可能会很奇怪,但是数学家很容易明白。取两个圆,并让它们彼此有一些交迭;交迭的区域就是合取,而整个区域就是析取。
合取和析取必须在递归文法中以特定的顺序和特定方式进行指定。说明这种文法结构的确切原因不在本文的讨论范畴之内。设法去理解规则序列,并查阅众多有关计算机解析和文法书籍中的某一本(一些有关该主题的优秀书籍表面上看起来似乎在讨论编译器主题)。
每个合取或析取都返回数组引用,它带有 执行
指定的逻辑操作的子例程和该操作的操作数(那些操作数可以是逻辑表达式本身)。这都是在 allowed_cfrun_atom() 函数中进行解释的。
文法是递归的,也就是说, input() 导向 disj() ,而后者又可以导向 conj() ,接着导向 unary() ,然后返回 input() 。
糊涂了吗?很遗憾,的确很乱。诸如 Parse::RecDescent 这样的递归文法有时对于初级程序员来说确实很难,并且不易弄明白,但是递归文法的众多好处远胜于初学时的困难和疑惑。我再次向您建议:如果您有兴趣了解有关计算机解析和文法的更多信息,请参考有关该主题的大量著作;在 参考资料一节中有到相应的 Google 类别的链接。