难以驾驭的引号
对于自己定义的宏,建议先在你的大脑中对它进行逐步展开,确信自己完全理解这个展开过程。如果大脑的堆栈不够用,可以用纸和笔记录展开过程。这样可以在很大程度上提高宏定义的正确性。
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』。
结束语
这份 GNU m4 指南至此终结。作为学习者,务必要记住 m4 官方手册的告诫之语:有些人对 m4 非常着迷,他们先是用 m4 解决一些简单的问题,然后解决了一个比一个更大的问题,直至掌握如何编写一个复杂的 m4 宏集。若痴迷于此,往往会对一些简单的问题写出复杂的 m4 脚本,然后耗费很多时间去调试,反而不如直接手动解决问题更有效。所以,对于程序猿中的强迫症患者,要对 m4 有所警惕,它可能会危及你的健康。
如果不想让 m4 危及你的健康,永远要记住:宏是用来缩写那些复杂但是又经常重复出现的文本模式的。