企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
## 注释符 `#` 符号是行注释符。不过,与我们所熟悉的注释文本不同,m4 的注释文本会被发送到输出流。例如: ~~~ define(`VERSION',`A1') VERSION # VERSION `quote' unmatched` ~~~ 会被展开为: ~~~ A1 # VERSION `quote' unmatched` ~~~ 可以用 `changecom` 宏修改 m4 默认的注释符,例如 ~~~ changecom(`@@') ~~~ 这样,`@@` 就变成了注释符。 如果你需要块注释符,也可以做到,例如: ~~~ changecom(/*,*/) ~~~ 如果不向 `changecom` 提供任何参数,其他 m4 实现会恢复默认的注释符,但是 GNU m4 不会恢复默认的注释符,而是关闭m4 的注释功能。如果要恢复默认的注释符,必须这样: ~~~ changecom(`#') ~~~ 如果不希望 m4 回显注释文本,可以用 `dnl` 宏替换注释符,例如: ~~~ define(`VERSION',`A1') VERSION dnl VERSION `quote' unmatched` ~~~ `dnl` 会将其后的内容一直连同行尾的换行符统统干掉。 如果让块注释文本不回显,需要基于条件语句进行一些 hack。不过,由于注释这种东西并没有存在的必要,所以就不再理睬它了。之所以说,注释不重要,是因为我们有更强大的注释机制——[文式编程](http://segmentfault.com/a/1190000004011686)! ## 引号,逃逸以及非 ASCII 字符 m4 有一个不足之处,它没有专用的逃逸符。对于非引号字符的字符,引号总是可以作为逃逸符使用。但是,怎么对引号本身进行逃逸呢?毕竟很多场合需要左引号字符作为普通字符出现。 > 事实上,这篇文档是用 Markdown 标记写的,我也无法将左引号符号以 Markdown 行内代码标记表现出来。 虽然可以在引号的外层再封装一层引号从而将前者变为普通字符,例如: ~~~ I said, ``Quote me.'' # -> I said, `Quote me.' ~~~ 但是,有些时候你只想以普通文本的形式显示左引号,不希望出现一个与之配对的右引号。对于这个问题,可以使用`changequote` 宏修改 m4 默认的引号定界符,例如: ~~~ changequote(<!,!>) a `quoted string ~~~ m4 会将其处理为: ~~~ a `quoted string ~~~ 因为此时,真正的引号是 `<!` 与 `!>`。 如果不向 `changequote` 提供任何参数,就恢复了默认的引号定界符。例如: ~~~ changequote(<!,!>)dnl a `quoted string changequote`'dnl a `quoted string' ~~~ m4 的处理结果为: ~~~ a `quoted string a quoted string ~~~ 一般情况下,应该避免使用 `changequote`,而是将引号字符定义为宏: ~~~ define(`LQ', `changequote(<,>)`dnl' changequote`'') define(`RQ',`changequote(<,>)dnl` 'changequote`'') ~~~ m4 遇到 `LQ` 宏将其展开为「`」字符,遇到 `RQ` 宏就将其展开为「'」字符。这两个宏的定义所体现的技巧是,临时的改变 m4 默认的引号定界符,然后再改回来。 不过,有时候需要全局性的修改 m4 的默认引号定界符,例如有些键盘上没有「`」字符,或者 m4 要处理的文本必须将「`」字符视为普通字符。使用 `changequote` 一定要小心陷阱:GNU m4 提供的 `changequote` 与其早期版本以及 m4 的其他实现有区别。 为了可移植,要么向 `changequote` 提供 2 个参数来调用它,要么就不提供任何参数,例如: ~~~ changequote ~~~ `changequoe` 会改变宏的定义,例如: ~~~ define(x,``xyz'') x # -> xyz changequote({,}) x # -> `xyz' ~~~ 不要用同样的字符作为引号的定界符,这样做,就无法进行引号的嵌套了。 > Markdown 用于格式化行内代码的标记用的就是相同的『左引号』与『右引号』……这样的错误,诞生于上个世纪 70 年代的 m4 没有犯。 不要将引号定界符更改为以字母、下划线或数字开头的字符。m4 虽然不反对这样做,但是它不认为这种字符是引号定界符。数字作为引号定界符,虽然可以被 m4 认可,但是当它作为一个记号本身的组成元素时,它就失去了引号定界符的身份了。 现在的 GNU m4 可以支持非 ASCII 字符,因此也可以用它们来作为引号定界符,例如: ~~~ changequote(左引号, 右引号) a 左引号quoted string右引号 # -> a quoted string define(我是宏, 我知道你是宏) 我是宏 ~~~ 但是最好不要这么干,特别是不要将它们用于宏名。因为,使用 8 位宽的字符,就已经让 m4 行为有些怪异了。GNU m4 1.4.17 版本(本文写作过程中所用的 m4 版本)的手册中说:GNU m4 不理解多字节文本,它只是将文本视为以字节为单位的数据,并且支持 8 位宽的字符作为宏名与引号定界符,但 `NUL` 字符(即 `'\0'`)除外。 m4 能处理中文,这是一种巧合。这种巧合应该只发生在 UTF-8 编码的输入流中。因为 UTF-8 的编码机制决定了中文字符的任何一个字节都与 ASCII 码不同。如果是 GB2312,GB18030 这样的字符集,或许就没有这么好的运气了。 ## 条件 m4 提供了两种条件宏,`ifdef` 宏用于判断宏是否定义,`ifelse` 宏是判断表达式的真假。 ~~~ ifdef(`a', b) ~~~ 对于上述条件宏,如果 `a` 是已定义的宏,那么这条语句的展开结果是 `b`。 ~~~ ifdef(`a', b, c) ~~~ 对于上述条件宏,如果 `a` 是未定义的宏,这条语句的展开结果是 `c`。 被测试的宏,它的定义可以是空字串,例如: ~~~ define(`def') `def' is ifdef(`def', , not) defined. # -> def is defined. ~~~ `ifelse(a,b,c,d)` 会比较字符串 `a` 与 `b` 是否相同,如果它们相同,这条语句的展开结果是字符串 `c`,否则展开为字符串`d`。 `ifelse` 可以支持多个分支,例如: ~~~ ifelse(a,b,c,d,e,f,g) ~~~ 它等价于: ~~~ ifelse(a,b,c,ifelse(d,e,f,g)) ~~~ ## 数字 m4 只认识文本,所以在它看来,数字也是文本。不过 m4 提供了内建宏 `eval`,这个宏可以对整型数的运算表达式进行『求值』——求值结果在 m4 看来依然是文本。 例如: ~~~ define(`n', 1)dnl `n' is ifelse(eval(n < 2), 1, less than, eval(n == 2), 1, , greater than) 2 ~~~ `eval(n < 2)` 是对 `n < 2` 这个逻辑表达式进行『求值』,结果是字符串 `1`,因此 `ifelse` 的第一个参数与第二个参数相等,因此 `ifelse` 宏的展开结果是其第三个参数 `less than`,所以展开结果为: ~~~ n is less than 2 ~~~ 我觉得没必要用 m4 来计算,因为它提供的计算功能太孱弱。可以考虑用 GNU bc 来弥补它的不足。在 m4 中,可以通过`esyscmd` 宏访问 Shell,例如: ~~~ 2.1 ifelse(eval(esyscmd(`echo "2.1 > 2.0" | bc')), 1, `greater than', `less than') 2.0 ~~~ 展开结果为: ~~~ 2.1 greater than 2.0 ~~~ 不过,`esyscmd` 是 GNU m4 对 `syscmd` 的扩展,别的 m4 的实现可能没有这个宏。 ## 挑战 (1) 如果用 m4 处理 C 代码文件,将 `#` 符号作为 m4 的行注释符,会有哪些显而易见的好处? (2) 借助 GNU m4 提供的 `esyscmd` 宏,结合 GNU bc,写一个可以计算数字平方根的的宏。