Boost.Spirit能使我们轻松地编写出一个简单脚本的语法解析器,它巧妙利用了元编程并重载了大量的C++操作符使得我们能够在C++里直接使用类似EBNF的语法构造出一个完整的语法解析器(同时也把C++弄得面目全非-_-)。
关于EBNF的内容大家可以到网上或书店里找:
EBNF基本形式<符号> ::= <表达式> 或 <符号> = <表达式>
表达式里常用的操作符有:
- | 分隔符,表示由它分隔的某一个子表达式都可供选择
- * 重复,和正则表达式里的*类似,表示它之前的子表达式可重复多次
- - 排除,不允许出现跟在它后面的那个子表达式
- , 串接,连接左右子表达式
- ; 终止符,一条规则定义结束
- '' 字符串
- "" 字符串
- (...) 分组,就是平时括号的功能啦,改变优先级用的。
- (*...*) 注释
- [...] 可选,综括号内的子表达式允许出现或不出现
- {...} 重复,大括号内的子表达式可以多次出现
- ?...? 特殊字符,由ISO定义的一些特殊字例如:
只允许赋值的简单编程语言可以用 EBNF 定义为:
- (* a simple program in EBNF ? Wikipedia *)
- program = 'PROGRAM' , white space , identifier , white space ,
- 'BEGIN' , white space ,
- { assignment , ";" , white space } ,
- 'END.' ;
- identifier = alphabetic character , [ { alphabetic character | digit } ] ;
- number = [ "-" ] , digit , [ { digit } ] ;
- string = '"' , { all characters ? '"' } , '"' ;
- assignment = identifier , ":=" , ( number | identifier | string ) ;
- alphabetic character = "A"|"B"|"C"|"D"|"E"|"F"|"G"|"H"|"I"|"J"|"K"|"L"|"M"|"N"|"O"|"P"|"Q"|"R"|"S"|"T"|"U"|"V"|"W"|"X"|"Y"|"Z" ;
- digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9" ;
- white space = ? white space characters ? ;
- all characters = ? all visible characters ? ;
一个语法上正确的程序:
- PROGRAM DEMO1
- BEGIN
- A0:=3;
- B:=45;
- H:=-100023;
- C:=A;
- D123:=B34A;
- BABOON:=GIRAFFE;
- TEXT:="Hello world!";
- END.
这个语言可以轻易的扩展上控制流,算术表达式和输入/输出指令。就可以开发出一个小的、可用的编程语言了。
由于C++语法规则的限制,Spirit改变了EBNF中的一部分操作符的使用方式,如:
- 星号重复符(*)由原来的后置改为前置
- 逗号串接符(,)由>>或&&代替
- 中括号可选功能([表达式])改为(!表达式)
- 大括号重复功能({表达式})由重复符(*表达式)替代
- 取消注释功能
- 取消特殊字符功能
- 同时Spirit又提供了大量的预置解析器加强了它的表达能力,因此可以把Spirit的语法看成是一种EBNF的变种。
版本1.6.x之前的spirit能支持大部分的编译器。在1.8.0之后,由于spirit加入了很多C++的新特性,使兼容各种不标准的编译器的工作变得非常郁闷,于是Spirit不再支持不标准的C++编译器,这意味着VC7.1,BCB2006以及GCC3.1之前版本将不再被支持。(注:据说江湖上有新版Spirit的牛人修改版,可以工作在VC6和VC7上,具体情况不明)
入门
头文件:
#include <boost/spirit.hpp>
例一,解析一个浮点数
首先,要弄一个关于浮点数的EBNF规则
假设我们的浮点数形式是: [±]xxxx[.xxxx][Ex],其中正负号可有可无,后面的幂可有可无,允许不带小数点
则对应的EBNF规则是:
digit = "0"|"1"|"2"|"3"|"4"|"5"|"6"|"7"|"8"|"9";
real = ["+"|"-"], digit, [{digit}], [".", digit, [{digit}]], ["E"|"e", ["+"|"-"], digit, {digit}]
那么对应在Spirit里的是什么样的呢?
- !(ch_p('+')|ch_p('-'))>>+digit_p>>! (ch_p('.')>>+digit_p)>>
- !((ch_p('e')|ch_p('E')) >> !(ch_p('+')|ch_p('-'))>>+digit_p)
在Spirit中,用于匹配表达式的对象叫解析器,如这里的ch_p, digit_p以及由它们和操作符组成的整个或部分都可以称为解析器。
- !符号代表其后的表达式是可选的,它代替了EBNF里的中括号功能。
- ch_p()是一个Spirit预置的解析器生成函数,这个解析器用于匹配单个字符。
- >>用于代替逗号顺序连接后面的解析器
- +符号代表1次或多次重复
- digit_p也是一个Spirit预置的解析器,它匹配数字字符。
这样,再看上面就好理解了:可选的+-号,接着是数字,再跟着是可选的小数点和数字,最后是可选的E跟一个可接+-号的数字
现在,把这个式子写到代码里:
- #include <iostream>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- int main()
- {
- parse_info<> r = parse("-12.33E-10",
- !(ch_p('+')|ch_p('-'))>>+digit_p>>
- !(ch_p('.')>>+digit_p)>>
- !((ch_p('e')|ch_p('E')) >>
- !(ch_p('+')|ch_p('-'))>>+digit_p)
- );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- return 0;
- }
这就是Spirit,这个变种的EBNF语法直接就写在C++代码里就可以了,实际上它们是由一系列的简单解析器对象通过重载操作符后组合而成的复杂解析器。
解析器重载的操作符也可以帮我们自动作一些转换工作,如上面的式子中ch_p('+')|ch_p('-')就可以改成ch_p('+')|'-',只要左边或右边的数值其中之一是解析器,它就能自动和另一边的数值组合。
简化后如下:
- !(ch_p('+')|'-')>>+digit_p>>!('.'>>+digit_p)>>!((ch_p('e')|'E') >> !(ch_p('+')|'-')>>+digit_p)
parse函数调用解析器来解析指定的字符串,它的原型是:
- parse_info<charT const*> parse(字符串, 解析器);
- parse_info<charT const*> parse(字符串, 解析器1, 解析器2);
第二个版本中的解析器2指出解析时可以忽略的一些字符,比如语句中的空格之类的。
另外,parse还有迭代器的版本
- parse_info parse(IteratorT first, IteratorT last, 解析器);
- parse_info parse(IteratorT first, IteratorT last, 解析器1, 解析器2);
IteratorT可以是任何迭代器类包括字符串指针,前面的这个两个版本其实只是简单地包装了一下这两个函数。
返回的parse_info类(其中的IteratorT模板默认为char const*)包含了解析结果信息,里面的成员有:
- IteratorT stop; //最后解析的位置
- bool hit; //是否与整个解析器匹配
- bool full; //是否与整个字符串匹配
- std::size_t length; //解析器解析了多少个字符,注意,first+length不一定与stop相同
其实,Spirit已经帮我们准备好了很多解析器,比如上面我们写得要死的浮点数匹配,只要一个real_p就行了(冷静,冷静,上面的一长串到后面还是会用到的)
- parse_info<> r = parse("-12.33E-10",real_p);
Spirit预置的一些原始解析器,它们的名字都是以"xxxx_p"的形式出现。
字符解析器
- ch_p('X') 返回单字符解析器
- range_p('a','z') 返回一个字符范围解析器,本例中匹配'a'..'z'
- str_p("Hello World") 返回一个字符串解析器
- chseq_p("ABCDEFG") 返回一个字符序列解析器,它可以匹配"ABCDEFG","A B C D E F G","AB CD EFG"等
- anychar_p 匹配任何字符(包括'\0')
- alnum_p 匹配A-Z,a-z,0-9
- alpha_p 匹配字母
- blank_p 匹配空格和TAB
- cntrl_p 匹配控制字符
- digit_p 匹配数字字符
- graph_p 匹配可显示字符(除空格,回车,TAB等)
- lower_p 匹配小写字符
- print_p 匹配可打印字符
- punct_p 匹配标点符号
- space_p 匹配空格,回车,换行,TAB
- upper_p 匹配大写字符
- xdigit_p 匹配十六进制数字符串
- eol_p 匹配行尾
- nothing_p 不匹配任何字符,总是返回Fail(不匹配)
- end_p 匹配结尾
字符解析器支持的操作符
- ~a 排除操作,如~ch_p('x')表示排除'x'字符
- a|b 二选一操作,或称为联合,匹配a or b
- a&b 交集,同时匹配a和b
- a-b 差,匹配a但不匹配b
- a^b 异或,匹配a 或 匹配b,但不能两者同时匹配
- a>>b 序列连接,按顺序先匹配a,接下来的字符再匹配b
- a&&b 同上(象C语言一样,有短路效果,若a不匹配,则b不会被执行)
- a||b 连续或,按顺序先匹配a,接下来的字符匹配b(象C语言一样,有短路效果,若a已匹配,则b不会被执行)
- *a 匹配0次或多次
- +a 匹配1次或多次
- !a 可选,匹配0或1次
- a%b 列表,匹配a b a b a b a...,效果与 a >> *(b >> a)相同
整数解析器 Spirit给我们准备了两个整数解析器类,对应于有符号数和无符号数int_parser和uint_parser
它们都是模板类,定义如下:
- template <
- typename T = int,
- int Radix = 10,
- unsigned MinDigits = 1,
- int MaxDigits = -1>
- struct int_parser;
- template <
- typename T = unsigned,
- int Radix = 10,
- unsigned MinDigits = 1,
- int MaxDigits = -1>
- struct uint_parser;
模板参数用法:
- T为数字类型
- Radix为进制形式
- MinDigits为最小长度
- MaxDigits为最大长度,如果是-1表示不限制
比如下面这个例子可以匹配象 1,234,567,890 这种形式的数字
- uint_parser<unsigned, 10, 1, 3> uint3_p; // 1..3 digits
- uint_parser<unsigned, 10, 3, 3> uint3_3_p; // exactly 3 digits
- ts_num_p = (uint3_p >> *(',' >> uint3_3_p)); // our thousand separated number parser
Spirit已预置的几个int_parser/uint_parser的特化版本:
- int_p int_parser<int, 10, 1, -1> const
- bin_p uint_parser<unsigned, 2, 1, -1> const
- oct_p uint_parser<unsigned, 8, 1, -1> const
- uint_p uint_parser<unsigned, 10, 1, -1> const
- hex_p uint_parser<unsigned, 16, 1, -1> const
实数解析器Spirit当然也会给我们准备实数解析器,定义如下:
- template<
- typename T = double,
- typename RealPoliciesT = ureal_parser_policies >
- struct real_parser;
模板参数用法:
- T表示实数类型
- RealRoliciesT是一个策略类,目前不用深究,只要知道它决定了实数解析器的行为就行了。
已预置的实数解析器的特化版本:
- ureal_p real_parser<double, ureal_parser_policies<double=""> > const
- real_p real_parser<double, real_parser_policies<double=""> > const
- strict_ureal_p real_parser<double, strict_ureal_parser_policies<double=""> > const
- strict_real_p real_parser<double, strict_real_parser_policies<double=""> > const
real_p前面实例里已经见过,ureal_p是它的unsigned版本。strict_*则更严格地匹配实数(它不匹配整数)
字符串形式为"real,real,real,...real"
参考上面的一堆预置解析器,我们可以这样组合:
- real_p >> *(',' >> real_p);
- real_p%','
- {
- //用于解析的字符串
- const char *szNumberList = "12.4,1000,-1928,33,30";
- parse_info<> r = parse( szNumberList, real_p % ',' );
- cout << "parsed " << (r.full?"successful":"failed") << endl;
- cout << szNumberList << endl;
- //使用parse_info::stop确定最后解析的位置便于查错
- cout << string(r.stop - szNumberList, ' ') << '^' << endl;
- }
对于real_p,它要求形式为:void func(double v)的 函数或函数对象,下面我们就来取出这些数字:
- #include <iostream>
- #include <boost/spirit.hpp>
- using namespace std;
- using namespace boost::spirit;
- //定义函数作为解析器的Ac