Expand Compressed .so Includes Safely with zsoelim
zsoelim turns roff .so requests into the content they point at, even when the include is sitting there gzipped. That matters the moment you need to inspect or feed a manual-page source into another roff tool and the file it points to isn't where you expect. Allow about ten minutes.
The route
Jump straight to the step you need, or tick off Done means at the end.
The examples use man-db 2.12.0, the version recorded by the installed zsoelim(1) page. You need a shell and a readable roff file. The command only reads files and writes expanded roff to standard output: it never edits the input, and none of this needs elevated privileges.
1. Check the command and version
On this host the executable sits outside the usual command search path, so verify both the package version and the executable path before relying on it in a script:
$ dpkg-query -W -f='${Package} ${Version}\n' man-db
man-db 2.12.0-4build2
$ /usr/libexec/man-db/zsoelim --version
zsoelim (man-db) 2.12.0
On another system, try command -v zsoelim first. If it returns a path, use zsoelim in the remaining examples. A missing command is an installation or PATH problem, not a reason to add a guessed path to a production script.
Checkpoint
Record the path that actually exists on your machine. The option set covered here is -C, -V and -h, with zero or more input files.
2. Create a small roff include
A .so request names another file. Use a temporary directory for a harmless test rather than experimenting in a real manual source directory:
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
printf '%s\n' '.TH DEMO 1' '.so included.roff' > "$workdir/main.roff"
printf '%s\n' '.SH NAME' 'demo \- a test include' > "$workdir/included.roff"
/usr/libexec/man-db/zsoelim "$workdir/main.roff"
The output contains the first file's roff lines followed by the contents of included.roff. The request is expanded in the output stream; main.roff itself is untouched.
The path is interpreted exactly as written in the request. For a relative request such as .so included.roff, keep the included file where the roff input and the command's resolution rules can find it. If you're diagnosing a real manual source, inspect the exact request instead of changing it to a path that merely happens to work from your current directory.
3. Let zsoelim find a compressed include
Here's the detail that actually saves time: if the requested file can't be opened, zsoelim tries the same name with .gz, .Z or .z. A compressed match is decompressed and inserted straight into the output.
printf '%s\n' '.TH DEMO 1' '.so compressed.roff' > "$workdir/main.roff"
printf '%s\n' '.SH NAME' 'demo \- a compressed include' | gzip -c > "$workdir/compressed.roff.gz"
/usr/libexec/man-db/zsoelim "$workdir/main.roff"
.TH DEMO 1
.SH NAME
demo \- a compressed include
This is a read-only transformation: it does not replace included.roff.gz with an uncompressed file. Other compression extensions may exist in a build with different compile-time options, so don't promise support for an extension your local manual page doesn't document.
Checkpoint
If the output stops at the .so line or reports a missing request, check the spelling, directory and exact compressed suffix. Do not silently substitute a different file: an incorrect include can produce a plausible but wrong manual page.
4. Process standard input in a pipeline
With no file arguments, zsoelim reads standard input, which is handy when the roff source is already sitting in a pipeline:
printf '%s\n' '.so included.roff' | /usr/libexec/man-db/zsoelim
.SH NAME
demo \- a test include
Use an explicit input file when repeatability matters. A pipeline can hide which source produced a bad request, and a command substitution or upstream filter can alter the bytes before zsoelim ever sees them.
5. Inspect failures without changing state
A missing include is worth investigating, not papering over. Capture the output and exit status separately:
/usr/libexec/man-db/zsoelim "$workdir/main.roff" > "$workdir/expanded.roff"
status=$?
printf 'zsoelim status: %s\n' "$status"
test "$status" -eq 0
- Inspect with
sed -norless, not by eyeballing a terminal dump ofexpanded.roff. - Treat a non-zero status as a failed expansion and go check the original request; exact diagnostics vary with the installed build.
- Do not reach for
sudojust because a file is missing. Elevated privileges can expose a file an ordinary man-page consumer can't read, which masks a real permissions defect in the source tree.
Two flags worth knowing and then forgetting: -h prints help and -V prints version information. -C exists for compatibility with other soelim implementations, but this version already enables the behaviour it would request, so it's ignored. Adding it will not make a missing include resolvable.
6. Feed the expanded result to a roff tool
Once the include expansion is verified, pass the output to a renderer or another preprocessor. Keep the stages visible while debugging:
/usr/libexec/man-db/zsoelim "$workdir/main.roff" > "$workdir/expanded.roff"
groff -Tutf8 -man "$workdir/expanded.roff" > "$workdir/demo.txt"
sed -n '1,24p' "$workdir/demo.txt"
The final command depends on the document macros and renderer you're using. zsoelim only satisfies the include requests; it is not a general roff formatter. If your source uses macros that need a particular preprocessor, run that tool after confirming the expanded text.
Safety boundary
The examples write only under a temporary directory and remove it when the shell exits. Before using a redirection such as > expanded.roff in a real source directory, confirm the destination: redirection truncates an existing file before the program even runs. Prefer a new temporary output and move it into place only after inspection, with a backup if replacing a maintained source is genuinely required.
Done means
- You verified the installed man-db and zsoelim versions.
- You expanded a plain relative
.sorequest and saw its content in the output. - You confirmed a documented compressed suffix gets decompressed during expansion.
- You know no input file is edited and no privilege is normally required.
- You capture the exit status and investigate missing includes instead of guessing replacements.
- You keep expansion separate from the later roff rendering step.