让这世界再多一份 GNU m4 教程(终结篇)

garfileo

上一篇:http://segmentfault.com/a/119...

难以驾驭的引号

对于自己定义的宏,建议先在你的大脑中对它进行逐步展开,确信自己完全理解这个展开过程。如果大脑的堆栈不够用,可以用纸和笔记录展开过程。这样可以在很大程度上提高宏定义的正确性。

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 展开了,结果为 $1,导致 test 宏定义语句中的宏体变成:

define(`test', $1)

接下来,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() 会被展开为 echo(),后者被展开为空字串。

下面改动一下 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 危及你的健康,永远要记住:宏是用来缩写那些复杂但是又经常重复出现的文本模式的

阅读 10.7k

5.9k 声望
0 粉丝
0 条评论
5.9k 声望
1.9k 粉丝
宣传栏