介绍

$\LaTeX$ 文学编程的思想最初由 Donald Knuth 提出并使用,现如今,不计其数的宏包开发者都以文学编程的方式编写发布他们的作品。简单说来,文学编程就是把 $\LaTeX$ 代码和文档放在一起,置于同一个源文件之中。

举个例子,假如你现在需要编写一个宏包(名为 demo),如果不按文学编程的路子走的话,你至少需要准备两个文件:一个是宏包源文件 demo.sty (用户通过 \usepackage{demo} 调用此文件),另一个是用户文档(或者称为用户手册)demo.pdf(通过 demo.tex 编译生成)。而通过文学编程,我们可以将 demo.stydemo.tex 结合起来放在同一个源文件 demo.dtx 之中(源文件的文件后缀名一般为 .dtx,取自 documented latex)。

文学编程的工具 —— doc 宏包

介绍与演示

一般而言,.dtx 源文件主要包含两部分内容:代码和文档(code and documentation),代码部分不以 % 开头,而文档部分都以 % 开头。

在编写 .dtx 源文件的过程中我们要用到 doc 宏包提供的各种命令及环境,此宏包由 Frank Mittelbach 编写。这里我们先从 doc 宏包中取出五个主要的命令与环境用于演示,它们是:

  • 命令
    • \DescribeMacro:接受一个宏参数,表示现在要介绍该宏的用法。将该宏名打印在边注区,并创建索引条目。
    • \DescribeEnv:接受一个环境名参数,表示现在要介绍该环境的用法。将该环境名打印在边注区,并创建索引条目。
  • 环境
    • macro:接受一个宏参数,表示现在要解释该宏的定义。将该宏名打印在边注区,并创建索引条目。
    • environment:接受一个环境名参数,表示现在要解释该环境的定义。将该环境名打印在边注区,并创建索引条目。
    • macrocode:此环境中的代码一码两用,既可以写入目标文件,又可以逐字抄录进入文档。

上面这些解释你可能听起来比较晕,不要急,看完下面的示例你就会明白具体的意思!

下面写一份实例代码 (文件名为 demopkg.dtx):

% \section{User's Manual}
% 
% In this section we will describe the usage of various commands and environments
% provided by this package.
%
% \DescribeMacro{\mycmd}
% Describe the usage of the macro \verb|\mycmd|.
%
% \DescribeEnv{myenv}
% Describe the usage of the environment \verb|myenv|.
%
% \section{Implementation}
%
% \begin{macro}{\mycmd}
% Describe the definition of the macro \verb|\mycmd|.
%    \begin{macrocode}
\newcommand{\mycmd}{This is a \LaTeX{} command}
%    \end{macrocode}
% \end{macro}
% \begin{environment}{myenv}
% Describe the definition of the environment \verb|myenv|.
%    \begin{macrocode}
\newenvironment{myenv}%
  {This is a \LaTeX{} environment}%
  {}
%    \end{macrocode}
% \end{environment}

上面这段代码虽然简短(只有两个 section),但却反映出了很多宏包 .dtx 文件的基本框架,第一小节为 User’s Manual,用于向用户解释宏包的用法;第二小节为 Implementation,用于记录宏包的具体实现细节。 为了演示,这段代码只定义了一个命令 \mycmd 和一个环境 myenv,事实上,可以直接在导言区 \input{demopkg.dtx} 来使用 \mycmd 命令和 myenv 环境(你不妨试试)。

关于 macrocode 环境需要说明:

  • %\end{macrocode} 之间必须有四个空格符
  • \begin{macrocode}\end{macrocode} 之间的代码行不应该以 % 开头

获取 PDF 文档

为了得到 PDF 文档,有两种方法:

  • 方法一:创建独立的驱动文件(将其中的代码称为驱动代码),一个简短的驱动文件(假设文件名为 demopkg.tex)如下:

    \documentclass{article}
    \usepackage{doc}
    \CodelineIndex
    \begin{document}
      \DocInput{demopkg.dtx}
    \end{document}
    

    使用 $\LaTeX$ 格式1编译 demopkg.tex 就可以得到文档 demopkg.pdf:

    demopdf
    编译生成 demopkg.pdf

    一般而言我们不会用「创建独立的驱动文件」这种方式,而是用下面的主流

  • 方法二:将驱动文件并入 demopkg.dtx 之中:

     % \iffalse
     \documentclass{article}
     \usepackage{doc}
     \CodelineIndex
     \begin{document}
       \DocInput{demopkg.dtx}
     \end{document}
     % \fi
     %
     % \section{User's Manual}
     % 
     % In this section we will describe the usage of various commands and environments
     % provided by this package.
     %
     % \DescribeMacro{\mycmd}
     % Describe the usage of the macro \verb|\mycmd|.
     %
     % \DescribeEnv{myenv}
     % Describe the usage of the environment \verb|myenv|.
     %
     % \section{Implementation}
     %
     % \begin{macro}{\mycmd}
     % Describe the definition of the macro \verb|\mycmd|.
     %    \begin{macrocode}
     \newcommand{\mycmd}{This is a \LaTeX{} command}
     %    \end{macrocode}
     % \end{macro}
     % \begin{environment}{myenv}
     % Describe the definition of the environment \verb|myenv|.
     %    \begin{macrocode}
     \newenvironment{myenv}%
       {This is a \LaTeX{} environment}%
       {}
     %    \end{macrocode}
     % \end{environment}
    

    此时我们直接编译 demopkg.dtx 文件,第一次读取 demopkg.dtx 时,% 为正常的注释符,由于这个时候驱动代码不在 % 后面,所以驱动代码会被执行,从 \documentclass{article} 开始,到 \end{document} 结束。在这个过程当中,第六行的 \DocInput{demopkg.dtx} 会二次读取文件 demopkg.dtx,但是这个时候,% 会被忽略掉,因此我们需要在驱动代码块上下方分别添加 \iffalse\fi 以避免驱动代码块二次读入。

条件代码(Conditional Code)

基本介绍

由于 doc 系统最开始并不能满足 $\LaTeX$ 开发者的一些需求,比如:从一个源文件中有条件性地抽取代码生成多个目标文件或者从多个源文件中抽取代码生成一个目标文件。于是乎,doc 系统从两个方面进行了改进:

  • 建立语法用于标记代码块,这些代码块可以被单独引用
  • 拓展 DocStrip 程序,提供了专门的脚本语言,用于根据标记代码生成目标文件

标记代码块的方式为:

%<*tagname>
    some lines of the code
%</tagname>

%<tagname> one line of code

其中第一种方式用于标记多行代码2,第二种方式用于标记单行代码。tagname 是标签名,按照习惯,一般使用 driver 标记驱动代码、package 标记宏包代码、class 标记文档类代码等,你会在 .dtx 文件中经常见到这些标签名。但是请注意,使用这些标签名只是习惯而已,自己标记代码块的时候可以随便选取标签名。

我们还可以利用 | (逻辑或)、& (逻辑且) 和 ! (逻辑否) 将多个标签名串起来,例如:

%<*Aname|Bname&!Cname>
    some lines of code
%</Aname|Bname&!Cname>

它表示:如果 tag-list 中 (见下面的 \generate)

  • 含有 Aname,或者
  • 含有 Bname 但不含 Cname

这段代码就会被引用。

举例

Example 1: classes.dtx
%<*10pt|11pt|12pt>
\if@twocolumn
  \setlength\parindent{1em}
\else
%<10pt>  \setlength\parindent{15\p@}
%<11pt>  \setlength\parindent{17\p@}
%<12pt>  \setlength\parindent{1.5em}
\fi
%</10pt|11pt|12pt>

这是一个典型的嵌套式标记代码块,其中

\if@twocolumn
  \setlength\parindent{1em}
\else
\fi

这四行代码被 10pt11pt12pt 共用,即,只要 tag-list 中含有 10pt11pt12pt 之一,就会引用这四行代码,然后剩下的三行代码就是嵌套在里面的标记代码块,用于在单栏排版中根据不同的文档类选项设置不同的 \parindent。例如如果 tag-list 中的标签为 10pt,那么抽取出的代码就是

\if@twocolumn
  \setlength\parindent{1em}
\else
  \setlength\parindent{15\p@}
\fi

DocStrip 工具

介绍

DocStrip 程序最开始是由 Frank Mittelbach 通过底层 $\TeX$ 语言编写的,后来 Denys Duchier、Johannes Braams、Marcin Woliński、Mark Wooding 等人对其进行了改进。DocStrip 程序的主要功能有:

  • 从源文件中剥离文档部分,即,以单个 % 开头的代码行(标签行除外)
  • 从源文件中生成目标文件,如 .sty 文件、.cls 文件
  • 自动将生成的文件安装至指定的目录中

使用 DocStrip 程序

方式一

直接在命令行交互式执行 DocStrip 程序

tex/latex docstrip.tex

上面的 / 表示二选一,回车后,$\LaTeX$ 会询问几个问题,用户依次回答之后,DocStrip 会据此完成它的工作。此方式只适用于从源文件中剥离文档部分,现在不常用。

方式二

写一个脚本文件(后缀名一般为 .ins),脚本文件的一般形式为:

\input docstrip
<other DocStrip commands>
\endbatchfile

注意第一行使用的是 $\TeX$ 原语 \input,这意味着我们既可以用 $\LaTeX$ 格式运行脚本文件,也可以用 $\TeX$ 格式运行脚本文件。

<other DocStrip commands> 中的核心命令是 \generate

\generate{\file{result-file-1}{\from{source-file-1}{tag-list-1}
                               \from{source-file-2}{tag-list-2}
                               ...
                               \from{source-file-n}{tag-list-n}}
          ...
          \file{result-file-n}{...}
         }

\generate 的参数可以含多个 \file\file 的第二个参数可以含多个 \fromtag-list 就是由前面所说的标签名构成的列表(标签名由逗号隔开)。举个例子:

\generate{\file{demopkg.sty}{\from{demopkg.dtx}{package}}}

表示从 demopkg.dtx 中抽取被 package 标签标记的代码块以及未被标记的代码块,将所有这些代码块串联起来生成宏包文件 demopkg.sty

现在我们创建一个脚本来处理上面的 demopkg.dtx 源文件,将脚本文件命名为 demopkg.ins,内容如下:

\input docstrip.tex
\keepsilent
\askforoverwritefalse
\generate{
  \file{demopkg.sty}{\from{demopkg.dtx}{package}}
}
\endbatchfile

关于 \keepsilent\askforoverwritefalse 等命令的意义,请查阅 DocStrip 手册。

然后 demopkg.dtx 文件也需要稍作修改:

% \iffalse
%<*driver>
\documentclass{article}
\usepackage{doc}
\CodelineIndex
\begin{document}
  \DocInput{demopkg.dtx}
\end{document}
%</driver>
% \fi
%
% \section{User's Manual}
% 
% In this section we will describe the usage of various commands and environments
% provided by this package.
%
% \DescribeMacro{\mycmd}
% Describe the usage of the macro \verb|\mycmd|.
%
% \DescribeEnv{myenv}
% Describe the usage of the environment \verb|myenv|.
%
% \section{Implementation}
%
%    \begin{macrocode}
%<*package>
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{demopkg}
%    \end{macrocode}
% \begin{macro}{\mycmd}
% Describe the definition of the macro \verb|\mycmd|.
%    \begin{macrocode}
\newcommand{\mycmd}{This is a \LaTeX{} command}
%    \end{macrocode}
% \end{macro}
% \begin{environment}{myenv}
% Describe the definition of the environment \verb|myenv|.
%    \begin{macrocode}
\newenvironment{myenv}%
  {This is a \LaTeX{} environment}%
  {}
%</package>
%    \end{macrocode}
% \end{environment}

可以看到,我们用 driver 标签标记了驱动代码,在第二节的开头添加了宏包必不可少的标识节3,然后用 package 标签标记了第二节(因为宏包代码都在这一节之中),然后命令行执行:

tex demopkg.ins

或者

latex demopkg.ins

DocStrip 执行任务之后,便会生成宏包文件 demopkg.sty

问:如果不用 driver 标记驱动代码会怎么样? 驱动代码块也会被写入 demopkg.sty 之中,这可不是我们想要的结果。

方式三

部分项目如 ctexwrapstuff 将脚本 .ins 文件合并到 .dtx 文件中,其内容看起来虽然更加复杂,但是核心框架容易分析出来:

% \iffalse meta-comment
% 一些魔法注释,如 !TeX program = XeLaTeX
%<*internal>
\iffalse
%</internal>
%<*readme>
项目的 README 文件内容
%</readme>
%<*internal>
\fi
\begingroup
  \def\temp{LaTeX2e}
\expandafter\endgroup\ifx\temp\fmtname\else
\csname fi\endcsname
%</internal>
%<*install>
\input ctxdocstrip %
\generate{...}
\endbatchfile
%</install>
%<*internal>
\fi
%</internal>
%<*driver>
\documentclass{ltxdoc} ^^M 或者其它文档类,如 ctex.dtx 使用的 ctxdoc
\begin{document}
  \DocInput{\jobname.dtx}
  \PrintChanges
  \PrintIndex
\end{document}
%</driver>
% \fi
% code and documentation ...

使用 plain $\TeX$ 格式运行 .dtx 文件会启动 DocStrip 程序,生成目标文件(根据 \generate 里的具体指令);使用 latex 格式运行会生成 PDF 手册。具体分析见 David Carlisle 在 TSE 的回答。

注释

参考

  1. 编译命令有 latexpdflatexxelatexlualatex,对于纯英文推荐使用 pdflatex,对于中英文混排则推荐使用 xelatex 或者 lualatex。 

  2. 注意:第一种方式中的 %<*tagname>%</tagname> 必须单独各占一行。 

  3. 见 clsguide