上一篇见:http://segmentfault.com/a/119...
宏
自定义一个 m4 宏所用的基本格式如下:
define(宏名, 宏体)
上一节,我们定义的一个很简单的 say_hello_world
宏:
define(say_hello_world, Hello World!)
say_hello_world
是宏名,Hello World
是宏体。如果在宏定义之后的文本中出现了 say_hello_world
,例如:
define(say_hello_world, Hello World!)
blab blab ... say_hello_world
假设上述文本均处于非负号缓存,那么当 m4 从输入流中读取到 say_hello_world
时,它能够检测出该文本片段是一个被定义了的宏,于是它就将这个宏展开为 Hello World
,并使用这个展开结果替换文本片段 say_hello_world
,所以,上述文本经过 m4 处理后发送到输出流,就变成:
blab blab ... Hello World!
上述输出结果中的空行,应该没什么玄机可言了,只是需要注意:宏定义语句本身也会被 m4 展开,因为 define
本身就是一个宏,只不过它的展开结果是一个空的字符串。
有参数的宏
宏可以有参数。遵循 POSIX 标准的 m4,允许一个宏最多有 9 个参数(似乎 Shell 脚本里的函数也最多支持 9 个参数),在宏体中可使用用 $1, ..., $9
来引用它们。GNU 的 m4 不限制宏的参数数量。
对于下面这段 C 语言的宏定义与调用:
#define DEF_PAIR_OF(dtype) \
typedef struct pair_of_##dtype { \
dtype first; \
dtype second; \
} pair_of_##dtype##_t
DEF_PAIR_OF(int);
DEF_PAIR_OF(double);
DEF_PAIR_OF(MyStruct);
用 m4 的有参数的宏可给出等价表示:
divert(-1)
define(DEF_PAIR_OF,
`typedef struct pair_of_$1 {
$1 first;
$1 second;
} pair_of_$1')
divert(0)dnl
DEF_PAIR_OF(int);
DEF_PAIR_OF(double);
DEF_PAIR_OF(MyStruct);
它们能够展开为同样的 C 代码(C 语言宏由 C 预处理器展开,m4 宏由 m4 展开):
typedef struct pair_of_int {
int first;
int second;
} pair_of_int;
typedef struct pair_of_double {
double first;
double second;
} pair_of_double;
typedef struct pair_of_MyStruct {
MyStruct first;
MyStruct second;
} pair_of_MyStruct;
注意,C 宏与 m4 宏的调用有点区别。在 C 中,调用一个宏,宏名与其后的 (
可以有空格,而 m4 宏的调用不允许这样。
m4 版本的 DEF_PAIR_OF
宏的宏体为:
`typedef struct pair_of_$1 {
$1 first;
$1 second;
} pair_of_$1'
这个宏体是一个带引号的字符串,形如:
`... ... ...'
注意左引号与右引号对应的符号。在大部分键盘上,左引号与 ~
符号同键,右引号与 "
同键,它们是单引号。不要对引号掉以倾心,它在 m4 中的重要地位仅位列宏之下,如果没有它,宏的世界会异常的混乱。后面,我会在单独给引号的基本用法留出一节专门阐述。在此只需将引号理解为一段文本的封装。
事实上,对于 m4 版本的 DEF_PAIR_OF
宏体,不用引号也不会出问题(可以去掉引号试一下)。但是,在复杂一些的宏体内,可能会出现 ,
符号,如果这样的宏体不用引号囊括起来,那么 ,
会被 m4 误认为宏参数的分隔符。所以,一定要记住:,
会被 m4 捕获为宏参数分隔符,而引号可使之逃逸。
小实践:reStructuredText 插图标记的简化
reStructuredText 是一种轻量级的文本标记语言,与 Markdown 属于同类,一般用于记笔记,然后以网页的形式发布。之所以要用轻量级文本标记,是因为直接手写 HTML 太繁琐了。我在我的机器上搭建的 Nikola 静态网站,默认用的就是 reStructuredText,我用它来整理我的一些笔记。
在使用 reSturcturedText 写文档时,我觉得它的插图标记过于繁琐。我常用的插图标记如下:
.. figure:: 图片文件路径
:align: center
:width: 宽度值
上述标记文本块前后必须要留出空行,否则 reStructuredText 的解析器就会抱怨。align
与 width
前面的缩进也是必须的,否则 reStructuredText 的解析器就会抱怨……
为了简化这个标记,我用 m4 定义了一个宏:
divert(-1)
define(place_figure, `
.. figure:: $1
:align: center
:width: $2
')
divert(0)dnl
然后我就可以愉快的像下面这样在 reStructuredText 文本中插入一幅图片了。
place_figure(`/images/amenta-needles/0001.png', 480)
用这种办法可以简化许多繁琐的文本标记,甚至可以实现 reStructuredText 不具备的功能,例如参考文献的管理。如果你不打算研究如何改造 reStructuredText 解析器来满足自己的需求,在这种前提下,用 m4 简单的 hack 一下,使得 reStructuredText 的易用性显著增强,这就是宏语言最大的用途。
不妨将宏语言视为生活中的方便袋。
宏的陷阱
m4 允许宏的重定义,结果是新的宏定义会覆盖旧的。例如:
define(LEFT, [)dnl
LEFT
define(LEFT, {)dnl
LEFT
如果你按照我说『新的宏定义会覆盖旧的』来判断,可能会认为上述文本流经 m4 会变为:
[
{
然而,事实上 m4 的处理结果是:
[
[
与理解这个诡异的结果是如何产生的,就需要认真的回顾一下 m4 的工作过程。
我将 m4 处理第一个 LEFT
宏定义的过程大致拆解为:
在输入流中,m4 遇到了
define
,在它的记忆里,define
是一个宏;接下来它遇到了一个
(
,它会认为这是define
宏参数列表的左定界符;接下来,它遇到了
LEFT,
,它会认为,
之前的文本是define
的第一个参数;接下来,它遇到了
[)
,他会认为[
是define
的第二个参数,而)
是define
参数列表的右定界符;它现在终于明白了,
define(LEFT, [)
是在调用define
宏;m4 对
define(LEFT, [)
进行展开,具体的展开过程,我们不得而知,因为define
是 m4 内建的宏。我们只知道在define(LEFT, [)
的展开过程中,m4 会为我们定义LEFT
宏,并且define(LEFT, [)
宏展开完成后,m4 会向输出流发送一个空字串。
当 m4 遇到第二个 LEFT
宏定义时,它的过程大致如下:
在输入流中,m4 遇到了
define
,在它的记忆里,define
是一个宏;接下来它遇到了一个
(
,它会认为这是define
宏参数列表的左定界符;接下来,它遇到了
LEFT,
,它会认为,
之前的文本——LEFT
是define
的第一个参数。但是 m4 随即发现LEFT
是一个宏,于是它就将这个宏展开,结果为[
,它认为[
才是真正的define
的第一个参数;接下来,它遇到了
{)
,他会认为{
是define
的第二个参数,而)
是define
参数列表的右定界符;它现在终于明白了,
define([, {)
是在调用define
宏;m4 对
define([, {)
进行展开,具体的展开过程,我们不得而知,因为define
是 m4 内建的宏。我们只知道在define([, {)
的展开过程中,m4 会为我们定义[
宏,并且define([, {)
宏展开完成后,m4 会向输出流发送一个空字串。
m4 处理输入流的过程,非常像人类,急功近利,目光短浅,一叶障目,不见泰山,管中窥豹,略见一斑……现在明白了吧!第二个 LEFT
宏定义,表面上看起来是重定义了 LEFT
宏,实际上定义的是 [
宏。
由于 m4 允许用任何符号作为宏名,所以定义一个 [
宏,这种行为是合法的,只不过 m4 不会真正的将它视为宏。我一直没有提 m4 的宏命名规则,现在是谈谈它的最好的时机,但是没什么好说的,在 m4 眼里,只有像 C 函数名的宏名才是真正的宏,也就是说,m4 的宏名名规则是:只允许使用字母、数字以及下划线构造宏名,并且宏名只能以字母或下划线开头。只有符合宏名规则的宏,m4 才会将它视为真正的宏。不过,不符合宏名规则的宏,也是有办法调用的,以后再讲。
若真的想对已定义的宏的重新定义,需要借助引号。例如:
define(`LEFT', [)dnl
LEFT
define(`LEFT', {)dnl
LEFT
在 m4 语法中,单重引号具有逃逸的作用:当 m4 读到带单重引号的文本片段 S 时,它会将 S 的引号消除,然后继续处理 S 之后的文本。
现在可以这样来理解引号的作用:
m4 将一切没有引号的文本都视为宏。对于已定义的宏,m4 会将其展开;对于未定义的宏,m4 会按其字面将其输出。
加了引号的文本,m4 不再检测它们是不是宏,而是将其作为普通文本按字面输出。
也就是说,加了引号的文本,可以让 m4 不需要判断它是不是宏。
记号
现在,我们继续探究 m4 究竟对于输入流都做了些什么。这件事,已经讨论了 3 次了,虽然每一次都比前一次更深入一些,但是迄今为止,真相依然未能堪破。现在应该到堪破真相的时候了。
m4 对输入流是以记号(Token)为单元进行读取的。一般情况下,m4 会将读取的每个记号直接发送到输出流,但是当 m4 发现某个单词是已定义的宏名时,它会将这个宏展开。在对宏进行展开的过程中,m4 可能会需要读入更多的文本以获取宏的参数。宏展开的结果会被插入到输入流剩余部分的前端,也就是说,宏展开后所得到的文本会被 m4 重新读取,解析为记号,继续处理。
上面这段文字尤为重要。当 m4 不能如你预期的那样展开你定义的宏,都应该重新理解上面这段文字。
什么样的文本对于 m4 而言是一个记号?带引号的字符串、宏名、宏参数列表、空白字符(包括换行符)、数字以及其他符号(包括标点符号),像这些类别的文本,对于 m4 而言都是记号。对于每种记号,m4 都有相应的处理机制。数字与标点符号(西文的),它们本身是记号,同时也是某些记号的边界,除非它们出现于带引号的字符串或者宏的参数列表中。
来看一个例子:
define(`definenum', `define(`num', `99')') num
若这行文本流经 m4,那么 m4 读到的第一个记号是 define
。因为 define
后面尾随的是 (
。由于 (
即是记号,也是某些记号的边界。m4 读取 define
文本之后,就遇到了边界,因此 define
是 m4 遇到的一个记号。
然后,m4 开始对 define
这个记号进行处理,它发现这个记号是一个带参数的宏。所以它暂停对 define
的处理,继续读取并分析 define
之后的文本,看是否能获得 define
宏的参数列表。
接下来, m4 读取的是 (
,这是个记号,而且是宏参数列表的左定界符。这对 m4 而言,已经开始经进入了一段可能是参数列表的文本。它期望接下来能遇到一个 ,
或者 )
,以得到完整的参数列表记号。
但是接下来,m4 读到的是一个左引号。这时,对 m4 而言,已经开始进入了一个可能是带引号的字符串文本,它期望接下来能遇到一些文本或右引号,以得到一个完整的字符串记号。
但是接下来,m4 读到是文本片段 definenum
,再读下去,就读到了右引号。这时, m4 很高兴,它确定自己已经读取了一个带引号的字符串记号,然后它就将包围这个字符串的引号消除,继续读取后面的文本。m4 之所以不在这时将 definenum
发送到输出端,因为它没有忘记自己还有一个使命:为 define
宏搜寻完整的参数列表。
接下来,m4 读到了 ,
——这是宏参数记号的边界。m4 很高兴,它终于得到了 define
宏的第一个参数,即 definenum
。此时,m4 认为刚才读到的 ,
就没什么用了,于是就将 ,
消除了,然后它认为后面也许还会有第二个参数,决定继续前进。
接下来,m4 遇到了一个空格。在宏参数列表中,在 ,
之后的空格是无意义的字符,m4 将这个空格扔掉,继续前进。然后它遇到了左引号……于是就像刚才处理 definenum
一样,m4 可以得到一个带引号的字符串:
`define(`num', `99')'
m4 将这个字符串的引号消除,然后继续前进,结果碰到了 )
。此时,m4 吁了口气,它终于为 define
宏获得了一个完整的参数列表,尽管这个参数列表只含有两个参数。
接下来,m4 对 define
宏进行展开。这个过程,我们无法得知,因为 define
是 m4 内建的宏,但是我们知道在 define
的展开过程中肯定发生了一系列计算,然后 definenum
变成了一个宏,最终
define(`definenum', `define(`num', `99')')
的展开结果是一个空的字符串。由于宏展开的结果会被插入到输入流剩余部分的前端,也就是说,宏展开后所得到的文本会被 m4 重新读取,解析为记号,继续处理,因此 m4 会将
define(`definenum', `define(`num', `99')')
的展开结果视为它下一步继续要读取并处理的文本。当 m4 继续前进时,它就会读到到一个空的字符串。空的字符串,虽然不具备被 m4 发送到输出流的资格,但是它可以作为其他记号的边界记号使用。
接下来,m4 遇到了一个空格字符。空格字符也是个记号,而且是其他记号的边界。m4 将空格记号直接发送到输出流,继续前进。
接下来,m4 一口气读到了输入流的末尾,得到了 num
记号。之所以说 num
是一个记号,是因为 num
的左侧与右侧都有边界,左侧是空格,右侧是输入流终止符。m4 将 num
这个记号视为宏,然后它确定这个宏没有被定义,因此无法对其进行展开,所以只好将它作为字符串发送到输出流。
挑战
对于以下 m4 文本
define(`definenum', define(`num', `99')) definenum num
推测一下 m4 的处理结果,然后执行 m4 命令检验所做的推测是否正确,然后再回顾一次 m4 的工作过程,最后用:
$ m4 -dV your-m4-file
查看一下输出,根据输出信息再回顾一次 m4 的工作过程。