groff/troff 宏包:ms、mm、me 与 mom
摘要
一篇关于如何在 groff/troff 宏包(ms、mm、me、mom)之间做选择的说明文章,介绍了 troff/groff 是什么、纯文本排版相比文字处理软件的优势,以及各宏包的适用场景。
<p><a href="https://lobste.rs/s/dbwbvr/groff_troff_macros_ms_mm_me_mom">评论</a></p>
查看缓存全文
缓存时间: 2026/11/01 15:37
# groff/troff 宏包:ms、mm、me 还是 mom
来源:https://www.stephenlindholm.com/groff_macros.html
[](https://www.stephenlindholm.com/picture_detail.html?md5=OAJgGwvLZMfhmaTJ4pabxw%3D%3D)
本文简要说明如何在 Unix 排版系统 *troff*(或 *groff*)中选择宏包。我假设你已经熟悉 Linux 或 Unix。
简短的回答是:用 *ms*,除非你在写小说(那就用 *mom*),或者在写结构化文档(那就用 *mm*——并且为它找一本好的参考书)。
## *troff* 和 *groff* 是什么?
*Troff* 是最初的 Unix 排版软件。*Troff* 不像现代文字处理器,文档页面不会像在纸上那样直接显示在屏幕上让你用鼠标编辑。用 *troff* 时,你的文档是一个纯文本文件,文本和内嵌的格式命令混在一起。要想看到文档的样子,需要用 *troff* 处理这个纯文本文件来生成 PDF 文件,然后打开 PDF 看看是不是你想要的效果。
*troff* 格式命令的一个例子是“`\ .ce`”,它把下一行文本居中。这些简单的命令称为“请求”(request)。另一种格式命令称为“宏”,它本质上是一捆格式命令。你可以把缩进文本、加粗字体、打印连续编号等命令打包成一个宏,然后用这个宏自动为文档的标题编号和排版。
宏帮助你把格式与文本分离,这样更容易修改格式或文本。如果你决定标题不再缩进而是居中,只需修改“标题”宏即可,不必手动更改文档中每个标题的格式。宏还意味着你可以轻松地把文本从一个 *troff* 文档复制到另一个文档。而在文字处理器中,复制的文本会带着原文档的格式,除非两个文档的样式完全相同,否则你多半需要重新格式化复制过来的文本。
Unix 最伟大的程序员之一 W. Richard Stevens 用 *troff* 排版了他的所有书籍。但连他也承认 *troff* 不好用,他说:“*Troff* 是一个工业级的套件,我花了多年时间学习它。”不过尽管难学,*troff* 用起来可能比文字处理器更高效。
- 技术写作可以更快,因为整个 *troff* 文档都用纯文本写成,包括制作表格、图表、数学公式等的命令。在文字处理器中,可能需要用鼠标手动绘制图表和数学公式,或者完全在另一个程序中创建,这更费时间。这正是 W. Richard Stevens 使用 troff 的原因。
- 重新排版文档更快,因为只需修改几个宏。为出版写书的作者需要多次重新排版,这在文字处理器中可能耗费大量时间。这正是 Peter Schaffter 创建宏包 *mom* 的原因。
- 整个 *troff* 文档都是纯文本,因此很容易在软件中参数化。例如,用 *troff* 为多人批量生成 PDF 格式的信件很容易。或者,如果你写了一个计算机程序,可以用 *troff* 让你的程序支持打印,而不必直接生成 PDF 文件或依赖非标准 API。
*Troff* 由贝尔实验室在 1970 年代初创建,此后几十年随 AT&T Unix 一起发布。但 AT&T Unix 现在几乎完全过时了,所以你不太可能找到 *troff* 的副本。相反,你更可能找到 *groff*,它是 GNU 项目发布的 *troff* 克隆。上面的大部分内容同样适用于 *groff*,下面除非特别说明,我只提 *groff*。
## *ms*、*mm*、*me* 和 *mom* 是什么?
如果你想,可以从零开始写一个 *groff* 文档。这意味着要自己定义标题、脚注、项目符号列表等的宏。但有更简单的方法。有一些标准宏包,每个都包含几十个有用的宏。其中四个叫 *ms*、*mm*、*me* 和 *mom*。
贝尔实验室为了内部使用——撰写技术论文和报告——制作了第一个宏包 *ms*。它相当好用,没有太多奇怪的意外。你可以混用 *groff* 请求和 *ms* 宏,如果你对文档的外观有清晰的构想,这很有帮助。W. Richard Stevens 为他写的书使用了定制版的 *ms*。但 *ms* 比 *mm* 或 *mom* 更受限,所以即使是编号列表这样的基本格式化也需要使用 *groff* 请求。*ms* 的在线文档够用但不算出色。
曾几何时,Unix 是世界上最先进的排版系统,使用的是 *mm* 宏包。这个宏包比 *ms* 功能丰富得多。例如,它内置了编号列表(阿拉伯数字、罗马数字或字母)。它还支持交叉引用,这对技术书籍等结构化文档至关重要。可惜 *mm* 比 *ms* 稍欠直观,而且互联网上关于 *mm* 的文档糟透了。不过,在 Unix 排版的黄金时代有关于 *mm* 的优秀书籍,二手买起来很便宜。
加州大学伯克利分校的一位开发者在 1980 年代写了 *me* 宏包。它比 *ms* 更完整,但 *me* 的创建者几十年来没有再维护它,文档仅限于创建者写的两篇短文。我不建议使用 *me*。
四个当中最现代的是 *mom*,由一位小说家创建。它具有高级排版特性,功能大约与 *mm* 相当。它为易用性而设计,并有优秀的在线文档。对许多用户来说,*mom* 是正确的选择。然而在某些用途中,使用 *mom* 实际上可能行不通,而且它缺少 *mm* 拥有的几个特定功能。
还有 *man*,你不会想用它来创建普通文档。它被 *man* 命令用来显示 *man* 手册页。
## 调用 *groff* 和 *troff*
如果你好奇为什么宏包的名字都以“m”开头,那是因为 *groff* 的“`-m`”命令行参数用来选择宏包,例如“`groff -ms letter.ms`”。贝尔实验室的宏包是“manuscript”(手稿)和“memorandum”(备忘录)的缩写。“me”似乎是个玩笑名字(比如 Eric P. Allman 亲手写的),而据其主页所说,“mom”也是(意思是,它是一个更友好的宏包)。
要对输入文件 *example.troff* 调用 *groff*,运行“`groff example.troff > example.ps`”(不使用宏包)或“`groff -ms example.ms > example.ps`”(使用 ms 宏包)。这会生成一个 PostScript 文件。你可能还安装了 *pdfroff*,它会生成 PDF 文件。
我唯一的 AT&T *troff* 来自 Solaris 11,用起来更晦涩。要对输入文件 example.troff 调用 *troff*,运行“`troff example.troff | /usr/lib/lp/postscript/dpost > example.ps`”(不使用宏包)或“`troff -ms example.ms | /usr/lib/lp/postscript/dpost > example.ps`”(使用 *ms* 宏包)。这会生成一个 PostScript 文件。
## 一个简单示例
上手 *groff* 有时会有点棘手,所以这里给出使用原生 *groff*、*ms*、*mm* 和 *mom* 的简单示例。
原生 *groff* *ms* *mm* *mom*
``
.sp 1i
Hello, world!
``
``
.LP
Hello, world!
``
``
.LP
Hello, world!
``
``
.PRINTSTYLE TYPESET
.START
.PP
Hello, world!
``
原生 *groff* 文件打印出“Hello, world!”,顶部边距为一英寸(“`\ .sp 1i`”表示“垂直方向空一英寸”),左边距也为一英寸(默认值)。页面其余部分是空白的。我包含这个示例,是为了让你看到对于简单的单页文档,使用原生 *groff* 是可行的选择。这可以让初学者避免使用宏包带来的挫败感。
*ms* 文件打印出“Hello, world!”,顶部和左边距都为一英寸(默认值)。页面其余部分空白。“`\ .LP`”宏告诉 *groff* 下一段是左对齐段落。还要注意 *groff* 区分大小写。通常宏用大写,请求用小写。
*mm* 文件在页眉打印页码。所以“Hello, world!”左边距为 0.963 英寸(默认值),顶部边距为七行,使其在页面上稍低一些。(如果你好奇为什么 mm 中默认左边距是 0.963 英寸,这是 26/27 英寸四舍五入到三位小数。如果你好奇 27 为什么重要,*troff* 为其设计的 C/A/T 排版机分辨率为 27×16=432 dpi。)
*mom* 文件在页脚而不是页眉打印页码,并且“Hello, world!”在页面上比 *mm* 印得更低。它还使用比其他三个示例更大的字体。这些宏与 *ms* 或 *mm* 差别很大;我下面会详细说明。
## *ms* 中一个更真实的示例
假设你想要一个文档:有标题,第一页顶部是一栏宽的排版,文档其余部分是两栏。例如,这可以是一篇科学论文或一份法律合同。这里我只讲解足够在 *ms* 中创建这样一个文档的 *groff* 知识。
初学者对 *groff* 最困惑的部分是段落宏(比如“`\ .LP`”或“`\ .PP`”)的工作方式。*Groff* 没有段落的概念;它只是在页面上一行接一行地填充文本,直到有理由停下来。*ms* 中段落宏的作用是告诉 *groff* 停止填充行、重置某些参数并向下移动一点。这些参数包括文本是否居中,因此把“`\ .ce`”紧放在“`\ .LP`”之前意味着“`\ .ce`”请求被“`\ .LP`”宏覆盖,因此没有效果。这与文字处理器不同,在那里你先让文字处理器居中再开始新段落,或者反过来,结果都一样。
接下来是字体。如上所述,段落宏会重置某些 *groff* 参数,其中一个参数是当前字体。在旧版 AT&T *troff* 中,这意味着除非特别更改,每个段落都会用 Times roman 排版。不过,*groff* 增加了字体族(typeface)的概念和“`\ .fam`”请求来更改当前字体族。例如,在文档顶部放上“`\ .fam H`”,整个文档就都是 Helvetica,无需在每个段落中更改字体。“`\ .fam`”的另一个好处是让你可以用“`B`”和“`I`”来指代当前字体族的粗体和斜体版本。否则,如果你的文档用 Helvetica 排版,每次要把某个词加粗或变斜体,你都必须明确要求 Helvetica 粗体(“`HB`”)或 Helvetica 斜体(“`HI`”),以免得到 Times roman。顺便说一下,*groff* 中的字体既可以通过请求更改,也可以通过“转义序列”更改。转义序列类似于请求,但它出现在文本中间而不是单独一行。例如,“`\fB`”和“`\fR`”分别与请求“`\ .ft B`”和“`\ .ft R`”效果相同。
像许多计算机语言一样,*groff* 有变量。数值变量称为“寄存器”。你用“`\ .nr`”请求设置寄存器,如果寄存器名只有一个字母,就用转义序列“`\n`”打印它。例如,“`\ .nr k 42`”把寄存器“`k`”设为 42,之后文档中的“`\nk`”会被替换为“`42`”。宏包使用一些你可以修改以改变格式的寄存器。例如在 *ms* 中,“`HM`”寄存器指页眉边距的大小(*即*页边距的顶部)。如上简要提到,这些格式寄存器可以带后缀来表示度量单位,比如“`i`”表示“英寸”或“`v`”表示垂直行。因此在 *ms* 中,“`\ .nr HM 1i`”把页眉边距(顶部边距)设为一英寸。另一个有用的功能是把寄存器设为自动递增,这意味着用一个转义序列就可以自动按指定量增加(或减少)寄存器并打印其值。你用同一个“`\ .nr`”请求设置初始值和步长(寄存器将增加或减少的量)。然后用转义序列“`\n+`”来递增并打印寄存器。例如,“`\ .nr k 0 1`”把寄存器“`k`”设为零,步长设为 1。那么“`\n+k\n+k\n+k`”在文档中会被替换为“`123`”。
字符串变量称为“string”,请求“`\ .ds`”用于定义字符串。正如你可能预期的,字符串的一个常见用途是在文档中多次插入不变的文本块,比如信件中收信人的名字。字符串也可以包含寄存器(包括自动递增寄存器)和格式化的转义序列。把转义序列放进字符串时,你可能需要额外的反斜杠(详情请参阅参考手册)。要插入字符串,如果字符串名只有一个字符,就用转义序列“`\*`”。例如,如果文档前面出现过“`\ .ds N answer`”,那么“`\*N`”会被替换为“`answer`”。
现在进入示例。我已经链接了完整的 *ms* 源文件 (https://www.stephenlindholm.com/proj/babyshark.ms) 和输出的 PDF 文件 (https://www.stephenlindholm.com/proj/babyshark.pdf)。首先我们设置文档并打印标题。
`\ .ds N \\\\fB\\\\n+n.\\\\fR`我用“`\ .ds`”请求定义了一个名为“`N`”的字符串,我将用这个字符串“`N`”来给段落编号,使用自动递增寄存器来跟踪段落号。我把寄存器命名为“`n`”,“`\.`”只是我想印在编号后面的一个点。字体“`R`”通常指 Times roman(非斜体、非粗体的 Times),但因为我在插入这个字符串之前已经把字体族改成 Helvetica,所以字体“`R`”改为指非斜体、非粗体的 Helvetica。因为我是在定义字符串,所以三个转义序列都需要额外的反斜杠。``
.nr HM 0.75i
.nr FM 0.75i
.nr PO 0.75i
.nr LL 7i
``
我想把各边边距缩小到 0.75 英寸。所以我把 *ms* 的页眉边距、页脚边距和页偏移(*即*左边距)寄存器都设为 0.75 英寸。*ms* 没有右边距寄存器。相反,*ms* 有一个行长寄存器,即每行的文本量(*即*页面宽度减左边距减右边距)。我把行长设为七英寸。``
.fam H
.ss 12 0
.nr PS 12
.nr VS 14
.nr PD 0.4v
``
文档将用 Helvetica 排版。我把字号和行距(leading)的 *ms* 寄存器设为 12 和 14。“`\ .ss`”请求控制词间距(通常应与字号相同),并控制句间距的额外空白。(给 *troff* 纯粹主义者注意:“`\ .ss`”的第二个参数是 *groff* 的扩展。)字号是 12,所以词间距设为 12,并且与现代设计原则一致,我不希望句末有额外空白,所以把额外的句间空白设为零。我把段落间距的 *ms* 寄存器设为 0.4 行文本;这是段落之间额外的垂直空白。(这实际上是 *groff* 的默认值,我写出来是为了清楚起见。)``
.LP
.ce 1
.nr n 0 1
.ft B
BABY SHARK
``
我把下一行文本(不计请求和宏行)居中并加粗,也就是标题“BABY SHARK”。我把寄存器“`n`”设为零
相似文章
Typst 0.15 包含众多新内容
Typst 0.15 是开源排版系统的一个重要版本,引入了对可变字体、MathML 导出、多文件输出、多个参考文献列表和多个 PDF 标准的支持,同时还改进了文档和诊断功能。
Emacs 31: Markdown-ts-mode 非官方指南
一本关于在 Emacs 31 中设置和使用实验性的 Markdown-ts-mode 的非官方指南,涵盖其功能、安装和使用技巧。
macro-inc/macro
Macro 是一个一体化工作空间,将电子邮件、消息、文档、任务、智能体和 CRM 统一到单一界面中,并具备共享的团队级记忆,由 macro-inc 构建。
哥德尔、艾舍尔、Elisp:宏之美
这篇文章颂扬了Emacs Lisp宏和同像性之美,将其与侯世达的怪圈和艾舍尔的艺术相提并论,并介绍了诸如macrostep之类的工具来探索宏。
Typst 取得重大进展
Typst,一个定位为 LaTeX 替代品的排版系统,发布了版本 0.15,包含新功能,支持可变字体、MathML 和多个参考文献列表。