HTML 编写 Man Pages

作者:Michael Hamilton

Web 入侵互联网的步伐持续加快。在短短几年内,HTML 已成为最广泛支持的文档格式之一。目前,成千上万的人正在努力将较旧的信息源重新格式化,以便在 Web 上展示。所需的技术相对简单易懂,而且组装成本不高。Linux 是理想的开发和交付环境:成本低、可靠,并且必要的软件包含在几个相互竞争的 Linux 发行版中。

在本文中,我将讨论我为在新媒体中提供旧文档而编写的解决方案——自动翻译。要转换的文档是 Unix man pages。Man pages 是高度结构化的文档,其中主要标题和对其他文档的引用很容易识别。构成 man page 系统的文件已经使用正式的命名系统组织成严格的层次结构;因此,通过 Web 交付整个现有的 man 系统和文档格式,而无需重新组织内容或整体结构非常容易。我组装了一些软件,可以将旧的 Unix 手册格式翻译成 HTML,同时保留旧的样式和组织。Web 中存在的技术允许进一步增强功能:文档可以交叉链接;可以自动生成索引的替代形式;并且可以进行全文搜索。

我将我组装的软件包称为 vh-man2html。它被设计为从 Web 服务器激活的一组 CGI(通用网关接口)脚本——驱动 HTML 表单的相同技术。这意味着 vh-man2html 可用于向 LAN 或 Internet 上的其他主机提供 man pages。vh-man2html 的主要组件是 Richard Verhoeven 的 man2html 翻译器,并由几个脚本增强以生成索引和方便搜索。vh-man2html 还有一些支持脚本,允许您从 Unix 命令行驱动 Netscape,以便 vh-man2html 实际上可以替换包含 Netscape 的系统上的 “man”。

它看起来像什么?

如果您可以访问 Web,您可以在以下位置查看 vh-man2html 的实际效果:

http://www.caldera.com/cgi-bin/man2html

Caldera 1.0 的 man pages 在线提供。

Writing Man Pages in HTML

图 1:主 vh-man2html 网页

Writing Man Pages in HTML

图 2:vh-man2html 生成的 Man Page

Writing Man Pages in HTML

图 3:名称-描述索引的一部分

Writing Man Pages in HTML

图 4:仅名称索引的一部分

Writing Man Pages in HTML

图 5:全文搜索结果

图 1 显示了主 vh-man2html 网页,该网页提供对单个 man pages 或三种不同类型的索引的直接访问:名称-描述、仅名称和全文搜索。在主窗口中,您可以输入 man page 名称、man page 名称和节号,或者通过指定层次结构或完整路径名来进一步缩小范围。

图 2 显示了 man2html 转换器生成的 man page。该转换器已将 man 格式标记转换为大致等效的 HTML 标记。它还为任何对其他 man pages 的引用创建了 HTTP 引用和链接。转换器还生成主题标题索引,这在阅读较大的 man pages 时很有用。文本高亮和字体更改已正确翻译,如果存在表格,它们将被翻译成 HTML 表格。目前,man2html 不翻译 eqn 描述的方程式,但由于很少有 man pages 使用 eqn,因此这不是主要缺点。

图 3 显示了 man pages 第 1 节的名称-描述索引的一部分。该索引包括一个字母子索引和指向手册其他部分的链接。图 4 中的仅名称索引与名称-描述索引类似,只是它更紧凑。

图 5 显示了对包含 “cdrom” 引用的页面进行全文搜索的结果。将生成指向每个匹配页面的链接。

这些图说明了通过 Web 提供 man pages 的主要优势之一:各种访问路径可以以集成形式呈现——一站式界面。

获取和安装 vh-man2html

我的开发平台是 Caldera 1.0,以及 Red Hat 3.0.3 升级版。如果您没有基于 Red Hat 的系统,您仍然可以成功使用 vh-man2html。如果您的系统使用未格式化的 man page 源并且您运行 HTTP 守护程序,vh-man2html 应该仍然可以工作;但是,您可能需要重新配置它并重建二进制文件以匹配您自己的设置。

不要因为必须运行 HTTP 守护程序而劝退您。您可以通过将 HTTP 守护程序限制为仅服务于您自己的主机来处理此过程的安全性方面。我使用 Caldera 和 Red Hat 附带的 Apache HTTP 守护程序,所以我只需调整系统 /etc/httpd/conf/access.conf 文件中的相应行,以防止从我的家庭网络外部访问

<Limit GET>
order allow,deny
allow from .pac.gen.nz
deny from all
</Limit>

(您还可以指定应将访问限制为特定的 IP 地址。)此外,我的系统配置了内核防火墙,这提供了额外的保护层。

运行 HTTP 守护程序的性能方面是最小的。大部分时间它都处于空闲状态——如果其他作业需要守护程序占用的内存,内核只需将其迁移到交换空间。为了最大限度地减少启动活动量和总内存消耗,我通过编辑 /etc/httpd/conf/httpd.conf 并更改以下内容,将备用守护程序的数量减少到一个

MinSpareServers 1
MaxSpareServers 1
StartServers 1

这对于我的家庭网络来说似乎很好,在我的家庭网络中,任何时候最多只有两个用户处于活动状态。

vh-man2html 以 Red Hat 软件包格式提供,源代码和 i386 ELF 二进制文件形式均可从以下位置获取:

ftp://ftp.caldera.com/pub/contrib/RPMS\
        /vh-man2html-1.5-1.src.rpm
ftp://ftp.redhat.com/pub/contrib/SRPMS\
        /vh-man2html-1.5-1.src.rpm
ftp://ftp.caldera.com/pub/contrib/RPMS\
        /vh-man2html-1.5-2.i386.rpm
ftp://ftp.redhat.com/pub/contrib/RPMS\
        /vh-man2html-1.5-2.i386.rpm

请注意,ftp.redhat.com 在 ftp.caldera.com 上镜像。

此外,包含 ELF 二进制文件的源 tar 文件可从以下位置获取:

ftp://sunsite.unc.edu/pub/Linux/system\
        /Manual-pagers/vh-man2html-1.5.tar.gz

此外,Christoph Lameter,clameter@waterf.org,修改了 vh-man2html 以用于 Linux Debian Distribution man pages。他的版本作为 man2html 软件包在任何 Debian 存档的 doc 目录中提供。

rpm 版本将正确安装在任何 2.0.1 以后的基于 Red Hat 的系统(包括 Caldera)中。以 root 身份登录后运行以下命令将安装二进制 rpm

rpm -i vh-man2html-1.5-2.i386.rpm

安装后,您可以启动 Web 浏览器并使用以下 URL 对其进行测试

https:///cgi-bin/man2html
如果您没有禁用 HTTP 守护程序,这将显示一个启动屏幕,您可以在其中输入 man page 的名称或按照链接访问各种 man 索引页面。

您可以使用浏览器将此页面另存为书签。如果您觉得编辑 HTML 文件很舒服,您可以将其插入到您自己系统的主文档中。在我的情况下,我编辑了系统的顶层文档

/usr/doc/HTML/calderadoc/caldera.html

并在文档的适当位置添加了以下行

<H3>
<A HREF="https:///cgi-bin/man2html">
<IMG SRC="book2.gif">
Linux Manual Pages
</A>
</H3>
Red Hat 用户将编辑
/usr/doc/HTML/index.html
并将以下内容添加到可用文档列表中
<LI><A HREF="https:///cgi-bin/man2html">
Linux Manual Pages</A>
<P>
vh-man2html 使用现有 man 安装中的一些文件。它使用 Unix “man -k” 命令使用的 “whatis” 文件作为名称-描述列表。这些文件由 makewhatis 命令构建。Caldera 和 Red Hat 系统通常每天清晨构建 whatis 文件。如果这些文件从未运行过(可能是因为您在晚上关闭机器),您可以通过以 root 用户身份登录并输入以下内容来构建它们
/usr/sbin/makewhatis -w
请注意,Caldera 1.0 中的标准 makewhatis 在我的 486DX2-66 上大约需要 30 分钟。我有一个修改后的 makewhatis 版本,它可以在短短 1.5 分钟内完成完全相同的工作。我的修改版本现在作为 man-1.4g 的一部分以 rpm 和 tar 格式提供,网址为
ftp://ftp.redhat.com/redhat-3.0.3/i386\
        /updates/RPMS/man-1.4g-1.i386.rpm
ftp://sunsite.unc.edu/pub/Linux/system/\
        Manual-pagers/man-1.4g.tar.gz
由于传统的 Unix man 程序不提供搜索手册页面的全文,我想将此功能添加到 vh-man2html 中。输入 Glimpse,这是一个由亚利桑那大学计算机科学系的 Udi Manber 和 Burra Gopal 以及台湾国立中正大学的 Sun Wu 创建的免费程序。Glimpse 是一个文本文件索引和搜索系统,它通过使用预先计算的索引来实现快速搜索速度。索引通常安排在凌晨进行,此时它不会影响用户。

要使用 Glimpse 全文搜索,您必须在 /usr/bin 中安装程序 Glimpse。Red Hat rpm 用户可以从以下位置获取 Glimpse

ftp://ftp.redhat.com/pub/non-free\
        /Glimpse-3.0-1.i386.rpm

Glimpse 主 ftp 站点是

ftp://ftp.cs.arizona.edu/Glimpse/
可以在其中找到最新的源代码和预构建的二进制文件(包括 Linux)的 tar 格式。请注意,Glimpse 不可免费再分发用于商业用途。我对听到任何限制较少的替代方案非常感兴趣。安装 Glimpse 后,您需要构建 Glimpse 索引。vh-man2html 希望此索引位于 /var/man2html 中。构建索引不需要很长时间——在我的机器上大约需要三分钟。以 root 身份输入
/usr/bin/Glimpseindex -H /var/man2html \
        /usr/man/man* /usr/X11R6/man/man*\
        /usr/local/man/man* /opt/man/man*
chmod +r /var/man2html/.Glimpse*
在 Red Hat 上,这可以设置为 /etc/crontab 中的 cron 作业,例如,(以下内容必须全部在一行上)
21 04 * * 1 root /usr/bin/Glimpseindex -H
   /var/man2html /usr/man/man* /usr/X11R6/man/man*
   /usr/local/man/man* /opt/man/man*i;
   chmod +r /var/man2html/.Glimpse*
如果您不想使用 Glimpse,您可以编辑软件包在 /home/http/html/man.html 中安装的 man.html 文件,并删除对全文搜索和 Glimpse 的引用。也可以编辑此文件以包含您希望包含的任何本地说明。

如果您要从源代码构建 vh-man2html,您将必须手动解压缩它并更改 Makefile 以指向您所需的安装目录,然后再发出 make install。您也可以使用 rpm2cpio 实用程序从 rpm 中提取 CPIO 存档,在这种情况下,您可以读取软件包 spec 文件以了解将内容放在何处。

如果您不想使用 HTTP 守护程序并且您懂一点 C 语言,您可以考虑使用脚本和 C 程序预先翻译和预先索引所有 man pages。然后可以直接引用它们,而无需 HTTP 守护程序按需调用转换。

技术概述

本节将介绍 vh-man2html 的一些实现细节。它非常简短,实际上旨在指出 CGI 脚本是任何具有少量编程知识的人都可以成功完成的事情。

在不深入探讨 CGI 脚本教程的情况下,CGI 脚本是由远程 HTTP 守护程序(即 Web 服务器)执行的程序。当您跟踪与其名称匹配的 HTTP 链接时,Web 浏览器可能会导致远程 Web 服务器运行 CGI 程序。例如,将 Web 浏览器指向

http://www.caldera.com/cgi-bin/man2html

在 Caldera 的 Web 服务器上执行 cgi-bin/man2html。Web 服务器准备运行的 CGI 程序通常仅限于服务器上 cgi-bin 目录中找到的程序。

CGI 脚本可以通过将文档写入其标准输出来将输出返回给远程调用者。输出文档的开头必须包含描述其内容的小文本标头。在 man2html 的情况下,返回的内容是 HTML 页面。列表 1 显示了 man page 到 HTML 转换器的 HTML 输出;标头行是

content-type: text/html

文档的其余部分是普通的 HTML,它由用 HTML 标记标记的文本组成。对于某些 Web 浏览器,您可以使用 Netscape 的 “查看文档源代码” 等选项来检查此 HTML 源代码。

脚本可以创建一个 HTML 页面,其中包含对其他 CGI 脚本的进一步引用。在列表 1 中,以下引用将读者返回到主 vh-man2html 内容页面

<A HREF="http:/cgi-bin/man2html">Return to Main
Contents</A>

CGI 脚本接收输入,该输入可能已嵌入到原始引用中,或者可能是由于用户输入而添加的。例如,在列表 1 中,“另请参阅” 部分指示 cgi-bin/man2html 程序返回特定手册页面的 HTML

<A HREF="http:/cgi-bin/man2html?man1/from.1l">
from</A>
在这种情况下,HTTP 引用提供了一个参数 “man1/from.1l”——man page 的名称。参数列表的开头以 “?” 分隔。如果有多个参数,它们将用 “+” 号分隔(并且有一些约定用于传递特殊字符,例如 “+” 和 “?” 作为参数)。CGI 程序看不到任何分隔字符;它只是以参数形式在其正常参数列表(或可选地通过标准输入)中接收参数。这意味着 CGI 脚本不必关心其输入是如何通过网络传递的,它只是以命令行参数、标准输入以及各种环境变量的形式接收它。

除了单击引用之外,用户还可以将数据输入到输入字段中。CGI 程序在表单上引入输入字段的最简单方法是在其生成的 HTML 中包含 <ISINDEX> 标记。这会产生一个输入字段,如图 1 所示。如果用户在输入字段中输入任何内容并按下 return,服务器将重新运行 CGI 程序,并通过我们刚刚讨论的参数传递约定将输入传递给它。您还可以创建 HTML 表单,但我不会在此处讨论它们。

通过生成上面介绍的 HTML 引用类型,CGI 程序可以与远程用户执行复杂的交互。所有这一切的美妙之处在于,要入门,您唯一需要的技能就是能够使用您选择的语言编写相当简单的代码。您需要知道如何处理命令行参数并写入标准输出。您需要的其余知识可以从 Web 文档或任何有关 HTML 和 CGI 的书籍中免费获得。CGI 是一个真正有效的客户端-服务器。Python 和 Perl 等重型 CGI 编程语言具有工具和库来帮助您完成任务。

我还应该提到安全问题。如果您的 HTTP 守护程序可以被潜在的恶意用户访问,您的 CGI 脚本可能会为他们提供攻击您的途径。恶意用户可能会尝试向您的 CGI 脚本提供恶意参数。例如,通过使用反引号和分号等特殊 shell 字符,他们可能能够让脚本执行任意命令。防止这种情况的唯一方法是仔细检查所有输入参数中是否有任何可疑内容。例如,vh-man2html 可以传递 man page 的完整文件名;但是,它不仅仅接受并返回传递给它的任何文件名——它只接受 man 层次结构中存在的那些文件名。该程序还确保文件名不包含相对引用,例如 “..”(父目录),并删除任何可疑字符,例如反引号,这些字符可能用于将命令嵌入到参数列表中。在 C 等缺乏内存边界检查的语言中,输入参数的长度应限制在为它们分配的空间内。否则,调用者可能能够写入超出分配空间的其他数据,并更改程序的行为以获得他/她的优势(例如,将程序执行的命令从 gzip 更改为 rm)。为了帮助检查长输入参数是否会威胁 vh-man2html 的完整性,我借用了一台 SGI 机器上的一些时间,并使用 Parasoft 的 Insight 边界检查器构建了 vh-man2html。Insight 预处理 C 或 C++ 程序,添加数组边界检查、内存泄漏检测和许多其他检查。我提到 Insight 的原因之一是 Parasoft 的网站 http://www.parasoft.com/ 将 Linux 列为受支持的平台。

vh-man2html 包括四个 CGI 程序。它们都生成相互依赖的 HTTP 引用。

Man page 到 HTML 的转换由 man2html C 程序处理。Unix man pages 用 man 或 BSD mandoc 标记标记,这些标记是 nroff/troff 宏。该程序的大部分是一系列大型 case 语句和表查找,旨在处理所有可能的宏。

列表 2 显示了一个典型的 nroff/troff 标记的手册页面,它正在使用 man 宏包。宏使用句点,即句点,作为一到两个字符的宏名称的引导。troff/nroff 使用两个字符的宏名称——显然它们非常适合旧 Unix 平台(如 PDP11)的 16 位字长(至少我是这么被告知的)——man2html.c 仍然利用了这一技巧。某些宏可以直接转换为适当的 HTML 标记;例如,以 “.SH” 节标题开头的行直接转换为 HTML <H1> 标题。

许多 troff 标记将其参数和效果限制为仅一行,并且没有相应的结束标记——而许多等效的 HTML 构造也需要结束标记。例如,troff “.SH” 节标题标记后面的文本需要包含在一对 HTML 标题级别 1 标记中,例如 “<H1>文本</H1>”。其他范围较大的 troff 标记(例如多种类型的列表)同时具有开始和结束标记,这使得转换为 HTML 非常容易。

一个棘手的问题是处理一行上的多个 troff 标记;例如,暗示括号括起以下文本或字体更改的标记。为了正确放置括号,转换器可以在一行内递归工作。例如,BSD mandoc 序列用于名为 -b 且参数名为 bcc-addr 的命令选项在 troff 中表示为

 .Op Fl b Ar bcc-addr

这表示读者应该看到

[ -
其中 b 为粗体,bcc-addr 为斜体。相应的 HTML 是
[ -<B>b</B> <I>bcc-addr</I> ]
通过在命中 Op 标记时使用递归,我们可以在整行的开头和结尾获得方括号。

有些 troff 标记的效果由等级相等或更高的标记终止;在这些情况下,转换器必须记住其上下文并生成任何必要的终止 HTML。嵌套列表也是可能的。在这些情况下,man2html 必须维护一个未完成嵌套的堆栈,当遇到新的相等或更高级别的元素时,必须完成这些嵌套。

我钦佩 Richard 在系统地构建所有标记的翻译方面的奉献精神。添加 BSD mandoc 标记被证明是一次痛苦的经历,最终,唯一正确的方法是转换我可以找到的每个 BSD mandoc 页面,并将输出通过管道传输到 weblint(一个出色的 HTML 检查器)。例如,在 tcsh/csh 中

foreach i ( `egrep -l '^\.Bl' /usr/man/man1/* \
        /usr/man/man8/*` )
/home/httpd/cgi-bin/man2html $i > tmp/`basename $i`
end
weblint tmp/*

如果您想了解 mandoc 翻译的范围,请查看上述 egrep 定位的任何页面——telnet、lpc 和 mail 都是很好的例子。

man2html.c 还必须进行次要的翻译修复,例如将引号和其他特殊标点符号翻译成 HTML 特殊字符。

最后,为了测试翻译器的坚固性,我转换了所有 man pages

find /usr/man/man* -name '*.[0-9]' \
  -printf "echo %p; /home/httpd/cgi-bin/man2html\
        %p | weblint -x netscape -\n" | sh \
        |& tee weblint.log

这些测试被证明在暴露错误方面非常有用。

该程序还必须导航 man 目录层次结构并生成可能相关的页面列表(例如,在多个 man 层次结构中可能存在同名页面)。要咨询的 man 层次结构列表是从 /etc/man.config 中读取的,这是 Redhat 和 Caldera 附带的 man-1.4 软件包的标准配置文件。还咨询此配置文件以了解如何处理已使用 gzip 或其他压缩程序压缩的 man pages。

man2html 可以很容易地用 Python 或 Perl 编写,但是速度方面 C 语言是无与伦比的。man2html 在我的 486 上足够快,以至于我认为缓存其输出是不值得的——每个页面都只是按需重新生成。但是,如果我要从服务器为大量高频用户提供 man pages,我可能会预先生成所有 man pages 作为静态文档集。

两个 awk 脚本 manwhatis 和 mansec 为 man 节生成名称-标题和仅名称索引,并将它们缓存在 /var/man2html 中。manwhatis 定位并将 whatis 文件翻译成所需的节索引,并将其缓存在 /var/man2html 中。如果自缓存版本生成以来任何 whatis 文件已更新,它将重建缓存。该脚本按字母顺序分隔 whatis 文件,并构建 HTML 文档的字母索引,以便用户可以快速跳转到他们感兴趣的字母部分。

mansec 遍历 man 层次结构以构建名称列表;如果层次结构中的任何目录已更新,它将重建其缓存。mansec 必须使用 sort 命令将找到的名称按字母顺序排列。它还构建了一个字母快速索引,就像 manwhatis 一样。

manwhatis 和 mansec 都接受一个参数,该参数指示要索引的节。他们必须检查参数中是否有任何潜在的恶意内容,如果发现任何他们不期望的内容,则返回包含错误消息的文档

section = ARGV[1];           # must be 0-9 or all.
if (section !~ /^[0-9]$/ && section != "all") {
 print "Content-type: text/html\n\n";
 print "<head>";
 print "<title>Manual - Illegal section</title>";
 print "<body>";
 print "Illegal section number '" section "'." ;
 print "Must be 0..9 or all";
 print "</body>";
 exit;
}

mansearch 脚本是 Glimpse 搜索实用程序的 awk 脚本前端。它接受用户输入,并将其传递给 Glimpse,因此我必须小心地包含代码来检查输入的安全性,然后再调用 Glimpse。这基本上意味着排除任何 shell 特殊字符,或者确保通过适当地引用它们来防止它们执行任何操作。例如,在 awk 中,我们可以静默忽略任何我们不愿意接受的字符

# Substitute "" for any char not in A-Za-z0-9
# space.
string = gsub(/[^A-Za-z0-9 ]/, "", string);
I chose awk over Python and Perl mainly because it is small,
widely available and adequate for the task. Note that I'm using the post
1985 "new awk". For larger, more complex CGI scripts I'd probably use
Python (if I had to start again without Richard's work, I think
man2html would be a Python script).
In order to make vh-man2html usable remotely, I changed man2html and my
scripts to generate HTTP references that were relative to the current server.
For example, I used:
<A HREF="http:/cgi-bin/man2html">Return to Main
Contents</A>
而不是
<A HREF="https:///cgi-bin/man2html">Return to Main
Contents</A>
除了 “重定向” 之外,这都很好用。“重定向” 是 CGI 脚本输出的小文档。这是一个重定向示例
Location: http://sputnik3/cgi-bin\
        /man2html/usr/man/man1/message.1
重定向没有上下文,因此必须指定主机。当用户输入近似名称(例如 “message 1”)时,man2html 会生成重定向。重定向 将此更正 为完整引用,例如上面的引用。服务器名称是从 HTTP 服务器通常在调用 CGI 脚本之前设置的许多环境变量之一中获取的。
与 Netscape 接口

最近,我希望能够从命令行向已运行的 Netscape 浏览器发出 man page 请求——我想用 Netscape 替换命令行 man。您可能知道,启动 Netscape(或 emacs)等大型应用程序的成本过高,因此我的首选是与已运行的 Netscape 通信,而不是为每个请求启动一个新的 Netscape。有关从命令行驱动已运行的 Netscape 的说明,请访问

http://www.mcom.com/newsref/std/x-remote.html

您可以使用新的 Netscape 将命令传递给现有的 Netscape——它会在传递命令后退出,而不会启动任何屏幕——或者您可以使用较小的示例 C 程序(请参阅 HTTP 引用)来完成相同的工作。以下 bash 脚本片段将使用两者中的任何一个(如果较小的实用程序可用,则首选较小的实用程序)

function nsfunc () {
 # If this user is running netscape - talk to it
 if ( /bin/ps -xc | grep -q 'netscape$' ) ; then
  if [ -x netscape-remote ] ; then
   # Use utility to talk to netscape
   exec netscape-remote -remote "openURL($1,new_window)"
  else
   # Use netscape to talk to netscape
   exec netscape -remote "openURL($1,new_window)"
  fi
 else
  # Start a new netscape.
  netscape $1 &
 fi
}

bash 脚本可以按如下方式调用此函数
nsfunc "http:/cgi-bin/man2html?who+1"
通过使用类似的技术,系统可以将它们的帮助浏览器建立在 Netscape 之上。希望类似的功能将被构建到一些免费浏览器中以提供替代方案(例如,Lynx 是一款快速的基于文本的浏览器)。
总结

创建 vh-man2html 是一项有益的业余时间努力。互联网社区提供了构建它所需的组件和反馈。它演示了一种可以重新包装旧信息以便于访问的方式。Linux 提供了一个出色的开发平台。所有必要的组件都可以在 Linux 发行版中和通过 ftp 获得。

我要感谢 Richard Verhoeven 创建了他的 man2html 翻译器并使其可供互联网社区使用。我还要感谢那些花时间向我发送反馈以协助 vh-man2html 开发的人们。

为什么要这样做?

这一切是如何组合在一起的?

参考文献

Michael Hamilton 自 1989 年以来一直担任自由 Unix C/C++ 开发人员。他在 1992 年初偶然发现了 Linus 的帖子之一,从此便迷上了它。可以通过 michael@actrix.gen.nz 与他联系。

加载 Disqus 评论