ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
## 难以驾驭的引号 对于自己定义的宏,建议先在你的大脑中对它进行逐步展开,确信自己完全理解这个展开过程。如果大脑的堆栈不够用,可以用纸和笔记录展开过程。这样可以在很大程度上提高宏定义的正确性。 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 危及你的健康,永远要记住:宏是用来缩写那些复杂但是又经常重复出现的文本模式的。