当前位置: 首页 > 工具软件 > GNU M4 > 使用案例 >

让这世界再多一份 GNU m4 教程 (4)

谭仰岳
2023-12-01

上一篇: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 宏的定义。

下一篇:http://segmentfault.com/a/1190000004137562

 类似资料: