Vivian Voss

mandoc Answers to Four Names

technical beauty scope freebsd documentation mandoc

A man called Kristaps Dzonsons wanted to change the colour of section headings in HTML manual pages, and grohtml would not let him. That is the whole of the provocation. His first commit went in on 22 November 2008, and what came out of it now formats the manuals on FreeBSD, OpenBSD, NetBSD, DragonFly, illumos and every Mac in the building.

The tool it displaced is GNU roff, which is a general typesetting system with a distinguished history and a great deal of range. Manual pages are one of the things it can do. On the machine in front of me they are the only thing anybody ever asked it to do, and that mismatch is what this measurement is about.

One file, four names

The binary that renders every manual page on this system is 526,504 bytes, stripped, linked against three libraries of which two ship with the C runtime. Ask the filesystem which paths point at it and the answer is longer than expected:

# find /usr/bin -samefile /usr/bin/mandoc
/usr/bin/whatis
/usr/bin/mandoc
/usr/bin/apropos
/usr/bin/makewhatis

One inode wearing four names, and behind them four jobs: format a page, search the index, print a one-line summary, build the index in the first place. The program looks at the name it was invoked under and behaves accordingly, which is a trick as old as vi and view. It means the index builder cannot drift away from the formatter: apropos searches exactly what makewhatis wrote, and both of them are mandoc under another name, reading the same parser.

One inode, four names man — 1,156 lines of shell one binary 526,504 bytes, 3 libraries mandoc formats a page apropos searches the index whatis one-line summary makewhatis builds the index 11,138 pages, 23 MB index, 3,306,968 bytes

Above it sits man, which turns out to be 1,156 lines of POSIX shell weighing 25,842 bytes, deciding which file you meant and handing it over. That is the entire user-facing apparatus of the FreeBSD manual: a shell script and half a megabyte of C.

What it has to get through is not small. The base system carries 11,138 manual pages in 23 MB, and the distribution across sections says something about what this operating system thinks documentation is for: 5,690 pages describe library functions, 2,514 describe kernel interfaces, 1,028 describe drivers. The 629 in section one, the commands most people think of as “the manual”, are a rounding error next to the parts written for somebody reading source.

What the general answer costs

I installed groff in a clean jail to see what the alternative weighs. One package became seven:

groff-1.24.1_1        30,102,150 bytes
perl5-5.42.3          65,437,143
libiconv-1.18_1        3,657,746
uchardet-0.0.8_1         654,249
psutils-1.17_6           221,769
libpaper-1.1.28_1         66,321
indexinfo-0.3.1_1         11,766
---
Result ->            100,151,144 bytes in 4,406 files

A Perl interpreter arrives with the typesetting system, because the scripts that drive its output devices are written in Perl, and two thirds of the weight is that interpreter. Every byte of this is what generality costs: eight preprocessors for tables, equations, diagrams and bibliographies, eight output devices from PostScript through PDF to a line printer nobody has owned since 1994, and the glue to hold that together. groff earns its size honestly.

So the total is the wrong thing to stare at, and the shape underneath is the right one. groff 1.24.1 is 150,350 lines of C and C++ once comments and blank lines come out, of which the troff engine alone is 28,257. mandoc 1.14.6 is 36,849 lines of C altogether, and that figure includes every output driver it has. Put plainly, the complete smaller program comes to about thirty per cent more than the engine of the larger one taken by itself, while covering the whole of the job it was built for.

Same page, same machine, two toolchains what is measured mandoc (base) groff (7 packages) bytes on disk 526,504 100,151,144 files installed 1 4,406 linked libraries 3 6 source lines, no comments 36,849 150,350 processes per page 1 2 peak memory 3,884 KB 8,216 KB pf.conf(5) rendered 50 times 0.43 s 6.77 s section one, all 629 pages 1.38 s 19.81 s whole base manual indexed 1.74 s no equivalent

Rather more of that source than one might expect is not source at all: 1,908 of the 2,101 files in the mandoc tarball are regression tests, which leaves 193 files doing the work. The parser for mdoc, the language BSD manual pages are actually written in, comes to 11,696 lines. The output drivers come to 10,556. The roff core, the part that handles the older and more general macro language, is 4,326.

The stopwatch, with a caveat

The host was not idle while I measured. Load average sat between 9.1 and 11.2 throughout, because the machine has other work to do and I let it get on with it. Absolute timings under those conditions are soft; the ratios between two programs running under the same load are not, and the ratios are the point.

Rendering pf.conf(5), which is 3,654 lines of roff source and comes out at 2,568 lines of text, fifty times over: mandoc 0.43 seconds, groff 6.77. Eight and a half milliseconds against a hundred and thirty-five. Section one of the manual, all 629 pages of it, takes mandoc 1.38 seconds and groff 19.81. Memory at peak: 3,884 KB against 8,216. Processes spawned per page: one, against two, since groff runs troff and pipes it into grotty.

Then the operation that has no equivalent on the other side. makewhatis, which is the same binary again, walks the entire base system manual, parses every page semantically rather than by pattern, and writes the search index:

11,138 pages parsed: 1.74 s
index written: 3,306,968 bytes

Under two seconds for the documentation of an entire operating system, from the same half megabyte that renders it. The output of the two formatters is not byte-identical, incidentally, and I would rather say so than let a table imply otherwise: 2,568 lines against 2,514 for the same page, differing in where blank lines and line breaks fall. Both are correct renderings. They simply disagree about typography, which after thirty years of separate development is the least surprising result in this piece.

What sits between the question and the answer man pf.conf the reader asks man 1,156 lines of POSIX shell, finds the file mandoc 526,504 bytes, parses mdoc semantically mdoc source unchanged since 4.4BSD, 480 pages predate 2000 2,568 lines of text on the terminal, in 8.6 ms

A page from 1991

The reason any of this holds together is that mdoc, the language BSD manual pages are written in, stopped moving a long time ago. It first appeared as a troff macro package in 4.4BSD, and pages written against it then are still the pages shipping now.

I went looking for the oldest thing in my own manual tree. Four hundred and eighty pages in this base system carry a document date from the twentieth century, and the two oldest are rusers(1) and rwall(1), both dated 23 April 1991. A program written in 2008 renders source written in 1991 without a murmur.

To be certain the effect was not FreeBSD quietly maintaining those files, I fetched ls.1 out of the 4.4BSD tree, carrying a Berkeley copyright notice that runs from 1980 to 1993, and pushed it through. It rendered: 189 lines of clean output, five style remarks about trailing whitespace and section ordering, no errors at all. Thirty-three years between the writing and the reading, and nobody had to publish a migration guide in between.

Apple has removed the entire roff toolchain from macOS. FreeBSD let it go with 12.0, OpenBSD deleted it in 2011. The typesetting system that set Unix manuals for thirty years is gone from the systems that invented them, and nobody has filed a complaint.

The limit

Four honest deductions, and the first one is about my own machine.

The sandbox is not here. On OpenBSD, mandoc runs under pledge and unveil; the strings for it sit in the binary on FreeBSD too, where they do nothing, because FreeBSD's equivalent is Capsicum and mandoc does not call cap_enter. A parser for a complex format, and a format designed in 1979 at that, is exactly the kind of program one would want confined. It is confined on the system it came from and not on the system I am typing this on.

Zero CVEs is not a certificate. The NVD returns nothing for mandoc. It returns eleven entries for groff, spread across 2000 to 2011, and the striking thing about them is where they sit: pdfroff.sh, groffer, eqn2graph, pic2graph. Temporary file races in shell scripts, and one case of invoking Ghostscript without -dSAFER. The typesetting engine itself is barely represented. A project with 4,406 files has more places to keep a mistake than a project with 193, and that is a fair reading of both numbers. The other fair reading is that attention manufactures CVE entries and GNU software gets rather more attention than a BSD manual formatter.

The last portable release was 23 September 2021. Version 1.14.6, and nothing since. Development continues in the OpenBSD tree under Ingo Schwarze, so the code in your base system is newer than the tarball, though the distribution path everyone else depends on has now been quiet for five years. Finished software still needs somebody to answer when a report comes in, and underneath the manuals of six operating systems there is one person and a CVS repository.

And mandoc cannot typeset. It reads mdoc and man and enough roff to get through them. Hand it a document that uses the full language, or ask it for PostScript, and it will decline, correctly, because that was never the offer. Anyone who needs a typesetting system still needs groff, which is presumably why groff is still maintained and shipping releases in 2026.

The sheet

Efficiency. 526,504 bytes stripped, answering to four names, linked against three libraries, spawning one process per page. 36,849 lines of C including all output drivers, against 150,350 for the general system it replaced. 3,884 KB resident at peak.

Security. No CVE entries in the NVD. Runs as an ordinary user, takes no data from the network, gives up no privileges, and parses a complex format, which is the one answer of the four that should give anybody pause. Sandboxed with pledge and unveil on OpenBSD, unsandboxed on FreeBSD.

Longevity. First commit 22 November 2008. In the OpenBSD base system since 6 April 2009 and the default there since 31 March 2010; NetBSD from 26 October 2009, DragonFly from 29 October 2009, FreeBSD imported on 19 October 2012 and switched over on 23 November 2014, illumos from 21 July 2014. groff was deprecated out of the FreeBSD base system with 12.0, and macOS 27 ships none of the roff toolchain whatsoever, only a 1,647,168-byte mandoc and a shell script wearing four names of its own.

Stability. The mdoc language has not broken since 4.4BSD. 480 pages in this base system predate the year 2000, the oldest dated 23 April 1991, and a page from the 4.4BSD tree renders today with five style remarks and no errors.

Measured on 18 September 2026 in a FreeBSD jail, 15.0-RELEASE userland on a 15.1-RELEASE-p3 kernel, load average 9.1 to 11.2, median of the runs stated.

Somewhere in the 5,690 pages of section three there is a library call documented in 1994 by somebody who assumed a printer was involved, and this morning half a megabyte of C rendered it for a terminal that would have puzzled them both.