9.4 为crowbar引入鬼车

crowbar名字是由“像Perl一样的语言”而来的。

近些年来,Perl被应用在了各个领域中,但是早期的Perl只是用来处理文本文件的。说起处理文本文件,最方便的莫过于正则表达式了。

但是想要从零开始制作一个正则表达式引擎的话太困难了,因此我们引入现有的正则表达式程序库“鬼车”。

9.4.1 关于“鬼车”

“鬼车”是小迫先生开发的正则表达式程序库。

官方网站(英语): 

http://www.geocities.jp/kosako3/oniguruma/

在写作本书的时候最新版本为5.9.1 [19],UNIX(含Mac操作系统)和Windows都可以安装。许可类型为BSD许可,标明著作权、许可条文和免责条款后,可以在自制软件中(可以不开放源代码)自由使用。

因为使用了这个程序库,所以可以简单地将正则表达式引入到crowbar中。在这里要感谢开发者小迫先生。

具体的安装方法和程序库的使用方法,考虑到很有可能会发生变化,请大家直接参考网站的内容。

http://avnpc.com/pages/devlang#oniguruma

9.4.2 正则表达式常量

在编程语言中处理正则表达式的时候,问题在于要将正则表达式特化到什么程度。

在Perl的语言设计上,专门针对正则表达式进行了优化。无论s///、m///还是s///g亦或=~,这些在我看来都太僵化了。对于在AWK级别中对文本处理进行特化的语言来说,这样也许很好,但是我不想让crowbar变成这样。

反之在Java和PHP中并没有直接支持正则表达式,而是用程序库的方式支持正则表达式。也许有人会想,这么做也还行吧。在这种情况下,如果只使用单纯的字符串来表现正则表达式的话,在字符串常量中可能会含有特殊含义的字符,因此不得不进行编码处理。字符串常量包含的特殊含义的字符,很有可能在正则表达式中也具有特殊的含义。为了使用这样的字符,在正在表达式中还要进行编码,即必须要进行双重编码。因此,例如Java中匹配 \的正则表达式必须要写成 \\。这让人感觉很愚蠢。

不知道是不是为了解决这个问题,在Python中引入了raw string的概念。在Python中,

r"字符串"

这样在字符串前面加上r的话,在这个字符串中像 \这样的字符将不再具有特殊含义。如此一来,匹配 \的正则表达式只要写成 \就可以了。在C#中加上了 @的字符串(逐字字符串:verbatim string literal)能达到同样的效果。

但是,如果\没有特殊含义的话,字符串常量中出现了"的时候该怎么办呢?

Python的使用手册 [16]中写道:

引号可以用反斜杠进行编码,但是这样一来反斜杠本身就成了遗留问题。例如, r"\""是正确的字符串常量,说明由反斜杠和双引号组成了一个字符串,而r"\"则是错误的字符串常量(raw字符串不能以反斜杠和连续奇数个字符串结尾)。严格地说, (因为反斜杠会编码跟在它后面的引号)raw字符串不能以单个反斜杠结束

在我看来,这是一种招致混乱的设计(顺便说一下,C#的逐字字符串采用了两个双引号写在一起代表一个双引号的设计,这种设计与Pascal相似)。

另外,如果想要高效地解释正则表达式,就必须要进行事先编译。但是,对于使用者(crowbar程序员)来说,每次都编译十分麻烦。在大多数的程序中,正则表达式都不是在运行时组合而成的,因此可以在编译源代码的同时编译正则表达式。这样一来,作为一门编程语言来说,它就需要一种表现“正则表达式常量”的格式。

在Ruby中,通过 %!字符串!的方式可以实现跟Python的raw string一样的效果。与Python不同的是, !可以是任意字符串。这种方法中,只要使用字符串常量中没有的字符把字符串包起来即可,也很好地回避了Python中出现的问题。另外,在Ruby中通过 %r!正则表达式!的方式可以表示一个正则表达式常量,使用这种方式定义的常量,可以在编译时先编译正则表达式。但在crowbar中,%会被当做模运算符来使用,也充分考虑到了(运算符左右两边不需要输入空格)像 a%r+3这样的程序。

从而在crowbar中,采用了 %%r"正则表达式"的格式。和Ruby一样, %%r后面可以是任意的字符。也就是说, %%r"hoge"和 %%r!hoge!两种写法达到的效果是相同的。

但是实际上,这样的语法一旦用多了,就会出现各种各样的符号(用来定义正则表达式),看上去就不那么美观了。

9.4.3 正则表达式的相关函数

crowbar中与正则表达式相关的函数如下所示。

reg_match(regexp, subject, region);

对字符串 subject 使用正则表达式 regexp 进行匹配,如果匹配返回 true,不匹配则返回 false。

可以省略 region,也可以传入一个使用 new_object()创建的对象。

在 region 中会返回 string、 begin、 end 三个数组成员。例如正则表达式 "hoge(.)piyo", string[0] 中保存着与表达式全部匹配的字符串, string[1]中保存着与“\1”(也就是正则表达式的 (.)部分)匹配的字符串。 start、 end返回 string[n]开始和结束位置+1(这个设计是照搬鬼车的)。 

reg_replace(regexp, replacement, subject);

在字符串 subject 中,将匹配正则表达式 regexp 的部分替换为字符串 replacement,并返回替换后的字符串。如果有多个位置匹配,则替换第一个匹配的位置。

reg_replace_all(regexp, replacement, subject);

在字符串 subject 中,将匹配正则表达式 regexp 的部分替换为字符串 replacement,并返回替换后的字符串。替换针对所有匹配的位置进行。

包括 reg_replace()在内,在 replacement中可以使用 回溯引用 , replacement 的类型只能是字符串。但我想,给 reg_replace()传递的 \1是不是必须要写成 \1呢?实际上在crowbar中,含有 \n(换行符)和 \t(制表符)的字符串常量需要特殊处理,因为在crowbar中没有将 \025、和\x5c之类的八进制或者十六进制的数值嵌入到字符串中(C语言中有)的功能,所以将\1写为\1也是没有问题的(看到这肯定会有人问我:“那前面那节的讨论还有什么意义呢?”)。

——虽然可能有人会认为以后使用这种设计比较好,但是我认为现在这样也没什么问题。 

reg_split(regexp, subject);

用 regexp分割字符串 subject,返回分割后的字符串数组。