书末的索引——把词条与它出现的页码排在一起的那份清单——并不是 LaTeX 自己做的。LaTeX 的活儿到此为止:把正文里的 \index{…} 标记收集起来,写成一份叫 .idx 的原始清单。把这份清单排序、整理成索引的,是 另一个程序 makeindex。这种分工颇有来历:makeindex 自己的文档把 LaTeX 的作者 Leslie Lamport 列为对其设计有重大贡献的人。本页从 makeidx 宏包与 \makeindex 声明讲起,经过条目写法(! 子条目、@ 排序键)、构建流程,一直讲到「为什么 Ångström 排在 Zulu 后面」。
构成索引的四个部件 —— makeidx 只有八行
做索引要用到四个部件:导言区里的 \usepackage{makeidx} 与 \makeindex,正文中词条出现处的 \index{词条},以及希望排出清单的位置上的 \printindex。出人意料的是,这四个里有两个——\makeindex 与 \index 本来就在 LaTeX 内核(latex.ltx)里。makeidx 宏包所补充的只有 \printindex,加上用于交叉引用的 \see 和 \seealso,实际代码也就八行左右。这个设计透露了一件事:索引的重活从一开始就打算放在 LaTeX 之外。
\usepackage{makeidx}—— 提供\printindex以及\see/\seealso(导言区)。\makeindex—— 打开\jobname.idx并把\index重定义为「真正写出」版本的声明(仅限导言区)。终端会显示Writing index file mydoc.idx。\index{词条}—— 放在词条出现处的标记。它什么也不打印,只记录该处的 页码。\printindex—— 真正排出成品索引的命令,实质是读入.ind文件,通常放在文档末尾。
值得强调的是,\index 是一个 看不见的标记。词本身仍要你自己写进正文,然后在紧后面加上 \index{…},比如 random numbers\index{random numbers} are used。还有一个要紧的坑:若忘了在导言区写 \makeindex,\index 会 吞掉参数、什么也不做——内核的默认定义正是如此——于是既无错误也无警告,只有索引是空的。当你写了几十个 \index 却什么都没出来时,先怀疑这缺失的一行。
\documentclass{article}
\usepackage{makeidx}
\makeindex % without this line, \index does nothing
\begin{document}
METAFONT\index{METAFONT} draws the shapes,
TeX\index{TeX} sets the type.
We cover random numbers\index{random numbers|textbf} here,
and touch on groups\index{group} and rings\index{ring}.
The treatment of algorithms\index{algorithm|(} starts here ...
% ... several pages later ...
... and the treatment of algorithms\index{algorithm|)} ends here.
\printindex
\end{document}条目写法 —— !、@、| 与双引号这四个字符
\index 的参数有一套自己的小语法,建立在四个特殊字符之上。要记住的关键是:解释这四个字符的是 makeindex,而不是 LaTeX。对 LaTeX 来说,参数只是一串字符,原样倒进 .idx。因此语法写错在排版阶段无人过问,只有在运行 makeindex 时才会作为警告出现在 .ilg 日志里。
子条目用 !。 感叹号分隔层级:\index{animals!cats} 会在主条目「animals」下放入「cats」。重复 ! 可继续嵌套,最深到 三层(0、1、2)——这是 makeindex 设计上的上限。排序键用 @。 写成 sortkey@display,就能 把用于排序的字符串与实际印出的字符串分开:\index{alpha@$\alpha$} 在索引中印出 α,却排在「alpha」的位置。对于符号和公式这类按字形排序毫无意义的内容,这是必需的。
页码加工用 |。 竖线之后写上 一个接受单个参数的命令名(不带开头的反斜杠),该条目的这一页码就会用它来排。\index{cat|textbf} 是把定义所在页加粗的经典用法,|textit 或你自己的命令同样可用。页码范围用 |( 与 |)。 当一个话题跨越数页时,起点写 \index{recursion|(},终点写 \index{recursion|)},就会得到 12--15。另外,makeindex 默认会把连续三页及以上自动缩成范围;关掉这个自动行为的选项是 -r。
交叉引用同样写在 | 之后。 \index{dog|see{pets}} 会输出「dog, see pets」而不是页码,|seealso{…} 则给出「see also」。二者都是通过调用 makeidx 定义的 \see 与 \seealso 实现的,所以输出的词可以通过重定义 \seename(默认 “see”)和 \alsoname(默认 “see also”)改成别的语言。最后,双引号是转义符:想把 !、@、| 或双引号本身当普通字符放进条目时,在它前面加一个双引号——\index{C"!} 得到条目「C!」。C 语言和 C++ 的索引通常就栽在这里。
| 字符 | 作用 | 写法 |
|---|---|---|
! | 子条目(最多三层) | \index{animals!cats} |
@ | 排序键:把排序用与印刷用的字符串分开 | \index{alpha@$\alpha$} |
|( |) | 页码范围的起点与终点 | \index{recursion|(} … \index{recursion|)} |
|cmd | 用命令排出该页码(加粗等) | \index{cat|textbf} |
|see |seealso | 输出指向其他条目的提示而非页码 | \index{dog|see{pets}} |
" | 把紧随其后的特殊字符当作普通字符 | \index{C"!} 得到「C!」 |
运行 makeindex —— 从 .idx 到 .ind,以及 No file mydoc.ind.
索引不会在一次编译中完成。和 bibtex 一样,中间夹着一个外部程序,共三个阶段。 首先 LaTeX 把 \index 收集进 mydoc.idx——那是一份朴素的文件,里面只有一行行 \indexentry{词条}{页码},可以打开来读。接着 makeindex 把它排序整理成可排版的 mydoc.ind。最后再跑一次 LaTeX,\printindex 读入 mydoc.ind,索引就出现在文档里。排序的记录留在 mydoc.ilg 中,条目写法出错时就看这里。
pdflatex mydoc # writes mydoc.idx ("Writing index file mydoc.idx")
makeindex mydoc # mydoc.idx -> mydoc.ind, log in mydoc.ilg
pdflatex mydoc # \printindex reads mydoc.ind
# -s picks a style file, -o names the output, -t names the log
makeindex -s style.ist -o mydoc.ind -t mydoc.ilg mydoc.idx忘掉中间那一步,症状安静得出奇:既无错误也无警告,日志里只留下这一行——No file mydoc.ind.。原因就在机制本身:\printindex 归根结底是调用 \@input@,文件存在就读入,不存在就正好打印那一行。正是这份安静,让整个索引缺失的文档看上去也一切正常。不过在实践中,latexmk 会 替你 跑完这个来回:.idx 一变就调用 makeindex,并按需要的次数重跑 LaTeX,于是手动敲三步的机会越来越少。
为什么 Ångström 排在 Zulu 后面 —— makeindex 的排序方式
makeindex 排的不是你看到的词,而是 排序键;不写 @ 时,条目文字本身就是键。默认顺序有明确文档:符号 → 数字 → 字母,字母之间 先不区分大小写比较,只有拼写完全相同时才让大写在前。作为面向英语的设计,这一套没有什么可挑剔的。问题出在「字母」的范围:对 makeindex 而言,字母只包括英文字母和数字。把 Ångström 和 émile 原样交给 TeX Live 2024 附带的 makeindex 2.17,它们不会落在 A 和 E 处,而是排在 索引的最末尾,Zulu 之后。
# entries written with no sort key at all:
# +plus 9nine apple sea lion seal Zulu Angstrom emile
# (the last two really spelled Ångström and émile)
makeindex mydoc # default: word ordering
+plus / 9nine / apple / sea lion / seal / Zulu / Ångström / émile
makeindex -l mydoc # letter ordering: blanks do not count
+plus / 9nine / apple / seal / sea lion / Zulu / Ångström / émile
# the fix is an ASCII sort key, not an accented one:
# \index{Angstrom@Ångström} files under A
# \index{emile@émile} files under E由此得出两点。第一,给带重音的词一个 ASCII 排序键:\index{Angstrom@Ångström} 印出来仍是 Ångström,但归入 A。有一个常见误解,以为 \index{Ångström@Ångström} 能解决问题——并不能,因为键那一侧仍是非 ASCII。第二,makeindex 提供了排序方式的选择。默认是 单词序(word ordering),其中空格排在任何字母之前,所以「sea lion」在「seal」之前。加 -l 则是 字母序(letter ordering),空格完全不计,「seal」排在前面。要词典式排列就用 -l,要电话簿式就用默认。针对德语还有遵循 DIN 5007 的 -g。
若文档中的重音字符多到逐个手写排序键并不现实,更快的路子是换掉排序程序本身。xindy(在 LaTeX 侧的入口是 texindy)从设计之初就以多语言的排序规则为前提,并随 TeX Live 提供。把同一个 Ångström 交给 texindy -L english -C utf8,即使没有排序键,它也会正确落在 abacus 与 zebra 之间,也就是 A 的位置。索引越大,换排序器比手敲键更划算。
日文索引 —— mendex 与 upmendex
上一节的道理原样适用于中文和日文,而且症状更严重。把 \index{群}、\index{环}、\index{体} 直接交给 makeindex,它们会按 字符编码顺序 排出,且一条警告都不给——这个顺序与读音毫无关系。因为不报错,本以为按音序排好的索引,实际上可能毫无秩序。这里的答案和 hyperref 那节一样:换成专用工具。pLaTeX 用 mendex,upLaTeX 与 LuaLaTeX 用 upmendex。两者都与 makeindex 兼容,所以只需把原来敲的那个词换掉即可。
换来的是 按读音排序。在 makeindex 时代,每个条目都要以 读音@显示 的形式给出读音,浊音符号之类还得手工规整。upmendex 使用 ICU(International Components for Unicode)的排序规则来正确排列假名,省下大半功夫。此外,用 -d 传入 词典文件 可以批量登记汉语词的读音,很多条目因此可以完全省去 @ 读音。经验法则是:pLaTeX 用 mendex,upLaTeX / LuaLaTeX 用 upmendex;用 @ 给出读音在两者中都有效。
uplatex mydoc # writes mydoc.idx
upmendex -s style.ist mydoc # kana sorted via ICU -> mydoc.ind
uplatex mydoc # \printindex reads mydoc.ind
# readings can still be given by hand with @, in either program:
# \index{さくいん@索引}
# \index{Knuth@クヌース}改变索引的外观 —— .ist 样式文件
索引的体例由 样式文件(.ist) 掌管,通过 -s 传入,例如 makeindex -s style.ist mydoc。它的格式很朴素:一串 参数 值 的配对,字符串用双引号括起,% 起到行末是注释。写在这里的是给 makeindex 而不是给 LaTeX 的指令,它直接决定 .ind 文件的内容。mendex / upmendex 的样式与 makeindex 向上兼容,所以现有的 .ist 可以照搬。
headings_flag—— 设为非零时,每当分组改变就插入一个 分组标题(字母 A、B… 或符号组)(默认 0)。heading_prefix/heading_suffix—— 放在该标题前后的字符串。symhead_positive——headings_flag为正时给符号组的标题(默认 "Symbols")。delim_0/delim_1/delim_2—— 各层级条目与其页码之间的 分隔符(默认均为 ", ");点线引导也写在这里。item_0/item_1/item_x1—— 条目之间、层级之间插入的字符串(换行、缩进)。preamble/postamble—— 写在.ind开头和结尾的代码(默认为\begin{theindex}与\end{theindex})。group_skip—— 分组交界处插入的空白(默认为\indexspace)。
% group headings in bold, and a dotted leader before the page number
headings_flag 1
heading_prefix "{\\bfseries "
heading_suffix "}\\nopagebreak\n"
delim_0 "\\dotfill "现代做法 —— imakeidx 与多个索引
imakeidx 取代 makeidx,带来两项实在的好处。第一,它会在编译过程中 自动调用索引程序,于是做索引的感觉和做目录差不多。第二,它支持在同一份文档中放 多个索引——比如事项索引与人名索引分开。配置方式是给 \makeindex 传选项:name= 区分索引,title= 设定标题,intoc 让它进入目录,program= 选择排序程序(makeindex / xindy / texindy,中日文用 mendex / upmendex),options= 转发 -s style.ist 之类的参数。每个索引写一个 \makeindex,正文中用 \index[name]{…} 分流,再用 \printindex[name] 输出。
自动调用建立在 shell escape 之上,这一点依赖于环境。在 TeX Live 2024 的默认配置中,makeindex 位于 受限 shell escape 的白名单 上,因此即使不加 -shell-escape,imakeidx 也能把索引建好(用 kpsewhich -var-value shell_escape_commands 可以查看自己环境上的名单)。而 xindy、texindy、mendex、upmendex 不在该名单中,所以 确实需要 -shell-escape。在完全禁止 shell escape 的投稿系统或严格的 CI 中,自动调用不可用;这时请退回到自己调用 makeindex 的三步流程,或交给 latexmk。
\documentclass{article}
\usepackage{imakeidx}
% two indexes, built during the compilation
\makeindex[name=subject, title=Subject index, intoc]
\makeindex[name=people, title=Index of names, intoc,
options={-s style.ist}]
\begin{document}
Groups\index[subject]{group} matter here.
Knuth\index[people]{Knuth, Donald} wrote TeX.
\printindex[subject]
\printindex[people]
\end{document}
% makeindex runs under restricted shell escape:
% pdflatex mydoc
% xindy / mendex / upmendex need the full permission:
% lualatex -shell-escape mydoc