💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 递归 现在再强调一次,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` 宏的定义。