Using sed to make indexes for books (long)

TMWNN1 pts0 comments

(12 Nov. 2001 - some minor typos corrected from earlier versions.)

---------- Forwarded message ----------<br>Date: Sat, 15 Mar 1997 03:10:29 -0600<br>From: Eric Pement<br>To: Al Aab<br>Subject: using sed to make indexes for books (long)

SUBJECT: using sed to make indexes for books (long)

I work with book- and magazine-publishing, and some time ago I<br>needed to create an index for a book after typesetting. On our proof<br>pages (hard copy), we used a yellow marker to highlight the terms we<br>wanted to index, and then several volunteers used the computer to enter<br>the terms and the page numbers, separating them with a semicolon. Each<br>term was entered on one line. The initial input file looked something<br>like this:

Buddhism, Zen; 1<br>atheism; 1<br>dualism; 1<br>Solomon; 2<br>Lausanne Covenant; 4<br>Lewis, C.S.; 4<br>Lausanne Covenant; 5<br>Mormonism; 6<br>Latter-day Saints; 6<br>Trinity; 6<br>Lausanne, Switzerland; 8<br>Trinity; 8<br>. . . .

Note that the data was entered in the order that we completed each<br>page or chapter. Next, we sorted the file with a sort utility:<br>case-insensitive and numeric-aware (i.e., the number "3" must come<br>before "19"; in a normal ASCII sort, "19" would appear before "3"). To<br>get a sort which satisfied both conditions was extremely difficult,<br>even using the GNU sort program (the manual pages for GNU sort don't<br>explain the switches very well). The proper syntax to use is:

sort -t";" +0f -1 +1n input.file

Briefly explaining the switches, -t";" sets the field delimiter to<br>be a semicolon. Fields are numbered beginning at zero (0), not one.<br>Thus, "+0f -1" means the first sort key will begin at field 0 (the 1st<br>field, to normal people) and end before reaching field 1, and be<br>case-insensitive ("f" for folded). "+1n" means that the next sort key<br>will begin at field 1 (the 2nd field) and, being followed by no "-NUM"<br>value, will continue to the end of the line. The "n" means this field<br>will be sorted according to numeric values, even including decimal<br>points, instead of in ASCII order.

If you use other sort utilities, the command syntax will probably<br>differ. The entries for the sorted file now looked like this:

Adam; 13<br>Adam; 21<br>Adam; 30-32<br>agnosticism; 9<br>agnosticism; 120<br>atheism; 1<br>atheism; 9<br>atheism; 40-41<br>atheism; 118<br>Bible; 3<br>Bible; 11-14<br>Bible; 22

We wanted to convert the data shown above to look like this, in a<br>format ready for printing:

Adam, 13, 21, 30-32<br>agnosticism, 9, 120<br>atheism, 1, 9, 40-41, 118<br>Bible, 3, 11-14, 22<br>. . .

Four years ago I used an awk script to perform this conversion, but<br>I have now realized that a sed script could do this simply and with<br>fewer lines of code. The sed script I came up with to perform this<br>conversion looked like this (at first, anyway):

# INDEXER.SED v1.0 - indexes sorted input file<br># Annotated for seders mailing list<br>{ # on every line of the file...<br>:loop<br>$! N; # if not the last line, get the Next line<br>s/^\([^;]*;\) \(.*\)\n\1 \(.*\)/\1 \2, \3/<br>t loop; # if previous substitution occurred, goto :loop<br>s/;/,/; # replace the semicolon with a comma<br>P; # print first line of pattern buffer<br>D; # delete 1st line of buffer & redo the loop

This script works! Well, sort of. As long as the input file is<br>perfectly formatted, the script works fine. But I discovered that if<br>*one* line anywhere in the file was in error, the script would fail to<br>change every other line after that. Consider the two following sets of<br>input files (made very short for explanation here):

===SET 1====== ===SET 2======<br>Adam; 13 Adam; 13<br>Adam; 21 Adam; 21<br>Adam; 30-32 Adam; 30-32<br>agnosticism; 9 agnosticism; 9<br>agnosticism; 120 agnosticism; 120<br>atheism; 9 atheism, 9 # this line differs in SET2<br>atheism; 40-41 atheism; 40-41<br>atheism; 118 atheism; 118<br>Bible; 3 Bible; 3<br>Bible; 11-14 Bible; 11-14<br>Bible; 22 Bible; 22<br>binitarian; 82 binitarian; 82

Now, compare the output produced by "sed -f INDEXER.SED set1 set2":

Adam, 13, 21, 30-32 Adam, 13, 21, 30-32<br>agnosticism, 9, 120 agnosticism, 9, 120<br>atheism, 9, 40-41, 118 atheism, 9<br>Bible, 3, 11-14, 22 atheism, 40-41<br>binitarian, 82 atheism, 118<br>Bible, 3<br>Bible, 11-14<br>Bible, 22<br>binitarian, 82

As you can see, the absence of a single semicolon (;) from a line<br>which requires it throws off the entire rest of the script. At first,<br>it doesn't seem obvious why this should be so, but look at the script<br>more carefully:<br>1 {<br>2 :loop<br>3 $! N<br>4 s/^\([^;]*;\) \(.*\)\n\1 \(.*\)/\1 \2, \3/<br>5 t loop<br>6 s/;/,/<br>7 P<br>8 D<br>9 }

See the search pattern in line 4: /^\([^;]*;\) \(.*\)\n\1 \(.*\)/<br>This looks for the beginning of the line "^", followed by one or more<br>words which end in a semicolon "\([^;]*;\)", eventually followed by<br>another newline and that same set of words with its semicolon.

The \(...\) syntax indicates that the expression matched within the<br>grouping may be re-used within the expression by \NUM, where the<br>numbering begins with 1. What we're really doing is checking the next<br>line to see if it starts with the same word (or group of words) that<br>the previous line started with. If so, the search expression...

atheism line bible adam sort agnosticism

Related Articles