转载

M4 的条件与宏

这篇笔记的主要内容取自 Michael Breen 所写的《 Notes on the M4 Macro Language 》

条件

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) 会比较字符串 ab 是否相同,如果它们相同,这条语句的展开结果是字符串 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 的实现可能没有这个宏。

定义带参数的宏

在遵循 POSIX 标准的 M4 中,一个宏最多可以有 9 个参数(好像 Shell 脚本里的函数也最多支持 9 个参数),可以用 $1, ..., $9 来引用它们。GNU 的 m4 不限制宏的参数数量。

宏的参数默认值是空字串。例如,如果向一个宏传递 2 个参数值,那么它的第 3 个参数值是空字串。如果不向宏传递任何参数,那么它的所有参数值都是空字串。也就是说,不管你向宏传还是不传参数,只要你用 $n 来引用某个参数,如果这个参数值的确存在,就可以取而用之,否则只能拿到空的字串。

在宏的定义中,参数的引用,例如 $1 总是立即被展开的,不管它外围有没有引号。如果不希望参数立即展开,可以用引号来维持,例如:

define(`world', `Hello World')  define(`say_hello', `$1') say_hello(`world')           # -> Hello World!  define(`say_hello', `$`'1') say_hello(`world')           # -> $1

在宏的定义中, $0 可以引用宏名, $# 的展开结果是参数的个数。调用宏时, 空的括号 () 表示一个参数 ,例如:

define(`count', ``$0': $# args') count        # -> count: 0 args count()      # -> count: 1 args count(1)     # -> count: 1 args count(1,)    # -> count: 2 args

$* 会被展开为参数列表。 $@ 也可以展开为参数列表,但是它会用引号来保护每个参数,保证它们不会在参数列表中被展开。例如:

define(`list',`$`'*: $*; $`'@: $@') list(len(`abc'),`len(`abc')')         # -> $*: 3,3; $@: 3,len(`abc')

其中, len 宏是 M4 内建的宏,用于统计字串的长度。

显然,参数列表可以用于处理变长参数,其处理过程类似于 C99。下面定义了一个宏,它可以输出所接受的参数列表中最后一个参数:

define(`echolast', `ifelse(eval($#<2), 1, `$1`'', `echolast(shift($@))')') echolast(one,two,three)    # -> three

其中, shift 是 M4 内建的宏,它的作用是对其所接受参数列表进行类似 Scheme 的 cdr 运算,也就是砍掉列表的首部,将剩下的发送到输出。由于 echolast 会被递归展开,于是它每一次递归展开都会被 shift 砍掉它所接受的参数列表的首部。当参数列表的长度为 1 时, eval($# < 2) == 1 这个条件成立,因此递归过程所得结果就是 echolast 一开始所接受的参数列表中的最后的参数。

宏的作用域

所有的宏都能掌控全局。

如果我们需要『局部变量』该怎么做?也就是说,如何将一个宏只在另一个宏的定义中使用?局部宏的意义就类似于编程语言中的局部变量,如果没有局部宏,那么在一个全局的空间中,很容易出现宏名冲突,导致宏被意外的重定义了。

为了避免宏名冲突,一种可选的方法是在宏名之前加前缀,比如使用 local 作为局部宏名的前缀。不过,这种方法对于递归宏无效。更好的方法是用

M4 实际上是用一个栈来维护宏的定义的。当前宏的定义位于栈顶。使用 pushdef 可以将一个临时定义的宏压入栈中,在利用完这个临时的宏之后,再用 popdef 将其弹出栈外。例如:

define(`USED',1) define(`proc',   `pushdef(`USED',10)pushdef(`UNUSED',20)dnl `'`USED' = USED, `UNUSED' = UNUSED`'dnl `'popdef(`USED',`UNUSED')') proc     # -> USED = 10, UNUSED = 20 USED     # -> 1

如果被压入栈的宏是未定义的宏,那么 pushdef 就相当于 define 。如果 popdef 弹出的宏也是未定义的宏, popdef 就相当于 undefine ,它不会产生任何抱怨。

GNU m4 认为 define(X, Y)popdef(X)pushdef(X, Y) 等价。其他的 m4 实现会认为 define(X) 等价于 undefine(X)define(X, Y) ,也就是说,新的宏的定义会更新整个栈。

没有参数就不展开

GNU m4 在文本中遇到与内建宏同名的单词,例如 define ,它不会改变这个单词,除非这个按此后面紧跟着括号。例如:

define(`MYMACRO',`text')    # -> define a macro              # -> define a macro

我们可以认为 m4 没有展开这个宏——但事实上它展开了,只不过是展开结果是宏名自身。我们也能让自己定义的宏具备这种能力,只加一个条件判断即可实现。例如,下面定义了的可逆转字符串的宏 reverse

define(`reverse',`ifelse($1,,,                   `reverse(substr($1,1))`'substr($1,0,1)')') reverse drawer: reverse(`drawer')     # ->  drawer: reward  define(`reverse',`ifelse($#,0,``$0'',$1,,,                   `reverse(substr($1,1))`'substr($1,0,1)')') reverse drawer: reverse(`drawer')     # -> reverse drawer: reward

让内建宏名更安全

m4 有一个 -P 选项,它可以强制性的在其内建宏名之前冠以 m4_ 前缀。例如下面的 M1.m4 文件:

define(`M1',`text1')M1          # -> define(M1,text1)M1 m4_define(`M1',`text1')M1       # -> text1

直接用 m4 处理,结果为:

$ m4 M1.m4 text1          # -> define(M1,text1)M1 m4_define(M1,text1)text1       # -> text1

如果用 m4 -P 来处理,结果为:

$ m4 -P test.m4 define(M1,text1)M1          # -> define(M1,text1)M1 text1       # -> text1
正文到此结束
Loading...