终结篇
最后更新于:2022-04-01 06:26:23
## 难以驾驭的引号
对于自己定义的宏,建议先在你的大脑中对它进行逐步展开,确信自己完全理解这个展开过程。如果大脑的堆栈不够用,可以用纸和笔记录展开过程。这样可以在很大程度上提高宏定义的正确性。
m4 宏调用的复杂之处在于嵌套的宏调用——在一个宏的展开结果中调用了其他宏。例如,宏 `A` 的展开结果中调用了宏`X`,如果期望 `X` 先于 `A` 被 m4 展开,那么在 `A` 的定义中就不要在 `X` 的外围加引号。如果在期望 `A` 展开后,当 m4 再度读取 `A` 的展开结果的过程中再展开 `X`,那么 `X` 的外围必须要有引号。再复杂一些,如果宏 `X` 的展开结果中又调用了宏 `Y`,并且期望 `Y` 是在 m4 再度读取 `X` 展开结果的过程中被展开,那么 `Y` 的外围也必须要有一重引号,此时因为 `X` 外围已经有了一重引号,那么 `Y` 实际上是处于两重引号的包裹之中。
m4 处理引号的基本规则是:在读取带引号的文本片段 S 时,无论 S 中含有多少重引号,m4 只消除其最外层引号,然后将剩余的文本直接发送到输出流。这个规则很简单,之前已经提到过一次。需要注意的是,如果在宏的参数列表中出现了引号,一定要记住宏的参数列表总是在宏展开之前被处理的。看下面的例子:
~~~
define(`echo', `$1')
define(`test', echo($1))
test(test)
~~~
在 `test` 宏定义过程中,`echo($1)` 先被 m4 展开了,结果为空字串,导致 `test` 宏定义语句中的宏体变成空字串,即:
~~~
define(`test', `')
~~~
接下来,`test(test)` 是嵌套的宏调用,括号内的 `test` 会先被展开,展开结果是空字串,导致括号外面的 `test` 被展开之前的形式变为:
~~~
test()
~~~
此时,`test` 宏接受了一个参数——空字串,然后它会被 m4 展开,展开结果为空字串。这个结果并非是因为 `test` 宏接受了空字串参数所导致的。
现在改动一下 `test` 的定义:
~~~
define(`echo', `$1')
define(`test',`echo($1)')
test(test)
~~~
由于引号的抑制作用,`test` 宏体中的 `echo` 不会先于 `test` 定义完成之前被 m4 展开。`test(test)` 的宏展开次序依然同上,m4 先展开括号里面的 `test`,得到:
~~~
test(echo())
~~~
然后,m4 不会去展开括号外层的 `test`,而是先去展开括号里面的 `echo` 宏,因为它认为括号里面的文本是括号外面的`test` 宏的参数,结果变为:
~~~
test()
~~~
接下来,`test()` 会被展开为空字串。
下面改动一下 `test` 宏调用语句:
~~~
define(`echo', `$1')
define(`test',`echo($1)')
test(`test')
~~~
这时,括号里面的 `test` 就不再是宏调用了,而是括号外面的 `test` 宏的一个参数。`test(`test`)` 宏会被展开为:
~~~
echo(test)
~~~
由于 m4 会将宏的展开结果插入到剩余的输入流中继续读取并处理,所以上述结果被进一步处理为:
~~~
echo(echo())
~~~
再进一步处理为:
~~~
echo()
~~~
最终的处理结果依然是一个空字串。
虽然这两次改动并没有得到新的结果,但是显然宏展开的过程并不相同。宏参数中的引号的作用并不是那么显而易见。大部分 m4 宏出错,宏参数中的引号往往是首恶元凶。要驾驭它,只能凭借自己的明确的逐步推导。这也导致了一个问题,很难用 m4 描述复杂的宏逻辑。
作为一次小挑战,请用笔在纸上推导下面 m4 宏的展开结果:
~~~
define(`echo', `$1')
define(`test',`echo($1)')
test(``test'')
~~~
然后使用 `m4 -dV your-m4-file` 印证自己的推导。注意, `m4 -dV` 所显示的宏展开过程,会对每个宏的展开结果包装一层引号,这其实是多余的引号,它只代表 m4 对宏的展开结果总是字符串。
## 非法的宏名
下面这个宏定义:
~~~
define(`?N?', 1)
~~~
m4 会认为 `?N?` 这个宏是不合法的,因为合法的宏的名字必须要遵守正则表达式 `[_a-zA-Z][_a-zA-Z0-9]*`。不过,GNU m4 是仁慈的,对于不合法的宏,它依然能展开,前提是借助 m4 内建的 `defn` 宏:
~~~
?N? # -> ?N?
defn(`?N?') # -> 1
~~~
非法的宏名可以用来模拟数组或 Hash 表,例如:
~~~
define(`_set', `define(`$1[$2]', `$3')')
define(`_get', `defn(`$1[$2]')')
_set(`myarray', 1, `alpha')
_get(`myarray', 1) # -> alpha
_set(`myarray', `alpha', `omega')
_get(`myarray', _get(`myarray',1)) # -> omega
defn(`myarray[alpha]') # -> omega
~~~
## 外援
GNU m4 内建了几个与 Shell 交互的宏,诸如 `syscmd`, `esyscmd`, `sysval`, `mkstemp` 等,其中最有用的是 `esyscmd`,因为它不仅能访问 Shell,而且还能获取 Shell 命令产生的输出。例如,下面这行 m4 代码可以借助 Shell 调用 GNU guile——GNU 的 Scheme 解释器来计算阶乘:
~~~
esyscmd(`guile -c "(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
(display (factorial 100))"')
~~~
如果你的系统中安装了 GNU guile,并且有一个 Shell 可用(既然是 m4 用户,系统中没有 Shell 说不过去的),那么 m4 对上述 `esyscmd` 宏的展开结果为:
~~~
93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
~~~
这样写也行:
~~~
define(`scheme_code',
`"`(define (factorial n)
(if (= n 1)
1
(* n (factorial (- n 1)))))
(display (factorial 100))'"')
esyscmd(`guile -c' scheme_code)
~~~
凡是能在 Shell 中运行并产生输出的程序,皆能被 GNU m4 所用,这是不是很神奇?
## 文本处理
我一直都忍着不去谈 GNU m4 针对文本处理提供的几个内建宏,主要是因为既然有 `esyscmd` 这样的宏可用,那么类 Unix 系统中那些很无敌的文本处理工具,诸如 tr, cut, paste, wc, md5sum, sed, awk, grep/egrep 等等,它们皆能被 m4 所用,那么何必再多此一举?
倘若是为了让 m4 脚本更具备可移植性,那么最好是将一个比较完整的 Shell 环境移植到目标平台……对于主流操作系统而言,这并不是太困难的事,因为已经有了很多针对不同操作系统的完整的 Shell 环境实现。
如果依然坚持用 m4 的方式处理文本,建议阅读:『[GNU m4 Text Handling](http://www.gnu.org/savannah-checkouts/gnu/m4/manual/m4-1.4.17/html_node/Text-handling.html#Text-handling)』。
## 结束语
这份 GNU m4 指南至此终结。作为学习者,务必要记住 m4 官方手册的告诫之语:有些人对 m4 非常着迷,他们先是用 m4 解决一些简单的问题,然后解决了一个比一个更大的问题,直至掌握如何编写一个复杂的 m4 宏集。若痴迷于此,往往会对一些简单的问题写出复杂的 m4 脚本,然后耗费很多时间去调试,反而不如直接手动解决问题更有效。所以,对于程序猿中的强迫症患者,要对 m4 有所警惕,它可能会危及你的健康。
如果不想让 m4 危及你的健康,永远要记住:宏是用来缩写那些复杂但是又经常重复出现的文本模式的。