Chinaunix首页 | 论坛 | 博客
  • 博客访问: 704343
  • 博文数量: 19
  • 博客积分: 5025
  • 博客等级: 大校
  • 技术积分: 290
  • 用 户 组: 普通用户
  • 注册时间: 2006-01-24 10:41
个人简介

null

文章分类

全部博文(19)

分类:

2006-07-02 15:19:31

 

M4 宏处理器

Brian W. Kernighan
Dennis M. Ritchie

Bell Laboratories
Murray Hill, New Jersey 07974

译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。
原文:

摘要

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 定义为 stuffname 的所有后续出现都被替代为 stuffname 必须是字母数字的并且必须开始于字母(下划线 _ 被当作字母)。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 在相应的操作系统上有预定义的名字 unixgcos,你可以如下这样确定正在使用的是哪个:

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)

定义 ab    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 页完 ----

作为一个简单例子,假设我们希望 M2**N+1

define(N, 3)
define(M, `eval(2**N+1)´)

作为法则,建议你引用对宏定义的文本,除非它确实非常简单(就是说是个数);这通常会给出你想要的结果,这是个好习惯。

文件操纵

你可以通过内置 include 在任何时候包含一个新文件到输入中:

include(filename)

filename 的内容插入到 include 命令的位置中。这个文件的内容经常是一组定义。include 的值(就是它的替代文本)是这个文件的内容;它可以 捕获于这些定义中。

include 中的文件不能被访问是致命的错误。要获得对这种情况的某种控制,可以使用替代形式的 sincludesinclude (“silent include”)在这个文件不能被访问的时候什么都不说并继续。

还可以在处理期间把 M4 的输出转向到临时文件,输出紧随这个命令后收集到的材料。M4 维护九个这种转向文件,编号从 1 到 9。如果你写

divert(n)

所有后续的输出被添加到 n 所引用的临时文件的结束处。通过另一个 divert 停止转向到这个文件;特别是 divertdivert(0) 恢复到正常的输出处理。

被转向的文本通常都在处理结束时按数字次序串联起来输出。但是也可以在任何时间取回被转向的文本,就是说把它们添加到当前转向文件之上。

undivert

按数字次序取回所有转向文本,带有参数的 undivert 按给定的次序取回选择的转向文本。去转向的活动丢弃已被转向的材料,转向到编号不在 0 到 9 之间的转向文件的东西也被丢弃。

undivert 的值是被转向的材料。此外,被转向的材料被宏所重新扫描。

内置 divnum 返回当前活跃的转向文件的编号,在正常处理期间它是零。

系统命令

你可以通过 syscmd 内置运行在本地操作系统中任何程序。例如

syscmd(date)

UNIX 上运行 date 命令。通常使用 syscmd 来为随后的 include 建立文件。

为了实现制作唯一的文件名字,提供了内置的 maketemp,带有同系统函数 mktemp 一致的规定: 在参数中的字符串 XXXXX 被替代为当前进程的进程 id。

条件

有一个叫做 ifelse 的内置使你能进行任意的条件测试。在最简单的形式中

ifelse(a, b, c, d)

比较两个字符串 ab。如果它们是同样的,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

如果 in 超出了范围,各种偶然的事情都可能发生。

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 页完 ----

引用

   B. W. Kernighan and P. J. Plauger, Software Tools, Addison-Wesley, Inc., 1976.

---- 第 6 页完 ----

阅读(11322) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~