Dynamic reports in troff, with R. Write a report with the classic -ms macros, put R
code between .SS and .SE, and knitroff runs it with
knitr, then typesets the page with GNU groff. The output is a
PDF, a PostScript file, or a draft for the terminal.
.NH
The data
.PP
In 1977 life expectancy ranged from \*[lo] to \*[hi] years.
.SR lo min(state.x77[, "Life Exp"])
.SS summaries
life <- state.x77[, "Life Exp"]
mean(life); median(life)
.SE
knitroff::render_roff("states.Rms") # states.pdf
knitroff::render_roff("states.Rms", format = "utf8") # states.txt, for less -R


troff has always worked this way. eqn finds the .EQ….EN blocks in a document,
replaces them with plain troff and passes the rest through, and tbl does the same with
.TS….TE. knitroff is one more preprocessor at the front of that pipeline, one that
runs R.
It began as spp in Compute S like 1979, a
simulated UNIX/32V running Bell Labs S. There, spp is a what-if: every piece it needs
existed in 1981, but nobody joined them until Sweave in 2002. knitroff is the same idea
for R and groff today, and it can do what 1981 couldn't: put the figures on the page.
From CRAN:
install.packages("knitroff")
Or the development version, from GitHub:
# install.packages("remotes")
# remotes::install_github("danielgccr/knitroff")
install.packages("knitroff", repos = c('https://danielgccr.r-universe.dev', 'https://cloud.r-project.org'))
knitroff needs GNU groff 1.22 or later to typeset: render_roff(). Most Linux
systems have it, though Debian and Ubuntu may have only groff-base, which has no ms
macros: sudo apt install groff; on macOS, which switched its man pages to mandoc, install it with
brew install groff; on Windows, see below. Figures in PDF need pdfinfo
from poppler.
knit_roff(), which only writes the troff, needs neither.
groff runs on Windows through MSYS2, and R users already have MSYS2 inside
Rtools. groff (1.24.1) is in MSYS2's
msys sub-repository, which is the one CRAN's Rtools instructions say may be added to it.
In the Rtools45 bash shell (C:\rtools45\msys2.exe or the Start menu):
pacman -Sy groff
This also installs Perl, which groff's PDF driver needs. R puts Rtools' tools on its
PATH, so check in R:
Sys.which("groff") # e.g. "C:\\rtools45\\usr\\bin\\groff.exe"
If it comes back empty, give the path: render_roff("report.Rms", groff = "C:/rtools45/usr/bin/groff.exe").
That is enough for PDF and PostScript with listings, equations and tables, and for the
terminal draft. Figures in PDF also need pdfinfo from poppler, and MSYS2 packages
poppler in its ucrt64 sub-repository, which Rtools warns against mixing in. Get it
another way:
C:\msys64): pacman -S groff mingw-w64-ucrt-x86_64-poppler,
then use that groff and put C:\msys64\ucrt64\bin on PATH for pdfinfo.scoop install poppler, with groff from Rtools.knit_roff() needs neither groff nor poppler, so writing the troff works anywhere.
A document is troff with the ms macros
(.TL, .AU, .AI, .AB/.AE, .NH, .SH, .PP, .LP, .DS/.DE …), saved as
.Rms. Three things are added:
| Write | And you get |
|---|---|
.SS … .SE
|
an R chunk. Chunk options follow .SS, as in .SS scatter, fig.cap="Income", echo=FALSE
|
`r expr` |
the value of expr in the text |
.SR name expr |
the troff string \*[name] set to the value of expr, as spp did |
Each chunk becomes one listing in a constant-width font, kept together on a page. The
code has > and + prompts and its output follows, without ##, the way an R session
looks. The characters -, ', `, ^ and ~ in code stay plain ASCII in every
output, so code can be pasted back into R.
Listings are highlighted the way printed listings were, without colour: R keywords, plus
TRUE, FALSE, NULL, NA, Inf and NaN, in constant-width bold, and comments in
constant-width italic. R's own parser finds the tokens. In the terminal draft, bold and
italic become the terminal's bold and underline.
You choose: render_roff(..., highlight = FALSE) gives the plain listing of 1981 for the
whole document, and a chunk can choose for itself with .SS highlight=FALSE or TRUE.
Output is never highlighted.
Everything else in the document goes to groff as it is, so .EQ equations, .TS tables
and .PS pictures work as usual, and so do .[ citations: render_roff() runs
groff -ms -k -R -e -t -p.
A plot in a chunk goes on the page with .PDFPIC in PDF and .PSPIC in PostScript,
centred and numbered, with fig.cap as its caption. Control the size with fig.width,
fig.height and out.width (a troff length, such as 4i).
A terminal can't show a picture, so the utf8 draft leaves a box, as authors did before
figures could be typeset:
+---------------------------------------------+
| Figure 1: paste up scatter-1.pdf here |
+---------------------------------------------+
Figure 1. Life expectancy against income, with a lowess smooth.
roff_table() turns a data frame into a tbl table: centred, boxed, with a bold header,
numeric columns aligned, and an optional caption. Make it the value of a chunk:
.SS extremes, echo=FALSE
knitroff::roff_table(head(mtcars[, 1:3]), caption = "Three columns of mtcars")
.SE
groff's refer, Mike Lesk's preprocessor from the Seventh Edition, finds citations in a
plain-text database of references, one per paragraph, with fields such as %A author,
%T title, %I publisher and %D date:
%A R. A. Becker
%A J. M. Chambers
%T S: An Interactive Environment for Data Analysis and Graphics
%I Wadsworth
%D 1984
Name the database once, cite with a few keywords that find one entry, and list the references where they should go, which ms heads "References":
.R1
database refs
accumulate
.R2
.PP
S was described by Becker and Chambers.
.[
becker interactive
.]
.[
$LIST$
.]
The citation becomes a number, [1]. The database path is relative to R's working
directory, as for data files read in a chunk.
groff can't set an equation on a terminal, because it drops eqn's half-line motions. So
the utf8 draft shows the eqn source instead, which Kernighan and Cherry designed to read
the way you'd say it aloud:
s sup 2 ~=~ 1 over {n - 1} sum from i=1 to n ( x sub i - x bar ) sup 2
The R help has a complete reference for writing knitroff documents, taken from the groff manuals and marking which features are extensions to the 1979 Seventh Edition:
| Help page | Covers |
|---|---|
?ms |
every ms macro (.TL, .PP, .NH, .IP, .DS, .FS, …), register and string |
?troff |
requests (.sp, .ce, .ft …), escapes (\fB, \*[name] …), units, special characters |
?eqn |
the equation language: sub, sup, over, sqrt, from, to, matrices, Greek |
?tbl |
tables: options, column formats, data, text blocks |
| Function | What it does |
|---|---|
render_roff(input, output, format = "pdf", highlight = TRUE) |
knits, then typesets with groff: "pdf", "ps" or "utf8"
|
knit_roff(input, output, format = "pdf", highlight = TRUE) |
knits to plain troff (.ms) only; no groff needed |
roff_table(x, caption, digits) |
a data frame as a tbl table |
groff_available(format) |
whether groff here can typeset that format with the ms macros |
hooks_roff() |
knitr's output hooks for troff, for driving knitr::knit() yourself |
knit_roff() restores knitr's patterns, hooks and options when it finishes, so it doesn't
interfere with other knitr documents in the same session.
The repository's .gitattributes tells GitHub to show .Rms files as Roff, so they are
highlighted there. RStudio's editor knows .Rnw and .Rmd but not .Rms, and has no way
to add a language, so it opens them as plain text, where its Insert Chunk shortcut and Knit
button do nothing.
knitroff gives RStudio two addins in their place, in the Addins menu:
| Addin | What it does |
|---|---|
| Insert troff chunk | wraps the selected lines in .SS and .SE, or inserts an empty chunk at the cursor |
| Render troff document | saves the .Rms file, runs render_roff() and opens the PDF |
A package can't bind keys, so bind them once in Tools → Modify Keyboard Shortcuts (search
for "troff"). Ctrl+Shift+Alt+I and Ctrl+Shift+Alt+K are free and sit next to RStudio's own
Ctrl+Alt+I and Ctrl+Shift+K; reusing those would take them away from .Rmd files.
Positron, Posit's newer editor for R, can learn a language, so
there knitroff has full support from a small extension in editors/positron:
.PP, escapes such as \fB and \*[name], comments
and eqn blocks; the R in chunks, in `r expr` and in .SR is highlighted as R.ss (chunk), fig (figure chunk), sr (.SR), eq (equation).render_roff() in the R console and opens the PDF.These keys apply only in .Rms files. To install, download
knitroff-rms-0.1.0.vsix,
then in Positron run Extensions: Install from VSIX... from the Command Palette
(Ctrl+Shift+P) and choose the file. It works the same in VS Code, where rendering runs
Rscript.
The nroff syntax works: vim.filetype.add({ extension = { Rms = "nroff" } }).
A chunk is a mapping away: vim.keymap.set("i", "<C-A-i>", ".SS<CR><CR>.SE<Up>").
knitroff is a vignette engine. In your package's DESCRIPTION:
VignetteBuilder: knitroff
Suggests: knitroff
Start the .Rms vignette with R's two lines, at the beginning of the line (knitroff
removes them before formatting):
%\VignetteIndexEntry{My report}
%\VignetteEngine{knitroff::roff}
R CMD build then typesets the vignette to PDF, and R CMD check runs its code.
inst/examples/states.Rms is the report pictured above: life expectancy in the 50 states
in 1977, with strings from .SR, a listing, a stem-and-leaf display, an equation, a figure
with a lowess smooth, and a table.
file.copy(system.file("examples", "states.Rms", package = "knitroff"), ".")
knitroff::render_roff("states.Rms")
groff-base for man
pages, which has no ms macros, so install the groff package. groff_available() says
whether this machine can typeset; the tests skip rendering where it can't, as on CRAN's
check machines. knit_roff() works everywhere. On Windows, see Windows.-U (unsafe mode), because .PDFPIC asks pdfinfo for each
picture's size. Render only documents you trust.getOption("digits"); round them in the expression,
as in `r round(x, 2)`.