## 宏
自定义一个 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` 宏定义的过程大致拆解为:
1. 在输入流中,m4 遇到了 `define`,在它的记忆里,`define` 是一个宏;
2. 接下来它遇到了一个 `(`,它会认为这是 `define` 宏参数列表的左定界符;
3. 接下来,它遇到了 `LEFT,`,它会认为 `,` 之前的文本是 `define` 的第一个参数;
4. 接下来,它遇到了 `[)`,他会认为 `[` 是 `define` 的第二个参数,而 `)` 是 `define` 参数列表的右定界符;
5. 它现在终于明白了,`define(LEFT, [)` 是在调用 `define` 宏;
6. m4 对 `define(LEFT, [)` 进行展开,具体的展开过程,我们不得而知,因为 `define` 是 m4 内建的宏。我们只知道在`define(LEFT, [)` 的展开过程中,m4 会为我们定义 `LEFT` 宏,并且 `define(LEFT, [)` 宏展开完成后,m4 会向输出流发送一个空字串。
当 m4 遇到第二个 `LEFT` 宏定义时,它的过程大致如下:
1. 在输入流中,m4 遇到了 `define`,在它的记忆里,`define` 是一个宏;
2. 接下来它遇到了一个 `(`,它会认为这是 `define` 宏参数列表的左定界符;
3. 接下来,它遇到了 `LEFT,`,它会认为 `,` 之前的文本——`LEFT`是 `define` 的第一个参数。但是 m4 随即发现 `LEFT` 是一个宏,于是它就将这个宏展开,结果为 `[`,它认为 `[` 才是真正的 `define` 的第一个参数;
4. 接下来,它遇到了 `[)`,他会认为 `[` 是 `define` 的第二个参数,而 `)` 是 `define` 参数列表的右定界符;
5. 它现在终于明白了,`define([, [)` 是在调用 `define` 宏;
6. 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 的工作过程。