(四)
最后更新于:2022-04-01 06:26:20
## 递归
现在再强调一次,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` 宏的定义。