类选项与自定义编写

写成 \documentclass[unknownoption]{article},编译照样能通过。类选项拼错了并不算错误:LaTeX 只是在日志深处留下一行 LaTeX Warning: Unused global option(s):,然后照常给你 PDF,所以几乎不会被察觉。可是换成 \usepackage[unknownoption]{color},编译立刻停住。这种不对称并非规格的怪癖,而是源于一个刻意的设计:无法识别的选项,在类里和在宏包里默认的去向不同。 本页就从这个机制入手,一路搭出 \DeclareOption\ProcessOptions、较新的 \DeclareKeys,以及用 \LoadClass 把自己的类建在现有类之上的做法。

为什么拼错的类选项不会让编译停下

答案写在官方手册 clsguide 里:如果类文件中没有 \DeclareOption*,那么它未声明的所有选项都会被悄悄传给全部宏包;如果宏包文件中没有 \DeclareOption*,那么它未声明的每个选项都会报错。 也就是说,类选项被当作「说不定还有人要用」而一路带着走,只有当直到 \begin{document} 都没人认领时,LaTeX 才报告 LaTeX Warning: Unused global option(s):,后面用方括号列出剩下的名字。宏包选项则无处可去,所以一个不认识的名字会立刻变成 ! LaTeX Error: Unknown option 'unknownoption' for package 'color'.

这个设计是合理的:类本身不认识、却由之后加载的宏包接手的选项(全局选项),例如 \documentclass[dvipsnames]{article},每天都在被使用。代价就是 拼写错误会保持沉默。 因此实务上养成两个习惯很划算。一是每次构建后在日志里搜索 Unused global option。二是在导言区开头放 \listfiles,让日志末尾列出被加载的所有文件及其版本。另外,在选项代码中调用 \OptionNotUsed,可以主动把该选项送进同一份「未使用」清单。

类(.cls)与宏包(.sty)的区别

clsguide 给出的判断标准只有一句:如果这些命令可以与任何文档类一起使用,就做成宏包;否则就做成类。 类定义文档类型本身,用 \documentclass 只加载一个。宏包用 \usepackage 加载,多少个都行,添加与文档类型无关的功能。手册自己的例子很直观:某公司为在自家信笺上排信件而做的 ownlet 建立在 letter 类之上,却无法与其他类共用,所以是 ownlet.cls;用于插入图像的 graphics 在任何类下都能用,所以是 graphics.sty

类也分两种:像 articlereportletter 那样 自成一体 的,以及作为现有类的扩展或变体的——clsguide 举的后者例子是建立在 article 之上的 proc。你自己写的类几乎肯定属于后者,因为从零搭版面很不划算。.cls.sty 的编写规约几乎相同,只是命令成对分为 Class 版和 Package 版(\ProvidesClass\ProvidesPackage\LoadClass\RequirePackage\PassOptionsToClass\PassOptionsToPackage)。

自制类应当接受的标准选项

用户会像对待标准类一样给你的自制类传选项,所以至少要备齐这些常客:选择正文基准字号的 10pt / 11pt / 12pt、纸张的 a4paper / letterpaper、栏数的 onecolumn / twocolumn、单双面的 oneside / twoside,以及草稿显示的 draft(用黑条标出溢出行,反义是 final)。不过这些都不必自己实现;后面会看到,惯常做法是把它们 原样转发 给基础类。

选项含义默认值
10pt / 11pt / 12pt正文基准字号10pt
a4paper / letterpaper纸张大小(还有 b5paperlegalpaper 等)letterpaper
onecolumn / twocolumn单栏 / 双栏onecolumn
oneside / twoside单面 / 双面版式oneside(仅 booktwoside
draft / final是否用黑条标出溢出的行final

若想让类自己决定用户什么都不指定时的默认值,就在 \ProcessOptions 之前\ExecuteOptions{a4paper,11pt}。这是在声明「先把这些选项的代码跑一遍」,clsguide 也正是以这种形式说明如何给类设定默认设计。至于在文档的 \documentclass[...] 一侧 使用 这些选项,以及 book 特有的 openright 等,归「文档类与导言区」那一页负责。从这里开始,我们专注于如何编写接收这些选项的类。

在文件开头报出身份 — \NeedsTeXFormat 与 \ProvidesClass

类文件(myclass.cls)的前两行几乎是固定写法。先用 \NeedsTeXFormat{LaTeX2e} 声明这个文件面向 LaTeX2e。接着用 \ProvidesClass{myclass}[2026/01/01 v1.0 My example class] 报出类名、发布日期、版本和简短说明。这一行真正发挥作用的地方是日志:编译之后会留下 Document Class: myclass 2026/01/01 v1.0 My example class 这一行。当合著者说「排不出来」时,先要来看这一行,就能立刻判断对方手里是不是一个旧的 .cls

方括号部分可以省略,但写上之后,用户就能通过 日期(YYYY/MM/DD 格式) 要求最低版本,例如 \documentclass{myclass}[2026/01/01]。若写的是宏包,对应命令是 \ProvidesPackage{mypackage}[2026/01/01 v1.0 ...]\NeedsTeXFormat 两者共用。铁则是 \ProvidesClass 中的名字必须与实际文件名一致——在 myclass.cls 里一定要写 \ProvidesClass{myclass}

latex
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/01 v1.0 My example class]

声明选项 — \DeclareOption 与 \CurrentOption

类能接受的每个选项都用 \DeclareOption{option}{code} 逐一声明。用户指定该选项后,等执行到下文的 \ProcessOptions 时,code 就会运行。内容可以是任意合法的 LaTeX 结构,但实际上最常见的是用 \newif 建立的布尔标志置位的那一行——把重活留到之后再依标志执行,可以避免顺序上的事故。

未声明选项的接收口是带星号的 \DeclareOption*{code},其中 \CurrentOption 会展开为「当前正在处理的选项名」。自制类里最常见的一行,就是用它把未知选项原样转发给基础类。有了这一行,即使你没有声明 10pta4paper,用户也能理所当然地传入——这等于把开头讲的「类会默默往下传」这一默认行为,重新指向了你选定的目的地。

latex
% pass anything we do not handle ourselves on to article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}

处理选项,再加载基础类 — \ProcessOptions 与 \LoadClass

只声明选项不会发生任何事;只有调用 \ProcessOptions,被选中的选项的代码才会运行。实务上几乎总写作 \ProcessOptions\relax。因为还存在带星号的 \ProcessOptions*,末尾的 \relax 能可靠地选中无星版本,避免不必要的向前扫描和可能令人困惑的报错——clsguide 也明确这样建议。无星版本按 声明顺序 处理选项,带星版本按 调用方书写的顺序 处理。

从零搭版面很不划算,所以大多数自制类都以现有类为基础,这就是 \LoadClass[options]{article},它会把 article.cls 的命令和体裁整体读入。这条命令 只能在类文件内部使用,且每个类文件至多用一次。顺序也很重要:为了让用户在 \documentclass[...] 里给出的选项影响基础类,\LoadClass 要放在选项处理(\ProcessOptions)之后——先写转发设置,再让 \ProcessOptions 分派,最后加载基础类。如果只想把自己收到的选项原样交出去,\LoadClassWithOptions{article} 更省事;写宏包时用 \RequirePackage 代替 \LoadClass,全部原样传递则用 \RequirePackageWithOptions

\LoadClass 之后,才是真正写出 这个类的个性 的地方:用 \renewcommand 改标题体裁,用 \setlength 调整边距,用 \newcommand / \newenvironment 定义新命令和环境。需要的附加宏包也从这里用 \RequirePackage 加载。反过来记:\LoadClass 之前只应放选项的声明与处理,这样顺序问题就不再是问题。

完整示例:扩展 article 的最小类

把以上内容合到一起,就是下面这个最小 .cls。它以 article 为基础,添加自己的 draft 选项,把未知选项转发给 article,注入 a4paper 作为默认值,最后按自己的口味设定边距和节编号体裁。把它保存为 myclass.cls 放在稿件同一文件夹,在文档里写 \documentclass[11pt,a4paper,draft]{myclass} 就能使用。

latex
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/01 v1.0 My example class]

% --- declare options ---
\newif\if@my@draft \@my@draftfalse
\DeclareOption{draft}{\@my@drafttrue}
% forward everything else to article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}

% --- defaults, then execute, then load the base class ---
\ExecuteOptions{a4paper}
\ProcessOptions\relax
\LoadClass{article}

% --- this class's own character ---
\RequirePackage[margin=25mm]{geometry}
\setlength{\parindent}{0pt}
\renewcommand{\thesection}{\Alph{section}}
\if@my@draft
  \AtBeginDocument{\typeout{myclass: DRAFT MODE}}
\fi

\endinput

末尾的 \endinput 告诉 LaTeX「本文件到此为止」,按惯例都会写上;写在它之后的笔记或示例不会被读入。想改成宏包时,把 \ProvidesClass 换成 \ProvidesPackage,把 \LoadClass 换成 \RequirePackage,同一骨架就变成了 .sty

新写法 — \DeclareKeys 与 \ProcessKeyOptions

\DeclareOption 至今仍完全有效,但它主要面向「有 / 无」式开关;像 logo=acme.pdf 这种 带值的选项,就得自己动手解析。于是 LaTeX kernel 自己提供了 key-value 接口:用 \DeclareKeys 声明 key,用 \ProcessKeyOptions 处理。声明时附加在 key 名上的「属性」决定其行为,基本属性有 .code(执行任意代码)、.if / .ifnot(设置 TeX 布尔开关)、.store(把值存入宏)、.usage(限定只能在加载时给出、可在导言区任意位置、或不受限制)。未知 key 交由 \DeclareUnknownKeyHandler 接手;一旦调用 \ProcessKeyOptions,就 不需要再调用 \ProcessOptions

latex
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{keyclass}[2026/01/01 v1.0 Key-value demo class]

\DeclareKeys[keyclass]{
  draft.if   = @keyclass@draft,
  logo.store = \@keyclass@logo,
  logo.usage = load
}
% anything that is not one of our keys goes to article
\DeclareUnknownKeyHandler[keyclass]{%
  \PassOptionsToClass{\CurrentOption}{article}}
\ProcessKeyOptions[keyclass]   % no \ProcessOptions needed
\LoadClass{article}

\endinput

这套机制原本由 l3keys2e 宏包提供,其核心此后并入了 LaTeX2ε kernel(TeX Live 2024 附带的 kernel 为 LaTeX2e 2023-11-01,提供 \DeclareKeys\ProcessKeyOptions\SetKeys)。现有宏包中仍能见到加载 l3keys2e 的写法,例如 jlreq.cls 就在开头执行 \RequirePackage{l3keys2e}。取舍很简单:只要有一个选项需要带值,就用 \DeclareKeys;若全是开关式选项,\DeclareOption 就够了。加载之后想改设置,则用 \SetKeys

发布前要跑的最小测试

类一旦加载就会影响整篇文档,因此在写正文之前,先用一个很小的测试文档把行为固定下来。要确认的只有两点——11pttwocolumn 之类的标准选项是否到达了基础类,以及是否只有你自己的选项由自制代码处理。如果这里表现不符合预期,原因几乎一定是 \ProcessOptions 的位置、\DeclareOption* 的转发,或 \LoadClass 的顺序。

latex
\listfiles                     % log every file and version that is loaded
\documentclass[11pt,a4paper,draft]{myclass}
\begin{document}
\section{Smoke test}
Check the body size, the paper, the draft switch,
the heading style and the margins.
\end{document}
  • 日志里有没有报出身份? 确认 .log 中出现 Document Class: myclass ... 这一行,以及你写在 \ProvidesClass 里的日期和版本。文件名与类名不一致,日后必定让人困惑。
  • 有没有破坏标准选项? 如果 11pttwocolumn 被忽略,请重新检查 \DeclareOption* 的转发或 \LoadClass 的位置。
  • 故意拼错一个选项。 编译 \documentclass[nosuchoption]{myclass},确认日志中出现 Unused global option(s)。若没有出现,说明你转发到的某个宏包把它默默吃掉了。
  • \endinput 之后保持空白。 末尾留下的笔记或示例,一旦这个标记丢失就会被当作输入读进来。

连发布形态一起考虑

自制类真正的考验,不是它在你机器上第一次能跑的瞬间,而是 别人在另一套环境里加载它的瞬间。最低限度,把 .cls、简短示例文档、README 和变更记录放在同一目录,并确认示例可原样编译。README 中要把「转发给基础类的选项」和「只由自制类接收的选项」分开写,用户才能追踪 11pt 到底在哪里生效。规模变大后,可改用 LaTeX 自带的 docdocstrip:把源码和说明一起放进 .dtx,再由 .ins 生成 .cls,这样发布和文档化就合为一条路径。

terminal
myclass/
  myclass.cls
  sample.tex
  README.md
  CHANGELOG.md

最后,在示例里加上 \listfiles。日志末尾会列出所有被加载的文件及其版本,你就能一眼分辨用户是不是本地留着旧的 myclass.cls,以及预期的宏包是否真的被加载。类是整篇文档的地基:就长期使用的文档而言,把加载顺序、选项处理和日志信息打理好,远比再添一个正文宏更可靠。