Flatten roff Includes Safely with soelim
By the end of this guide, you will have expanded a roff document's nested .so requests into a stream that another preprocessor can read, while retaining useful source-file diagnostics. You will also know when to use a search directory and when the output should be raw.
The route
Jump straight to the step you need, or tick off Done means at the end.
This guide targets GNU soelim 1.23.0, installed here by groff-base 1.23.0-3build2. Allow about 10 minutes. You need a shell and read access to the roff files; no elevated privileges are needed because the examples write only under a temporary directory.
Checkpoint: know what soelim changes
soelim is a filter. It reads named input files, or standard input when no file is named, and writes the expanded text to standard output. A line whose dot is the first character and whose request is exactly .so is replaced by the named file. Included files are processed recursively.
This is useful before pic, tbl, eqn or another roff preprocessor needs to see content that would otherwise be loaded later by troff. It does not understand arbitrary roff control flow. For example, .if 1 .so otherfile is not expanded. The request must be on its own input line in the form that soelim recognises.
1. Create a small include tree
Make a disposable directory and two files. The second file is included by the first, which makes the expansion visible without touching a real manpage source tree.
work=$(mktemp -d /tmp/soelim-guide.XXXXXX)
mkdir "$work/inc"
printf '%s\n' 'child line' > "$work/inc/child.roff"
printf '%s\n' '.TH DEMO 1' '.so inc/child.roff' 'end' > "$work/main.roff"
cd "$work"
Keep the cd. Relative include names are looked up from the process's search context, so running from an unrelated directory can make a valid source request fail. The input file's directory is not a substitute for setting the search path explicitly.
2. Expand the requests and inspect the provenance
Run the filter with the main file as its operand. The default output includes .lf requests, which record the current file and line number for diagnostics produced by later roff processing.
soelim main.roff
Expected output is similar to this:
.lf 1 ./main.roff
.TH DEMO 1
.lf 1 ./inc/child.roff
child line
.lf 3 ./main.roff
end
The exact path spelling depends on the directory in which you ran the command. The significant checks are that the include line has gone, child line appears, and the stream returns to main.roff for end. A missing include produces an error on standard error and leaves the source request in the output, so do not treat output alone as proof of a complete flattening. Check the exit status in a script:
if ! soelim main.roff > expanded.roff; then
printf '%s\n' 'soelim could not expand every include' >&2
exit 1
fi
test "$(grep -c 'child line' expanded.roff)" -eq 1
3. Set an explicit include search path
Use -I dir when the include directory is not naturally available from the current working directory, or when a build should document its dependencies. You may repeat -I; directories are searched in the order given. The current directory is otherwise searched last, but putting it first is explicit with -I ..
cd /tmp
soelim -I "$work" "$work/main.roff"
That command still finds inc/child.roff because the search path contains the directory holding inc. For a project with shared and local includes, put the preferred directory first:
soelim -I ./local -I ./shared document.roff > expanded.roff
Read access is enough. If an include contains sensitive material, remember that the expanded stream now contains it and may be captured in a build log or redirected file.
4. Choose the output metadata deliberately
Use -r for raw output when the consumer is a general text tool, or when you explicitly do not want .lf requests. The included text is still expanded recursively.
soelim -r "$work/main.roff"
For TeX-oriented workflows, -t emits comment lines beginning with % instead of roff .lf requests. This installation prints a line such as % file /tmp/.../main.roff, line 1 before the source content. Do not use -t merely to make ordinary roff output prettier.
If both -r and -t are present, the last one wins. Thus soelim -r -t file selects TeX comments, while soelim -t -r file selects raw output.
Common traps and recovery
A source request must start with a dot at the beginning of the line. Indenting it, or placing spaces or tabs between the dot and so, protects it from this preprocessor. There must normally be at least one space between so and the file name. -C relaxes that last rule, but use it only when matching an existing input format because it can cause lines intended as ordinary text to be treated as includes.
File names containing a backslash or spaces need roff escaping. A backslash in a macro-file name is represented with \\ or \e; a space is written as backslash followed by a space escape, \~. Other escapes can stop replacement. Prefer simple relative names where you control the source.
Nothing in these examples changes the original files. To discard the disposable test tree after you have finished checking it, remove the exact directory shown by printf '%s\n' "$work" with your normal file-management tool. If you redirected output to expanded.roff, recovery is simply to delete that generated file; the source remains untouched.
Done means
- The nested
.sorequest expanded to the included text. - You chose
-Ideliberately when lookup should not depend on the current directory. - You selected default, raw or TeX comment provenance to match the next tool.
- Your script checked the exit status and did not mistake a partial stream for success.
- The original roff files are unchanged and any generated output is disposable.