This week I gave my tool suite real man pages — nine of them, one per
command. I wrote them in scdoc
source (the .scd format), compiled each to a .1 with
the scdoc binary, and wired both into the Makefile. It was my
first time treating docs as code rather than as an afterthought, and a few
things surprised me.
scdoc's source is a stripped-down markup. A man page opens with a
.TH line (title, section, date, source, manual) and then mostly
looks like Markdown with a few roff-flavored twists:
# NAME git-summary - show a rich summary of a git repository # SYNOPSIS *git-summary* [_path_] [_--json_] [_--since_ _date_] # DESCRIPTION ...
The *bold* / _underline_ emphasis and the
_argument_ convention for italicized placeholders are the main
things to internalize. That's most of it. The smallness is the point: a man
page is a contract about what a command accepts, not a prose essay, and the
format keeps you honest about that.
One gotcha I hit: scdoc is strict about its heading levels. A line starting
with # is an .SH (section); it does not do
arbitrary Markdown heading depth the way a blog renderer does. If you write a
trailing # on a heading — thinking of a Markdown ##
style or a C# mention — scdoc's parser mishandles it (I read the
source and reproduced this; it's a known rough edge, not user error on my
part). The lesson: man-page structure is flat by design, and the tooling
enforces that.
The build step is trivial and pure:
scdoc < doc/git-summary.1.scd > git-summary.1
No templating engine, no generator config. The .scd is the
source of truth; the .1 is a build artifact I commit alongside it
so the pages ship without requiring readers to have scdoc
installed. That "commit the artifact" choice is a small doc-as-code decision:
it trades a little repo bloat for zero install friction. For a tool suite
people might git clone and immediately want man on,
that trade is worth it.
What I didn't expect: keeping the artifact in sync with the
source is a discipline, not a given. If I edit
doc/git-summary.1.scd and forget to re-run the build, the
committed .1 is stale. So the Makefile became the enforcement
point:
%.1: doc/%.1.scd scdoc < $< > $@
Now a plain make regenerates every page from source. The
artifact can't drift unnoticed, because the rule exists to rebuild it.
The real test of "docs as code" isn't writing them — it's what happens when you change the tool. Mid-week I did two refactors:
scan /
db-info) and dropped an old mode flag. Its man page still
described the flag. I had to rewrite the SYNOPSIS and DESCRIPTION to match,
and the make rebuild caught that the old example no longer
ran.Neither change was hard, but both were easy to forget. The thing
that saved me was treating the man page like any other file the refactor
touched: if I edited bin/todo-scan, I owed an edit to
doc/todo-scan.1.scd in the same commit. When docs and code live
in the same repo and the same diff, "update the docs" stops being a separate
task and becomes part of the change.
.1 from .scd is the only thing
standing between you and stale docs.Nine pages later, man git-summary actually works on a fresh
clone, and each page reflects what the command does today. The suite is
small, but it's documented like it intends to stay.