2019独角兽企业重金招聘Python工程师标准>>>
M4 宏处理器
Brian W. Kernighan
Dennis M. Ritchie
Bell Laboratories
Murray Hill, New Jersey 07974
翻译:寒蝉退士
译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
原文:http://cm.bell-labs.com/7thEdMan/vol2/m4
摘要
M4 是在 UNIX ® 和 GCOS 上可用的宏处理器。它的主要用途是在无参数宏不够强力的情况下用做 Ratfor 的前端。它也被用于各异的语言如 C 和 Cobol。M4 特别适合于函数式语言如 Fortran、PL/I 和 C,因为宏是以函数表示法指定的。
M4 提供了即使在更大的宏处理器中都少见的特征,它包括了
参数
条件测试
算术功能
字符串和子串函数
文件操纵
本文是 M4 用户手册。
July 1, 1977
介绍
宏处理器是增强一门编程语言,使它更加美味或更加可读,或者把它裁剪得适合特定应用的有用的方式。C 语言中的 #define 语句和 Ratfor 中类似的 define 就是任何宏处理器都提供的基本设施的例子 — 它把文本替代为其他文本。
M4 宏处理器是对叫做 M3 的宏处理器的扩展,它是 D. M. Ritchie 为 AP-3 小型机写的;M3 基于的是为 [1] 实现的宏处理器。不熟悉宏处理基本概念的读者可以读一下其中的某些讨论。
M4 适合于做 Ratfor 和 C 的前端,而且还成功的用于 Cobol。除了直接把一个字符串替代为另一个字符串之外,它还提供带有参数的宏、有条件的宏扩展、算术、文件操纵和某些特 殊的字符串处理函数。
M4 的基本操作把输入复制到输出。但在读入输入的时候,检查每个字母数字(alphanumeric)的“记号”(token) (就是说字母和数字的字符串)。如果它是一个宏的名字,则把这个宏的名字替代为它所定义的文本,把结果的字符串压回到输入中被重新扫描。调用宏可以带有参数,在这种情况下,参数被收集并在重新扫描之前替换入定义的文本中正确的位置上。
M4 提供了一组大约二十个内置宏来进行各种有用的操作;此外,用户可以定义新宏。内置和用户定义的宏以完全一样的方式工作,除了某些内置宏有对处理器状态的副作用之外。
用法
在 UNIX 上使用
m4 [files]
依次处理每个参数文件;如果没有参数或参数是‘−’,则在此位置上读取标准输入。处理后的文本写到标准输入,它可以被后续处理用下列方式捕获
m4 [files] >输出文件
在 GCOS 上用法是一样的,但程序叫做 ./m4。
定义宏
M4 的首要内置函数是 define,它被用来定义新宏。输入
define(name, stuff)
导致把字符串 name 定义为 stuff。name 的所有后续出现都被替代为 stuff。name 必须是字母数字的并且必须开始于字母(下划线 _ 被当作字母)。stuff 是包含配对的圆括号的任何文本;它可以伸展到多行之上。
所以作为典型的例子,
define(N, 100) ... if (i > N)
定义 N 为 100,并在后面的 if 语句中使用了这个“符号常量”。
define 必须立即跟随着左圆括号,它通知 define 有参数。如果一个宏或内置的名字没有立即跟随着‘(’,则假定它没有参数。这是上面 N 的情况;它实际上是一个没有参数的宏,所以在后面用到它的时候不要跟随着 (...)。
---- 第 1 页完 ----
你还应注意宏名字只在它出现被非字母数字围绕的时候才被识别。例如,在
define(N, 100) ... if (NNN > 100)
变量 NNN 绝对与定义的宏 N 无关,即使它包含很多个 N。
可以把事物定义为另一个事物。例如
define(N, 100) define(M, N)
定义 M 和 N 二者都是 100。
如果 N 被重新定义了会怎样呢? 或者换个方式说,M 被定义为 N 还是 100? 在 M4 中,后者是真的 — M 是 100,所以即使 N 随后改变了,M 也不变。
引起这种行为的原因是,M4 尽可能早的把宏名字展开为它们的定义文本。这意味着在收集 define 的参数时见到字符串 N 的时候,他立即被替代为 100;如同你首先写的是
define(M, 100)
如果这不是你实际上想要的,还有两种方式可用。首先,特定于这种情况,可以对换两个定义的次序:
define(M, N) define(N, 100)
现在 M 被定义为字符串 N,所以当你以后查找 M 的时候,你将总是得到那时 N 的值(因此 M 会被替代为 N,它又会被替代为 100)。
引用
更一般的解决方式是通过引用上 define 的参数来延迟它们的展开。围绕着单引号 ` 和 ´ 的任何文本都不被立即展开,而是剥除这些引号。如果你写
define(N, 100) define(M, `N´)
围绕着 N 的引号在收集参数的时候被剥除,它们已经完成了使命,M 被定义为字符串 N,而不是 100。M4 的普遍规则是在它求值某个东西的时候总是剥除一层单引号。即使是在宏外部也是这样。如果你希望字 define 出现在输出中,你必须在输入中引用它,比如
`define´ = 1;
作为同一事情的有些令人惊讶的另一个实例,考虑重定义 N:
define(N, 100) ... define(N, 200)
可能有些遗憾,在第二个定义中的 N 在被见到之后马上就被求值;就是说,它被替代为 100,所以如同你写的是
define(100, 200)
这个语句被 M4 忽略,因为你只能定义看起来是名字的东西,并且这明显的不是你想要的效果。要真正的重定义 N,你必须通过引用延迟这个求值:
define(N, 100) ... define(`N´, 200)
在 M4 中,引用宏的第一个参数经常是明智的。
如果出于某种原因使用 ` 和 ´ 不方便,可以用内置 changequote 改变引用字符:
changequote([, ])
使左右方括号成为引号字符。恢复最初的字符只需要
changequote
有两个增补的与 define 有关的内置。undefine 去除某个宏或内置的定义:
undefine(`N´)
去除 N 的定义。(为什么引号是绝对必须的?) 内置可以用 undefine 去除,比如
---- 第 2 页完 ----
undefine(`define´)
但是一旦你去除了它,就再也不能让它回来了。
内置 ifdef 提供了确定一个宏是否定义了的方式。特别是,M4 在相应的操作系统上有预定义的名字 unix 和 gcos,你可以如下这样确定正在使用的是哪个:
ifdef(`unix´, `define(wordsize,16)´ ) ifdef(`gcos´, `define(wordsize,36)´ )
使一个定义适合特定的机器。不要忘记引号!
ifdef 实际上允许三个参数;如果这个名字未定义,ifdef 的值就是第三个参数,比如
ifdef(`unix´, on UNIX, not on UNIX)
参数
迄今为止我们已经讨论了宏处理的最简单形式 — 把一个字符串替代为另一个(固定的)字符串。用户定义的宏也可以有参数,因此不同的调用可以有不同的结果。 在一个宏的替代文本内(define 的第二个参数),$n 的任何出现都被替代为实际使用这个宏的时候的第 n 个参数。所以,如下定义的宏 bump
define(bump, $1 = $1 + 1)
生成把它的参数加上 1 的代码:
bump(x)
是
x = x + 1
宏可以有同你想要的一样多的参数,但只有前九个是可以访问的,从 $1 到 $9。(宏名字自身是 $0,但不常用)。没有被提供的参数被替代为空字符串,所以可以通过简单的串联它的参数来定义一个宏 cat,比如:
define(cat, $1$2$3$4$5$6$7$8$9)
所以
cat(x, y, z)
等价于
xyz
$4 到 $9 是空的,因为没有提供相应的参数。
丢弃在参数收集期间出现的前导的未引用的空格、tab 或换行。保留所有其他空白。所以
define(a, b c)
定义 a 为 b c。
参数用逗号分隔,但是圆括号可以被正确处理,所以逗号可以用圆括号“保护”起来而不终止一个参数。就是说如下之中
define(a, (b,c))
只有两个参数;第二个参数是文字的 (b,c)。当然可以通过引用它来插入裸露的逗号或圆括号。
算术内置
M4 (只)提供两个做整数算术的内置函数。最简单的是 incr,它向数值参数增加 1。要处理常见的编程情况,你希望定义一个变量为“比 N 多一”,可以写
define(N, 100) define(N1, `incr(N)´)
则定义 N1 为比 N 的当前值多一。
更一般的算术机制是叫做 eval 的内置函数,它有能力做关于整数的任意算术。它提供如下算符(按优先级递减的次序)
一元 和 或 (指数)(模)(非) 或 (逻辑与) 或 (逻辑或)
可以用圆括号在需要的地方组合算符。给 eval 的所有操作数(operand)最终必须是数值。真关系(如 1>0)的数值值为 1,而假为 0。eval 的精度在 UNIX 上是 32 位在 GCOS 上是 36 位。
---- 第 3 页完 ----
作为一个简单例子,假设我们希望 M 是 2**N+1。则
define(N, 3) define(M, `eval(2**N+1)´)
作为法则,建议你引用对宏定义的文本,除非它确实非常简单(就是说是个数);这通常会给出你想要的结果,这是个好习惯。
文件操纵
你可以通过内置 include 在任何时候包含一个新文件到输入中:
include(filename)
把 filename 的内容插入到 include 命令的位置中。这个文件的内容经常是一组定义。include 的值(就是它的替代文本)是这个文件的内容;它可以 捕获于这些定义中。
在 include 中的文件不能被访问是致命的错误。要获得对这种情况的某种控制,可以使用替代形式的 sinclude;sinclude (“silent include”)在这个文件不能被访问的时候什么都不说并继续。
还可以在处理期间把 M4 的输出转向到临时文件,输出紧随这个命令后收集到的材料。M4 维护九个这种转向文件,编号从 1 到 9。如果你写
divert(n)
所有后续的输出被添加到 n 所引用的临时文件的结束处。通过另一个 divert 停止转向到这个文件;特别是 divert 或 divert(0) 恢复到正常的输出处理。
被转向的文本通常都在处理结束时按数字次序串联起来输出。但是也可以在任何时间取回被转向的文本,就是说把它们添加到当前转向文件之上。
undivert
按数字次序取回所有转向文本,带有参数的 undivert 按给定的次序取回选择的转向文本。去转向的活动丢弃已被转向的材料,转向到编号不在 0 到 9 之间的转向文件的东西也被丢弃。
undivert 的值不是被转向的材料。此外,被转向的材料不被宏所重新扫描。
内置 divnum 返回当前活跃的转向文件的编号,在正常处理期间它是零。
系统命令
你可以通过 syscmd 内置运行在本地操作系统中任何程序。例如
syscmd(date)
在 UNIX 上运行 date 命令。通常使用 syscmd 来为随后的 include 建立文件。
为了实现制作唯一的文件名字,提供了内置的 maketemp,带有同系统函数 mktemp 一致的规定: 在参数中的字符串 XXXXX 被替代为当前进程的进程 id。
条件
有一个叫做 ifelse 的内置使你能进行任意的条件测试。在最简单的形式中
ifelse(a, b, c, d)
比较两个字符串 a 和 b。如果它们是同样的,ifelse 返回字符串 c;否则返回 d。因此我们可以定义叫做 compare 的一个宏,它比较两个字符串并返回“yes”或“no”,分别在它们为相同的或不同的的时候。
define(compare, `ifelse($1, $2, yes, no)´)
注意引号,它防止 ifelse 的过早求值。
如果省略第四个参数,它被当作空。
ifelse 实际上可以用任何数目的参数,因此提供了有限形式的多路判断的能力。在输入中
ifelse(a, b, c, d, e, f, g)
如果字符串 a 匹配字符串 b,结果为 c。否则,如果 d 同于 e,结果为 f。否则结果为 g。如果省略了最后的参数,结果为空,所以
ifelse(a, b, c)
如果 a 匹配 b 则为 c,否则为空。
---- 第 4 页完 ----
字符串操纵
内置 len 返回构成它的参数的字符串的长度。因此
len(abcdef)
是 6,而 len((a,b)) 是 5。
可以使用内置 substr 来生成字符串的子串。substr(s, i, n) 返回 s 的从第 i 个位置(起始于零)开始的 n 个字符长的子串。如果省略了 n,返回余下的字符串,所以
substr(`now is the time´, 1)
是
ow is the time
如果 i 或 n 超出了范围,各种偶然的事情都可能发生。
index(s1, s2) 返回在 s1 中字符串 s2 出现的索引(位置),或者 −1 如果它没有出现的话。同于 substr,字符串起始于 0。
内置 translit 进行字符转换。
translit(s, f, t)
通过把在 f 中找到的任何字符都替换为 t 中对应的字符修改 s。比如
translit(s, aeiou, 12345)
替代元音为相应的数字。如果 t 比 f 短,在 t 中没有条目的字符会被删除掉;作为极限情况,如果 t 更本就不存在,把来自 f 的字符都从 s 中删除掉。所以
translit(s, aeiou)
删除 s 中的元音。
还有一个内置叫做 dnl,它删除跟随它的直到并包括下一个换行的所有字符;它主要用于丢弃空行,否则会使 M4 输出混乱。例如,如果你写
define(N, 100) define(M, 200) define(L, 300)
在每一行结束处的换行不是定义的一部分,所以它们被复制到输出,而这可能是我们不希望的。如果你在这些行上添加 dnl,这些换行就不见了。
完成这个目的的另一种方式,是 J. E. Weythman 提出的
divert(-1) define(...) ... divert
打印
内置的 errprint 把它的参数写出到标准错误文件。所以你可以写出
errprint(`fatal error´)
dumpdef 是个调试辅助函数,它转储定义的项的当前定义。如果它没有参数,则打印所有的项;否则你将得到同参数同名的项。不要忘记引用上这些名字!
内置总结
每个条目都前导上描述它的页号。
3 changequote(L, R)
1 define(name, replacement)
4 divert(number)
4 divnum
5 dnl
5 dumpdef(`name´, `name´, ...)
5 errprint(s, s, ...)
4 eval(numeric expression)
3 ifdef(`name´, this if true, this if false)
5 ifelse(a, b, c, d)
4 include(file)
3 incr(number)
5 index(s1, s2)
5 len(string)
4 maketemp(...XXXXX...)
4 sinclude(file)
5 substr(string, position, number)
4 syscmd(s)
5 translit(str, from, to)
3 undefine(`name´)
4 undivert(number,number,...)
致谢
我们感谢 Rick Becker、John Chambers、Doug McIlroy,特别是 Jim Weythman,他率先使用 M4 导致了很多有价值的改进。我们还要感谢 Weythman 对代码做的实质贡献。
---- 第 5 页完 ----
引用
[1] B. W. Kernighan and P. J. Plauger, Software Tools, Addison-Wesley, Inc., 1976.