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.

These problems are especially hard to spot in the more usual case where the publisher performs the final page layout (the typographers, while being proficient in their field, are not necessary cognizant of the subtleties of code structure and layout). The disparity between program code and the listing in the text can occur if the author changes the former as a result of, for example, technical reviewer feedback.

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.