Including Formatted Source Code in Groff Documents
Introduction
Several people have asked me for more details of the c2ms
script I wrote to include the program listings in my book,
Solaris Systems Programming, so with Ed Schaefer’s gentle
encouragement, I decided to write an article about it.
Two Options
Before I describe the scripts in detail, I should explain the two options an author has when they want to include program listings into the text of their book. The first method is to literally transcribe the program into the running text, formatting as required. Because it is a manual (some might say laborious) process, this method is susceptible to several errors, including:
- Typos.
- Formatting errors.
- Disparity between the actual code and the listing in the book.
The second option is to include the program code automatically. I knew that my book would have a lot of code examples in it, so I wanted to automate the process of including the listings as much as possible: as well as reducing the number of errors, it made my job easier.
Formatting Program Listings
I used the excellent groff package to typeset my book, so the
first thing I had to do was write a little macro that (among
other things) included—in the C preprocessor sense of the
word—a suitably massaged version of the program’s source code.
I called this macro insert_src, and a simplified version of
it is shown in the following code (the version I used for my book was
more complicated because it had the ability to include partial
listings):
1 .\" Define a macro to insert a (suitably massaged)
2 .\" source file, with a line on either side telling
3 .\" us the name of the file (all files are assumed
4 .\" to live in the ./src directory.
5 .de insert_src
6 .par@reset
7 .nr indent \\n[\\n[.ev]:pli]u
8 .in \\n[indent]u
9 \\l'\\n(LLu-\\n[indent]u-\\w' \\f(PI\\$1\\fP'u\\(rn' \\v'-0.375'\\f(PI\\$1\\fP\\v'0.375'
10 .ps 8
11 .vs 10
12 .CW
13 .sp -0.6
14 .so src/\\$1.ms
15 .ps
16 .vs
17 .par@reset
18 .in \\n[indent]u
19 \\l'\\n(LLu-\\n[indent]u-\\w' \\f(PI\\$1\\fP'u\\(rn' \\v'-0.375'\\f(PI\\$1\\fP\\v'0.375'
20 ..
Before continuing, let’s take a closer look at this code.
The first 4 lines are just comments, and the 5th line says that we
are defining a new macro called insert_src. The next 3 lines
reset the current paragraph and set the indent appropriately.
The strange-looking text on line 9 draws a horizontal line
across the page, ending in the name of the file we’re listing.
Lines 10 to 13 set the point size and the vertical spacing between
lines (to 8 and 10 points respectively), switch to a constant-width
font, and move “up” the page a bit so that the listing appears
in the correct place. Line 14 is replaced by the massaged file
(in much the same way that a #include line in a C program is
replaced by the contents of the included file), and finally the
remaining lines clean up: the point size and line spacing are
restored, the current paragraph is reset, the indent is restored,
and another horizontal line printed.
The insert_src macro takes one argument: the name of the program
to include, which is assumed to reside in a subdirectory called
src. So if we wanted to include the source code for a program
called foo.c, we would put the following line at the appropriate
place in our document:
.insert_src foo.c
(A line starting with a period is how groff recognizes a macro
invocation.) Strictly speaking, as those looking closely at
line 14 of Listing 1 will notice, the actual file included in
this example is src/foo.c.ms; the file with the
.ms suffix is the one that results from a successful
invocation of the other scripts described in this article.
However, before we can include the program, it must first be
processed: this is the job of the c2ms script, and its companion
sed script, c2ms.sed. These scripts are shown below.
Here’s the code for c2ms:
1 #!/bin/ksh
2 #*******************************************************************************
3 #
4 # Name: c2ms
5 # SCCS Id: @(#)c2ms 1.3 05/08/05
6 #
7 # Description: This script converts a C source file in to a format
8 # suitable for including into an ".ms" document.
9 #
10 #
11 # Copyright © 1998-2005 by Rich Teer. All rights reserved.
12 #
13 #*******************************************************************************
14 SED_SCRIPT=/usr/local/lib/c2ms.sed
15 FILE="$1"
16 # For each file on the command line, we do the following:
17 #
18 # * Expand tabs to 4 spaces, piping the result through nl to number
19 # the lines (we use a space to separate the line number from the
20 # C source).
21 #
22 # * The above is piped through sed, which massages the source to a
23 # suitable form for feeding to groff. The output is surrounded
24 # by groff .nf and .fi requests to control line filling.
25 if [ -f "" -a -r "" ]; then
26 NUM_LINES=`grep -v "^$" | wc -l | awk '{printf $1}'`
27 if [ "$NUM_LINES" -lt 10 ]; then
28 NUM_COLS=1
29 elif [ "$NUM_LINES" -lt 100 ]; then
30 NUM_COLS=2
31 elif [ "$NUM_LINES" -lt 1000 ]; then
32 NUM_COLS=3
33 else
34 NUM_COLS=4
35 fi
36 echo ".nf" > $FILE.ms
37 expand -t4 $FILE | nl -w$NUM_COLS -s" " | sed -nf $SED_SCRIPT >> $FILE.ms
38 echo ".fi" >> $FILE.ms
39 else
40 if [ ! -f "$FILE" ]; then
41 echo "c2ms: Can't open $FILE: No such file or directory"
42 elif [ ! -r "$FILE" ]; then
43 echo "c2ms: Can't open $FILE: Permission denied"
44 fi
45 fi
The interesting bit of this script starts on line 25. If the file specified on the command line exists and is readable, line 26 determines how many non-blank lines the file contains (i.e., we are interested only in lines that aren’t blank). Lines 27 to 35 determines how many columns are required for the line number (programs up to 9999 lines in length are supported).
Line 36 emits the string .nf to the output file. This temporarily
turns off groff’s line filling ability so the program’s layout is
not tampered with. Conversely, line 38 outputs .fi to turn line
filling back on.
The pipeline on line line 37 is the crux of this script. It uses
the expand utility to expand tabs to 4 spaces, then calls nl to
number the lines. The -w argument tells nl how wide the line number
field is to be, and the -s argument tells nl to separate the line number
and its corresponding text by a space (the default is a tab). The
output of nl is piped through the sed script shown in Listing 3,
the output of which is concatenated to the output file. (Incidentally,
the listings for this article were produced by a variation of this line,
omitting (among other things) the filtering by the sed script described
next.)
The following code shows the c2ms.sed script that c2ms calls.
1 #*******************************************************************************
2 #
3 # Name: c2ms.sed
4 # SCCS Id: @(#)c2ms.sed 1.3 05/08/05
5 #
6 # Description: This file is a sed script that is used to convert C source
7 # files into a format suitable for .ms groff files. It
8 # doubles up all the '\' characters, and converts blank lines
9 # to ".sp 0.7v".
10 #
11 #
12 # Copyright © 1998-2005 by Rich Teer. All rights reserved.
13 #
14 #*******************************************************************************
15 s/\\/\\\\/g
16 s/^$/.sp 0.7v/
17 s/^ $/.sp 0.7v/
18 s/^ $/.sp 0.7v/
19 s/^ $/.sp 0.7v/
20 s/^ $/.sp 0.7v/
21 s/^ $/.sp 0.7v/
22 p
Line 15 doubles up the backslash (\) characters. This causes them to be escaped, which
is required to prevent groff from interpreting constructions like
“\n” in printf statements. Lines 16 to 21 causes blank lines or those
consisting of just spaces (the latter of which are created by nl)
to be replaced by the string “.sp 0.7v”. This tells
groff to insert a vertical space equal to 0.7 of a line spacing.
In other words, we replace blank lines with lines that are not quite a full line height.
This enables us to squeeze a few more lines onto the page without it
looking too cramped.
The final line of the script causes the line to be printed, after all of our modifications have been applied.
Example
Nothing illustrates all this better than a simple example. The following
shows the source code for a small document that includes a program
called hello.c.
.ce
An Example Document
.LP
This is a very small example document.
Its sole purpose is to illustrate the
use of my macro to include formatted
program listings.
The following is the ubiquitous "Hello,
World" program.
.LP
.insert_src "hello.c"
.LP
This is some text that comes after the
formatted version of the program.
We tell groff to format
this example and create a PostScript document using the following command:
cat macro.ms c2ms_example.ms | groff -mgs > c2ms_example.ps
(The file macro.ms contains the definition of our insert_src macro.)
The resulting page looks like this.
Summary
This article has described two scripts and a macro that can be
used to include program listings into documents formatted using
groff and other troff-like formatters.
They were invaluable to me while I wrote Solaris Systems Programming,
and I hope they’re useful to you too.
Author’s Bio
Rich Teer is an independent Solaris consultant who has been an active member of the Solaris community for more than ten years. He is the author of the best-selling Sun Microsystems Press book, Solaris Systems Programming, and several Solaris-related articles. He was a member of the OpenSolaris pilot program, and currently serves on the OpenSolaris Community Advisory Board (CAB). Rich lives in Kelowna, British Columbia, with his wife, Jenny, and their canine child, Judge. His web site can be found at richteer.ca.