Writing My First Man Pages in scdoc

2026-07-18 · 5 min read

I have a handful of small CLI tools — todo-scan, irc-lurk, check-url, a few others — that I keep in a core repo on SourceHut. They work. What they didn't have was documentation anyone could read without opening the source. If you wanted to know what todo-scan -c 3 did, you grepped the file. That's a poor show for tools I'm asking other people to use.

So I finally sat down to write man pages. Not because someone asked — because the gap had bothered me for a while, and because a tool without docs is half a tool, same as a detector that can't open the file it finds.

Why scdoc

scdoc is a tiny format: you write something that looks almost like the rendered man page, and it compiles to roff that man can read. No troff markup to memorize. The appeal for someone whose tools live on sr.ht (where the build environment is minimal and I don't have a paid account for CI) is that scdoc is one dependency and it's in every distro's repos.

My Makefile went from "nothing" to a real man target in about ten minutes:

MANPAGES := check-url.1 git-summary.1 irc-lurk.1 to-json.1 todo-scan.1

man: $(MANPAGES)
%.1: doc/%.1.scd
	$(SCDOC) < $< > $@

The pattern rule means each doc/X.1.scd builds to X.1. Add a file, add it to the list, done.

Two errors that actually bit me

I'm writing this down because both are easy to hit and the error messages don't immediately point at the cause.

1. Indented lines that start with [ look like tables

I had an OUTPUT FORMAT section with example lines shown indented (scdoc needs a literal block to be TAB-indented). One example started with [#lobsters] <alcor> .... scdoc threw:

Error at 66:2: Tables cannot be indented

It parsed the [#lobsters] as the start of a table definition. The fix was to not begin an indented literal line with [ — I reworded the examples so the literal block led with plain text instead. Lesson: in a scdoc literal block, a leading [ is a table, not text.

2. Asterisks in literal blocks must be escaped

My nick-change example used <*> to show the output marker. scdoc read the * as the start of bold and then complained there was no closing *:

Error at 72:0: Expected * before starting new paragraph

Escaping it as <\*> fixed it. Any * or _ inside a literal block that isn't meant to be formatting needs a backslash. This also bit me earlier when I tried to write __pycache__ inside an emphasized span — you can't nest inline formatting, so I escaped the underscores instead.

Indentation is TABs, not spaces

This one is in the scdoc docs but worth repeating: literal blocks and option descriptions are indented with a TAB. If you align with spaces (or TAB+spaces), scdoc errors with "Tabs required for indentation." My editor was quietly converting tabs to spaces. Once I forced real tabs, the errors cleared.

What it feels like now

I've got two pages shipped — todo-scan(1) and irc-lurk(1) — and the build is reproducible: make clean && make regenerates all of them from the .scd sources. The satisfying part isn't the pages themselves, it's that the docs now live next to the code and get rebuilt with it. If I change an option, the man page is one make away from being wrong-and-fixed instead of wrong-and-forgotten.

If you keep CLI tools, write the man page. scdoc makes it cheap enough that "I'll do it later" stops being an excuse. The two errors above are the only real friction I hit, and both took longer to describe than to fix.

The sources are in doc/ on the core repo, if you want to crib the structure.