在 LaTeX 里打印一个反斜杠,比打印一个积分号还难——这不是玩笑。verbatim(片段用 \verb,整块用 verbatim 环境)是整个系统中唯一必须临时关闭 LaTeX 自身语法才能工作的地方。所有看似不讲道理的规则都源于此:\verb 不能跨越换行,\end {verbatim} 中间多一个空格就会让编译停住,把 \verb 放进 \section{...} 或 \footnote{...} 会报出一个根本没提 verbatim 的错误。本页介绍 verbatim 环境与 \verb、把空格显示出来的星号形式、整文件读入的 \verbatiminput 与 \VerbatimInput、保留少量命令的 alltt、可加行号和边框的 fancyvrb,以及在正文中打出单个反斜杠或下划线的方法。
verbatim 环境到底关掉了什么
写在 \begin{verbatim} 和 \end{verbatim} 之间的内容,会连同换行和空格一起按输入原样用等宽字体打印,不需要任何宏包。其原理不是「转义」,而是 降级。进入该环境时,LaTeX 会一次性把 \、{、}、$、&、#、^、_、%、~ 的类别码(category code)改写为「普通字符」。类别码是 TeX 在读入时给每个字符贴上的角色标签,它决定该字符是开始一条命令、打开一个分组,还是仅仅是一块墨迹。verbatim 里的反斜杠之所以不开始命令,并不是因为命令被忽略,而是因为 在那一刻,反斜杠只是一个长得像反斜杠的普通符号。
\begin{verbatim}
for i in range(3):
print("100% & $5 \n") # none of this is interpreted
\end{verbatim}实现这一点对作者本人也很别扭,而这份别扭被原封不动地保留在 LaTeX 自己的源码里。在定义 LaTeX 本体的 latex.ltx 中,负责扫描 verbatim 结尾的那个宏,是 在把转义字符改成 |、把分组符改成 [ 和 ] 的状态下定义的。因为在那段定义内部,\、{、} 这三个字符必须都是普通的可打印字符,这门语言无法用自己惯常的记法来表达自己。在定义 verbatim 的那一小段时间里,LaTeX 的源码不再用 LaTeX 书写。这两行在 TeX Live 2024 附带的 latex.ltx 中可以直接读到。
从这种「寻找结尾」的方式,可以推出两条实用规则。第一,环境内部不能原样出现字符串 \end{verbatim}:LaTeX 一看到它就判定环境结束。第二,\end 与 {verbatim} 之间不能有空格。终止符是作为宏参数的分隔标记逐字符匹配的,因此 \end {verbatim} 根本不会被识别,TeX 会一直读到文件末尾,然后以 Runaway argument? 加上 ! File ended while scanning use of \@xverbatim. 停下来。会整理空白的源码格式化工具,正好容易制造这个故障。如果需要显示空格的数量,可以用带星号的 verbatim* 环境,它会把每个空格打印成 ␣。
\verb 的分隔字符怎么选,以及它为何不能跨行
要在一行中间插入一小段逐字内容,使用 \verb。在 \verb 后面紧跟一个分隔字符,写上要原样输出的文字,再用同一个字符收尾——形如 \verb|\textbf{x}|。分隔字符几乎可以是任何 不出现在内容中的字符;如果内容里有 |,就换成 \verb!...!、\verb+...+ 或 \verb/.../。只有两类字符不能选。字母不行,因为写成 \verbx 之后 TeX 会把它读成另一个命令名。* 也不行,因为 \verb* 已被保留为把空格打印成 ␣ 的星号形式。
The macro \verb|\textbf{...}| sets bold text;
a pipe in the content needs another delimiter, as in \verb!a|b!.
Count the gaps: \verb*|a b| prints the spaces as visible marks.\verb 还有一条硬性限制:收尾的分隔字符必须在同一行上。 如果先到达行尾,编译会停在 ! LaTeX Error: \verb ended by end of line. 真正的原因通常是忘了收尾字符,但另一种情况出人意料地常见:编辑器自动换行或格式化把一行长文本拆成了两行。内容一长,就应该从 \verb 换到块级环境。另外,如果只是要处理容易包含 ~、#、%、_ 的字符串(典型如 URL),那么 url 或 hyperref 宏包的 \url{...} 更合适:它既是逐字的,又会在合适的位置断行。
在正文里打出反斜杠或下划线
如果只是想打出一两个字符,根本不必动用 verbatim。反斜杠用 \textbackslash,下划线用 \_。这里最常见的事故是 \\ 并不是反斜杠,而是换行命令:写 \\ 不会打印出任何字符,只会在此处折行。数学模式的 $\backslash$ 确实能出现这个形状,但用的是数学字体,所以正文中正确的写法是 \textbackslash。直接写裸的下划线会得到 ! Missing $ inserted.——对 TeX 来说 _ 表示「后面是下标」——写成 file\_name 即可解决。
| 写法 | 输出 | 注意 |
|---|---|---|
\textbackslash | \ | \\ 是换行命令,不会输出反斜杠 |
\_ | _ | 裸的 _ 会引发 ! Missing $ inserted. |
\% \& \# \$ | % & # $ | 每个只要前面加一个 \ 即可 |
\{ \} | { } | 把分组符号当作字符输出 |
\textasciitilde | ~ | 裸的 ~ 是不可断行的空格,而不是波浪号 |
\textasciicircum | ^ | 裸的 ^ 表示「后面是上标」 |
为什么 \verb 放进 \section、\footnote 里会失败
\verb 和 verbatim 环境 都不能出现在另一条命令的参数里。原因不是什么禁令,而是简单的先后次序。\verb 在读取自己的文字之前才切换类别码,但 \section{...} 的内容 在 \section 被调用的那一刻,就已经按通常的类别码转换成了 token 序列。等轮到 \verb 时,\foo 早已不是「要打印的四个字符」,而是「名为 \foo 的命令」。verbatim 无法重新读一遍已经读过的东西——这就是全部原因。
麻烦的是,这类失败给出的报错里根本不会出现 verbatim 这个词。\section{The \verb|\foo| command} 会停在 ! Undefined control sequence.——因为 \foo 确实被当成命令读了,而这个命令并不存在。如果内容里一个特殊字符都没有,例如 \mbox{\verb|abc|},则会得到少见的、比较友好的 ! LaTeX Error: \verb illegal in argument. 块级环境更难解读:\footnote{\begin{verbatim} ... \end{verbatim}} 会给出 Runaway argument? 加 ! Paragraph ended before \@xverbatim was complete.;放在 \parbox{5cm}{...} 里会变成 ! Argument of \@xverbatim has an extra }.;放在 \caption{...} 里则是 ! Argument of \@caption has an extra }. 它们都是同一个原因的不同面孔。
另一方面,表格单元并不是参数。tabular 的每个单元是在 TeX 盯着列分隔与行分隔的过程中读入的,因此 \verb 在那里照常可用,p{4cm} 列也一样。但在同一个表格里,\multicolumn{2}{c}{...} 的第三个参数确实是参数,放在那里就会失败。所以要记的规则不是「表格里不能用」,而是 「花括号包起来的参数里不能用」。
绕过的办法有三种。第一种是 cprotect 宏包(Bruno Le Floch,v1.0e),它的全部职责就是让 verbatim 能进入宏参数:在出问题的命令前加 \cprotect,于是 \cprotect\section{The \verb|\foo| command} 就能编译。它还附带 \cprotEnv,用于保护环境的 \begin。第二种是 fancyvrb 的 \SaveVerb / \UseVerb:先把逐字文本用一个名字保存起来,在参数里只调用这个名字。第三种专门针对脚注——在导言区声明 fancyvrb 的 \VerbatimFootnotes,\footnote 里就能用逐字内容了。注意,把 \verb 换成 fancyvrb 的 \Verb 并不能解决问题,它有着完全相同的时序困境。最后,如果只是一个标题,直接手写 \texttt{\textbackslash foo} 反而是最短的路。
% Fails: \foo was already a command token before \verb could act
% \section{The \verb|\foo| command} -> Undefined control sequence
% Workaround 1 -- cprotect
\usepackage{cprotect}
\cprotect\section{The \verb|\foo| command}
% Workaround 2 -- save it first, use it later
\usepackage{fancyvrb}
\SaveVerb{cmd}|\foo|
\section{The \UseVerb{cmd} command}
% Workaround 3 -- verbatim inside footnotes
\VerbatimFootnotes整个文件读进来:\verbatiminput 与 \VerbatimInput
在导言区写上 \usepackage{verbatim},正文里写 \verbatiminput{hello.py},这个外部文件的每一行都会被逐字排版。与把代码复制进稿件不同,只要修改源文件,PDF 就会自动跟上,代码和文档不会各走各的。如果你要展示的是真正能运行的代码,这是最稳妥的做法。
不妨问一句:既然标准的 verbatim 环境已经存在,为什么还需要一个叫 verbatim 的宏包?答案并不是 \verbatiminput。这个由 Rainer Schöpf 编写、属于 LaTeX Tools 套件的宏包,其文档把动机说得很清楚:内建环境必须把 \end{verbatim} 之前的全部内容当作一个宏参数读完,才能输出哪怕一行,因此长清单可能撑爆 TeX 的内存。宏包把实现换成 一次读一行 并随即排版(其文档说这个手法借自 AMS-TeX 的 \comment 宏);一旦能逐行读取逐字内容,\verbatiminput 基本上就是顺带的产物。有一个可见的副作用:写在 \end{verbatim} 同一行后面 的文字,内建环境会打印出来,宏包版则会悄悄丢弃。这个差异是有意为之,并写在文档里。
\usepackage{verbatim}
% ...
\verbatiminput{hello.py}
\begin{comment}
This paragraph is skipped entirely -- not printed, not typeset.
\end{comment}同一个宏包还提供 comment 环境,它会整段跳过 \begin{comment} 到 \end{comment} 之间的内容——不是逐字输出,而是 完全不输出,适合把草稿段落暂时搁置。若想更精细地控制文件读入,可用下一节 fancyvrb 提供的 \VerbatimInput[options]{filename}。它不像 \verbatiminput 那样只能整篇读入,可以接受边框与行号,还能用 firstline=10, lastline=25 只截取文件的一部分,正适合长源码里只想展示一小段的情形。
alltt:仍能执行少数命令的逐字环境
想给代码例子的 某一部分 加粗或上色时,普通 verbatim 束手无策,因为所有命令都被关掉了。这时可以用标准 LaTeX 附带的 alltt 宏包 中的 alltt 环境。alltt 与 verbatim 一样按输入原样、用等宽字体排版,但 有三个字符保留通常含义:反斜杠 \ 与花括号 {、}。于是外观仍是逐字的,内部却能执行 LaTeX 命令。
\usepackage{alltt}
% ...
\begin{alltt}
def \textbf{greet}(name):
return "Hi, " + name \textit{# a comment}
\end{alltt}在这个例子里,函数名 greet 变成粗体,注释变成斜体,其余部分仍按输入原样保留。代价很清楚:要把 \、{、} 这三个字符 作为字符 打印出来,就必须写成 \textbackslash、\{、\},而 verbatim 环境会直接输出它们。所以 alltt 是一笔交易:拿三个字符的逐字性,换来对其余内容排版的权利。想手动加一点强调就用 alltt,一个字符都不许动就用 verbatim。
加行号和边框:fancyvrb 的 Verbatim 环境
行号和边框,内建的 verbatim 都给不了。承担这项工作的是 fancyvrb 宏包,其核心是 大写 V 开头的 Verbatim 环境——它和小写的 verbatim 是两回事。选项可以逐个环境给出,如 \begin{Verbatim}[numbers=left, frame=single],也可以在导言区写 \fvset{numbers=left, ...} 作为全文默认值。fancyvrb 由 PSTricks 的作者 Timothy Van Zandt 于 1992 年动笔,自 2000 年起由 Herbert Voß 维护(TeX Live 2024 附带的是 v4.5c)。三十年积累下来的实用选项,正是它几乎什么需求都能应付的原因。
| 选项 | 常见值 | 作用 |
|---|---|---|
numbers | none / left / right | 行号位置,默认 none;numbersep 调整与正文的间距 |
frame | none / single / lines / leftline / topline / bottomline | 边框类型,默认 none;framerule 控制线宽,framesep 控制内边距 |
fontsize | \small、\footnotesize 等 | 字号,默认与正文相同 |
showspaces | true / false | 把空格显示为可见符号;showtabs 对应制表符,tabsize 控制宽度 |
firstline / lastline | 整数 | 配合 \VerbatimInput 只截取文件的一部分 |
commandchars | 例如 \\\{\} | 指定转义字符与两个分组字符,从而在逐字内容中重新启用命令 |
\usepackage{fancyvrb}
\fvset{fontsize=\small} % document-wide default
% ...
\begin{Verbatim}[numbers=left, frame=single]
def greet(name):
return "Hello, " + name
\end{Verbatim}
% only lines 10-25 of an external file, framed
\VerbatimInput[firstline=10, lastline=25, frame=lines]{server.py}指定 commandchars=\\\{\} 之后,在逐字内容内部,\、{、} 会重新作为转义字符和分组符起作用,于是可以像 alltt 那样嵌入命令。与其每次都写同一组选项,更地道的做法是 自定义一个环境:\DefineVerbatimEnvironment{Code}{Verbatim}{numbers=left, frame=single},此后只需写 \begin{Code}。不过本页所有工具都只负责「原样输出」,都不会给关键字上色,也就是都不做 语法高亮。若要展示带颜色、经过排版的源码,那是 listings(仅用 TeX 宏着色)或 minted(交给 Python 的 Pygments)的职责——相关页面「代码清单」中对两者做了比较。