这篇笔记的主要内容取自 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)
会比较字符串 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 的实现可能没有这个宏。
在遵循 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