m4是一种宏处理器,它扫描用户输入的文本并将其输出,期间如果遇到宏就将其展开后输出。宏有两种,一种是内建的,另一种是用户定义的,他们能接受任意数量的参数。除了做展开宏的工作之外,m4内建的宏能够加载文件,执行Shell命令,做整数运算,操纵文本,形成递归等等。m4可用做编辑器的前端,或者单纯作为宏处理器来用。
所有的 Unix 系统都会提供 m4 宏处理器,因为它是 POSIX 标准的一部分。通常只有很少一部分人知道它的存在,这些发现了 m4 的人往往会在某个方面成为专家。这不是我说的,这是 m4 手册说的。
有些人对 m4 非常着迷,他们先是用 m4 解决一些简单的问题,然后解决了一个比一个更大的问题,直至掌握如何编写一个复杂的 m4 宏集。若痴迷于此,往往会对一些简单的问题写出复杂的 m4 脚本,然后耗费很多时间去调试,反而不如直接手动解决问题更有效。所以,对于程序猿中的强迫症患者,要对 m4 有所警惕,它可能会危及你的健康。这也不是我说的,是 m4 手册说的。
上文提到『m4 是一种宏处理器,它扫描用户输入的文本并将其输出,期间如果遇到宏就将其展开后输出』,其实更正式的说,应该是:m4 从文本输入流中获取文本并将其发送到文本输出流,期间如果遇到宏就将其展开后发送到文本输出流。
在 Brian Kernighan 与 Dennis Ritchie 合著的《C Programming Language》中将 流 (Stream)定义为『 与磁盘或其它外围设备关联的数据的源或目的地 』。基于这个定义,m4 的 输入流 就是 与磁盘或其它外围设备关联的数据的源 ,其 输出流 就是 与磁盘或其它外围设备关联的数据的源或目的地 ,只不过 m4 希望它的输入流与输出流的内容是文本。如果你不那么较真,可以将 流 理解为文件,对于 m4 而言,就是文本文件,但是下文会坚持使用流的概念。
m4 使用流的概念并非巧合,如果说巧合,也只是因为它的作者恰好也是 Brian Kernighan 与 Dennis Ritchie。
m4 是如何从输入流中获取文本并将其发送到输出流的?肯定不是简单的读取文本就了事,因为 m4 有一个任务是『遇到宏就将其展开』。这意味着 m4 在从输入流中读取文本的过程中至少需要检测所读取的某段文本是不是宏。也就是说,从 m4 的角度来看,它首先要将输入流所提供的文本分为两类: 宏 与 非宏 。如果 m4 读取的是一段文本是 非宏 ,它基本上会将它们直接发送到输出流。之所以说是『基本上』,是因为非宏的文本会被进一步分类处理,其中细节后文会讲。如果 m4 读取的是一段文本是 宏 ,m4 就会将它展开,然后将展开结果发送到输出流。
m4 的工作过程具有一定程度的即时性,它不需要将输入流中全部信息都读取出来,然后再进行处理,而是扮演了一种过滤器的角色。从用户的角度来看,文本流入 m4,然后又流出。
从图灵的角度来看 m4,输入流与输出流可以衔接起来构成一条无限延伸的纸带,m4 是这条纸带的读写头,所以 m4 是一种图灵机。事实上,m4 的确是一种图灵机。因此 m4 的计算能力与任何一种编程语言等同,区别只体现在编程效率以及所编写的程序的运行效率方面。感觉基于 m4 来讲解计算机原理还是挺不错的。
m4既然是图灵机,他至少需要有一个状态寄存器,否则他无法判断当前从输如流中读取的文本是宏还是非宏。为了提高文本处理效率,还应该有一个缓存空间,是的m4在这一空间中高效工作。现在的cpu,没有缓存的很是罕见。
m4 缓存的容量为 512KB。当它满了的时候,m4 会将自动将其中的内容妥善的保存到一份临时文件中备用。所以,只要你的磁盘或其它外围设备的容量足够,就不要担心 m4 无法处理大文件。
注意,m4 缓存,这个概念是我瞎杜撰的。GNU m4 官方文档没这个概念,官方的概念是 转移(diversion)。
类似 CPU 的多级缓存,m4 的缓存空间也是划分了级别的。符合 POSIX 标准的 m4,可将缓存空间划分为 10 种级别,编号依次为 0, 1, 2, …, 9 。GNU m4 对缓存空间的级别数量不作限制。
m4 默认在 0 号缓存中工作,它在这个缓存对文本进行处理,然后将其发送到输出流。使用 m4 内建的宏 divert ,可以从当前缓存切换到其他缓存。例如:
divert(3)
就从当前的缓存切换到 3 号缓存了,然后 m4 就在 3 号缓存中对输入流中的文本进行处理。如果不继续使用 divert 进行缓存切换,m4 会一直在 3 号缓存中工作,直到输入流终结。最后,m4 会将各个缓存中的文本汇总到 0 号缓存中。
缓存的汇总过程是按照缓存级别进行的,m4会根据缓存级别的编号的曾序进行汇总。例如,他总是先将1号缓存的内容汇总到0号缓存中,然后将3号缓存的内容汇总到0号缓存中,以此类推,最后将0号缓存中的内容依续发送到输出流中。
划分了级别的缓存,像是一道一道分水岭,使得文本流像河流一样拥有支流,不同的支流最终又汇集到一起,奔流到海……是不是有些气势恢宏的感觉,然而你也应该考虑到这样的现实:百川东到海,何时复西归?也就是说,文本流经 m4 的过程也像河流入海一样的不可逆。这是宏最大的弱点。在程序中滥用宏,形同过度开采水资源。
这时,你应该有一个问题。如果你真的想学习 m4,那就必须要有这个问题——m4 为什么要对缓存划分级别?回顾一下上文,各个缓存的汇总过程是遵循特定次序的。有了这种分级的缓存汇总机制,你就有能力借助缓存来控制文本的支流,决定哪条支流先汇入 0 号缓存。你可以说这样你有机会扮演大禹,但是我觉得这更像铁路调度员所做的事。对于铁路调度员而言,文本流是他要调度的一组列车。
m4也提供了暗黑缓存,他的编号是-1。GNU m4缓存对黑暗缓存也不限制数量,只要他们的编号是负数就可以。
暗黑缓存,似乎有点恐怖,实际上你可以将它们理解为地下河。也就是流过暗黑缓存的文本,m4 会将它们汇总到 0 号缓存,汇总过程按照暗黑缓存编号的递减次序进行的,但是 m4 不会将暗黑缓存汇总的内容发送到输出流。这没什么不好理解的,现实中没有什么东西是 负数 的。
在 m4 的应用中,暗黑缓存的主要作用就是作为宏定义的空间。如果在 0 号缓存定义一个宏,例如:
divert(0)
define(say_hello_world, Hello World!)
定义了一个名为 say_hello_world 的 m4 宏。宏定义语句『展开』为一个长度为 0 的字符串,然后发送到输出流。长度为 0 的字符串,就是空文本,即使它被发送到输出流,对输出流不会产生任何影响,但是 say_hello_world 宏之前,也就是 divert(0) 之后存在一个 换行符 ,m4 会将这个换行符发送到输出流。除非你原本就希望输出流中需要这个换行符,否则你就在输出流中引入了一个额外的换行符,通常情况下,它不是你想要的结果。为了更好的说明这一点,可以看下面的示例:
divert(0)
define(say_hello_world, Hello World!)
say_hello_world
这个示例就是在上述代码中又增加了一行文本,它表示调用了上一行所定义的 say_hello_world 宏。假设示例代码保存在 hello.m4 文件中,然后执行以下命令:
$ m4 hello.m4
此时,hello.m4 就是 m4 的输入流。m4 从输入流中读取文本,处理文本,然后将处理结果发送到输出流。此时,输出流是系统的标准输出设备(stdout),也就是当前的终端屏幕。
执行上述命令后,我们期望的结果通常是:
$ m4 hello.m4
say_hello_world
然而,m4 输出的却是:
$ m4 hello.m4
Hello World!
Hello World! 前面出现了两处空行,一处是 divert 语句后面的换行符导致的,另处是 say_hello_world 宏定义语句后面的换行符导致的。
如果将 say_hello_world 宏定义语句放在暗黑缓存中,可以解决一半问题。例如:
divert(-1)
define(say_hello_world, Hello World!)
divert(0)
say_hello_world
再次执行 m4 命令,可得:
$ m4 hello.m4
Hello World!
现在 Hello World! 前面只有 1 处空行了,它是 divert(0) 后面的换行符导致的。要消除它,有两种方法。第一种方法就是 divert(0) 后面不换行,例如:
divert(-1)
define(say_hello_world, Hello World!)
divert(0)say_hello_world
另一种方法是使用 m4 内建的 dnl 宏,它会从将它被调用的位置到后面的第一个换行符之间的文本(包括换行符本身)一并删除,例如:
divert(-1)
define(say_hello_world, Hello World!)
divert(0)dnl
say_hello_world
这两种方法输出的结果是相同的。为了让文本具有更好的可读性,通常用 dnl 来做这样的事。