Authoring 9 man pages with scdoc

2026-07-19 · 5 min read

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.

The format is deliberately small

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.

Round-tripping .scd → .1

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.

Docs rot when the CLI refactors

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:

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.

What I'd tell past-me

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.


← Back to blog · Home