索引

书末的索引——把词条与它出现的页码排在一起的那份清单——并不是 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 却什么都没出来时,先怀疑这缺失的一行。

latex
\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 中,条目写法出错时就看这里。

shell
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 之后

shell
# 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,即使没有排序键,它也会正确落在 abacuszebra 之间,也就是 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;用 @ 给出读音在两者中都有效。

shell
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)。
style.ist
% 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-escapeimakeidx 也能把索引建好(用 kpsewhich -var-value shell_escape_commands 可以查看自己环境上的名单)。而 xindytexindymendexupmendex 不在该名单中,所以 确实需要 -shell-escape。在完全禁止 shell escape 的投稿系统或严格的 CI 中,自动调用不可用;这时请退回到自己调用 makeindex 的三步流程,或交给 latexmk

latex
\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