上一篇:http://segmentfault.com/a/1190000004128102
递归
现在再强调一次,m4 会将当前宏的展开结果插入到待读取的输入流的前端。也就是说,m4 会对当前宏的展开结果再次进行扫描,以处理嵌套的宏调用。这个性质决定了可以写出让 m4 累死的递归宏。
例如:
define(`TEST', `TEST')
TEST
当 m4 试图对 TEST
进行展开时,它就会永无休止的去展开 TEST
,而每次展开的结果依然是 TEST
。
既然有递归,那么就可以利用递归来做一些计算,为了让递归能够结束,这就需要 m4 能够支持条件。幸好,我们已经知道 m4 是支持条件的。下面,是一个递归版本的 Fibonacci 宏的实现与应用,它可以产生第 47 个 Fibonacci 数:
divert(-1)
define(`FIB',
`ifelse(`$1', `0',
0,
`ifelse(`$1', `1',
1,
`eval(FIB(eval($1 - 1)) + FIB(eval($1-2)))')')')
divert(0)dnl
FIB(46)
m4 的展开结果应该是 1836311903
。也许你要等很久才会看到这个结果。因为递归的 Fibonacci 数计算过程中包含着大量的重复计算,效率很低。
不过,迭代版本的 Fibonacci 数计算过程也能写得出来:
divert(-1)
define(`FIB_ITER',
`ifelse(`$3', 0,
$2,
`FIB_ITER(eval($1 + $2), $1, eval($3 - 1))')')
define(`FIB', `FIB_ITER(1, 0, $1)')
divert(0)dnl
FIB(46)
迭代计算很快,在我的机器上只需要 0.002 秒就可以得出 1836311903
这个结果。不过,如果想尝试比 46 更大的数,比如 FIB(47)
,结果就会出现负数。这是因为 m4 目前只支持 32 位的有符号整数,它能表示的最大正整数是 2^31 - 1,而 FIB(47)
的结果会大于这个数。
循环
既然有递归,那么就可以用它来模拟循环。例如:
define(`for',
`ifelse($#,
0,
``$0'',
`ifelse(eval($2 <= $3),
1,
`pushdef(`$1',$2)$4`'popdef(`$1')$0(`$1', incr($2), $3, `$4')')')')
这个 for
宏可以像下面这样调用:
for(`i', 1, n, `循环内的计算')
它类似于 C 语言中的 for
循环:
for(int i = 1; i <= n; i++) {
循环内的计算
}
例如,可以用 for
宏将 64 个 -
符号发送到输出流:
for(`i', 1, 64, `-')
这个宏的展开结果为:
----------------------------------------------------------------
如果你用过 reStructuredText 标记语言,一定会知道怎么用 for
宏构建一个协助你构造一个用于快速撰写 reStructuredText 标题标记的宏。
要理解 for
宏的定义,有几个 m4 小知识需要补习一下。请向下看。
宏参数列表的特征值
我们已经知道 $1, $2, ..., $9
对应于宏参数列表中的各个参数(GNU m4 不限定参数的个数,其他 m4 实现最多支持 9 个参数)。如果对 C 或 Bash 有所了解,那么当我说 $0
是宏本身,估计不会觉得很奇怪。因此,在上一节 for
宏定义中,$0
表示引用了宏名 for
。不妨将 $0
改成 for
试一下。
$#
表示宏参数的个数。例如:
define(`count', ``$0': $# args')
count # -> count: 0 args
count() # -> count: 1 args
count(1) # -> count: 1 args
count(1,) # -> count: 2 args
#
是注释符,->
后面的文本是 m4 对注释符号之前的文本处理后发送到输出流的结果。
值得注意的是,即使 ()
内什么也没有,m4 也会认为 count
宏是有一个参数的,它是空字串。
for
的定义中,第一处条件语句为:
ifelse($#,
0,
``$0'',
... ...)
它的作用就是告诉 m4,遇到 for
的调用语句,如果 for
的参数个数为 0,那么 for
的展开结果为带引号的字符串:
`for'
要理解为什么在条件语句中,for
用两重引号包围起来,你需要再认真的复习一次 m4 的宏展开过程。如果用单重引号,那么以无参数的形式调用 for
宏时,m4 会陷入对 for
宏无限次的展开过程中。
宏的作用域
所有的宏都是全局的。
如果我们需要『局部宏』该怎么做?也就是说,如何将一个宏只在另一个宏的定义中使用?局部宏的意义就类似于编程语言中的局部变量,如果没有局部宏,那么在一个全局的空间中,很容易出现宏名冲突,导致宏被意外的重定义了。
为了避免宏名冲突,一种可选的方法是在宏名之前加前缀,比如使用 local
作为局部宏名的前缀。不过,这种方法对于递归宏无效。更好的方法是用栈。
m4 实际上是用一个栈来维护宏的定义的。当前宏的定义位于栈顶。使用 pushdef
可以将一个临时定义的宏压入栈中,在利用完这个临时的宏之后,再用 popdef
将其弹出栈外。例如:
define(`USED',1)
define(`proc',
`pushdef(`USED',10)pushdef(`UNUSED',20)dnl
`'`USED' = USED, `UNUSED' = UNUSED`'dnl
`'popdef(`USED',`UNUSED')')
proc # -> USED = 10, UNUSED = 20
USED # -> 1
如果被压入栈的宏是未定义的宏,那么 pushdef
就相当于 define
。如果 popdef
弹出的宏也是未定义的宏,popdef
就相当于 undefine
,它不会产生任何抱怨。
GNU m4 认为 define(X, Y)
与 popdef(X)pushdef(X, Y)
等价。其他的 m4 实现会认为 define(X)
等价于 undefine(X)define(X, Y)
,也就是说,新的宏的定义会更新整个栈。 undefine(X)
就是取消 X
宏的定义,使之成为未定义的宏。
让宏名更安全
m4 有一个 -P
选项,它可以强制性的在其内建宏名之前冠以 m4_
前缀。例如下面的 M1.m4 文件:
define(`M1',`text1')M1 # -> define(M1,text1)M1
m4_define(`M1',`text1')M1 # -> text1
直接用 m4 处理,结果为:
$ m4 M1.m4
text1 # -> define(M1,text1)M1
m4_define(M1,text1)text1 # -> text1
如果用 m4 -P
来处理,结果为:
$ m4 -P test.m4
define(M1,text1)M1 # -> define(M1,text1)M1
text1 # -> text1
挑战
理解 for
宏的定义。