[comp.sources.unix] v10i070: A "changebar" interface for *roff

rs@uunet.UU.NET (Rich Salz) (07/31/87)

Submitted-by: tektronix!tessi!bobl (Bob Lewis)
Posting-number: Volume 10, Issue 70
Archive-name: nrchbar

[  This program works with diff on ostensible Unix formatter input to
   output formatting codes to do changebars.  Very nice.  --r$  ]

#!/bin/sh
# to extract, remove the header and type "sh filename"
if `test ! -s ./README`
then
echo "writing ./README"
cat > ./README << '\Rogue\Monster\'
The "nrchbar" program effectively inserts n/troff-style change bar commands
into files which are (presumably) n/troff source.  For more information,
consult the man page.

To install "nrchbar":

1)	Inspect "Makefile" for proper destinations (currently,
	"/usr/local/bin" for the binary and "/usr/man/manl" for the man
	page).  Correct these if necessary.  If "diff" on your system isn't
	"/bin/diff", you'll also need to modify the "DIFFPROG" #define in
	"nrchbar.c".  "nrchbar" assumes "getopt" is present, so you'll need
	to contact your friendly neighborhood comp.sources.unix archive if it
	isn't.

2)	In the source directory:
		
		% make install

3)	If everything goes well, still in the source directory:

		% make clean

I claim that this program was written entirely by me, entitling me (with the
approval of my management) to release it into the public domain, which I now
do.  However, I make no warrantees, either express or implied, on "nrchbar"
or its associated files and I do not assume responsiblity for any damages
resulting from its installation or use.

The software has only been compiled and tested on a Sun 3/50, but I don't
expect any major problems on other UNIX machines, including System V.

In spite of the disclaimer above, I'd like to hear about bugs and/or feature
requests.

Acknowledgements and thanks to Robert Reed of Tektronix, who wrote the
original shell script; and to Jeff Hahs, Ron Lunde, Charlie Mills, and Bill
den Beste of TSSI for encouragement, ideas, and testing.

	- Bob Lewis
	  Test Systems Strategies, Inc.
	  8205 SW Creekside Pl.
	  Beaverton, OR  97005
	  (503) 643-9281

	  ...!tektronix!tessi!bobl   or   bobl@tessi.UUCP

\Rogue\Monster\
else
  echo "will not over write ./README"
fi
if `test ! -s ./Makefile`
then
echo "writing ./Makefile"
cat > ./Makefile << '\Rogue\Monster\'
SRC = nrchbar.c

BIN = nrchbar
BINDEST = /usr/local/bin

MAN = nrchbar.1
MANDEST = /usr/man/manl

SHARNAME = nrchbar

$(BIN):	$(SRC)
	cc -O -o $(BIN) $(SRC)

clean:
	rm -f $(BIN) *.o core a.out

install:	$(BIN) $(MAN)
	install -s $(BIN) $(BINDEST)
	cp $(MAN) $(MANDEST)

lint:	$(SRC)
	lint -bchx $(SRC)

shar:	README Makefile $(SRC) $(MAN)
	shar -f $(SHARNAME) README Makefile $(SRC) $(MAN)
\Rogue\Monster\
else
  echo "will not over write ./Makefile"
fi
if `test ! -s ./nrchbar.c`
then
echo "writing ./nrchbar.c"
cat > ./nrchbar.c << '\Rogue\Monster\'
/* nrchbar -- insert nroff change bar commands into a file */

#include <stdio.h>

#define DIFFPROG "/bin/diff"

typedef int bool;
#define TRUE 1
#define FALSE 0

#define ADD		'a'
#define CHANGE	'c'
#define DELETE	'd'

typedef struct {
	int lnOldFrom;
	int lnOldTo;
	int cmd;
	int lnNewFrom;
	int lnNewTo;
} diffdescr;

char *progname;
bool flagAll = FALSE;
bool flagBreakAfter = TRUE;
bool flagDelete = FALSE;

/* forward declarations */
FILE *fopenOrElse();
void nrchbar();
FILE *pipeDiffs();
void skipLinesOrElse();
void userErr();

#ifndef lint
char *rcsid = "$Header: nrchbar.c,v 1.3 87/03/31 16:33:16 bobl Exp $";
#endif

main(argc, argv)
	int argc;
	char *argv[];
{
	int c;
	extern int optind;
	FILE *fpDiff, *fpNew;
	char *fnOld, *fnNew;
	
	progname = argv[0];
	while ((c = getopt(argc, argv, "abd")) != EOF)
		switch (c) {

		case 'a':
			flagAll = TRUE;
			break;

		case 'b':
			flagBreakAfter = FALSE;
			break;

		case 'd':
			flagDelete = TRUE;
			break;

		case '?':
			(void) fprintf(stderr, "%s: unknown flag \"-%c\"\n", c);
			exit(1);
			break;
		}


	/*
	 *	Depending on the number of files on the command line.
	 */
	switch (argc - optind) {
	
	case 1:
		fpDiff = stdin;
		fnNew = argv[optind++];
		fpNew = fopenOrElse(fnNew, "r");
		break;

	case 2:
		fnOld = argv[optind++];
		fnNew = argv[optind++];
		fpDiff = pipeDiffs(fnOld, fnNew);
		fpNew = fopenOrElse(fnNew, "r");
		break;

	default:
		(void) fprintf(stderr, "Usage: %s [ -a ] [ -b ] [ -d ] [ <old> ] new\n", progname);
		exit(1);
	}
	/*
	 *	Now the real work begins.
	 */
	nrchbar(fpDiff, fpNew);

	exit(0);
}

/* beginChbar -- put out a *roff command to begin a change bar */
void beginChbar(chMark)
	int chMark;	/* in: use this character */
{
	(void) printf(".mc %c\n", chMark);
	return;
}

/* copyLines -- copy a given number of lines from one file to standard output */
bool copyLines(nLn, nSkip, fpFrom)
	int nLn;		/* in: copy this many lines ... */
	int nSkip;		/* in: ... skipping this many characters at the start of each one ... */
	FILE *fpFrom;	/* in: ... from this file to stdout */
{
	int ch;
	int iSkip;

	while (nLn-- > 0) {
		for (iSkip = 0; iSkip < nSkip; iSkip++)
			if (getc(fpFrom) == EOF)
				return FALSE;
		do {
			ch = getc(fpFrom);
			if (ch == EOF)
				return FALSE;
			putchar(ch);
		} while (ch != '\n');
	}

	return TRUE;
}

/* copyLinesOrElse -- copy a given number of lines from one file to standard output; exit if unable */
void copyLinesOrElse(nLn, nSkip, fpFrom)
	int nLn;		/* in: copy this many lines ... */
	int nSkip;		/* in: ... skipping this many characters at the start of each one ... */
	FILE *fpFrom;	/* in: ... from this file to stdout */
{
	if (!copyLines(nLn, nSkip, fpFrom))
		userErr("unexpected EOF");
	return;
}

/* endChbar -- put out a *roff command to end a change bar section */
void endChbar()
{
	if (flagBreakAfter)
		(void) printf(".br\n");	/* make sure the previous change bar is visible */
	(void) printf(".mc\n");
	return;
}

/* fopenOrElse -- open a file or else exit with an error message */
FILE *fopenOrElse(fname, type)
	char *fname;	/* in: file name to open */
	char *type;		/* in: way in which to open fname */
{
	FILE *fp = fopen(fname, type);

	if (fp == NULL) {
		(void) fprintf(stderr, "%s: can't open \"%s\" for mode \"%s\"\n",
			progname, fname, type);
		exit(1);
	}
	return fp;
}

/* getDiff -- get a "diff" description line */
int getDiff(fp, ddscr)
	FILE *fp;			/* in: file containing "diff" output */
	diffdescr *ddscr;	/* in: a diff section descriptor */
{
#define EOA				0
#define START_ACCUM		1
#define ACCUM			2
#define SET_OLD_FROM	3
#define SET_OLD_TO		4
#define SET_CMD			5
#define SET_NEW_FROM	6
#define SET_NEW_TO		7
#define MXN_ACTION		8
	static struct {
		int state;
		char *trig;
		int stateNext;
		int action[MXN_ACTION];
	} fsm[] = {
		{ 0,	"0123456789",	1,	{ START_ACCUM, EOA } },
		{ 1,	"0123456789",	1,	{ ACCUM, EOA } },
		{ 1,	",",			2,	{ SET_OLD_FROM, EOA } },
		{ 1,	"acd",			4,	{ SET_OLD_FROM, SET_OLD_TO, SET_CMD, EOA } },
		{ 2,	"123456789",	3,	{ START_ACCUM, EOA } },
		{ 3,	"0123456789",	3,	{ ACCUM, EOA } },
		{ 3,	"acd",			4,	{ SET_OLD_TO, SET_CMD, EOA } },
		{ 4,	"0123456789",	5,	{ START_ACCUM, EOA } },
		{ 5,	"0123456789",	5,	{ ACCUM, EOA } },
		{ 5,	",",			6,	{ SET_NEW_FROM, EOA } },
		{ 5,	"\n",			-1,	{ SET_NEW_FROM, SET_NEW_TO, EOA } },
		{ 6,	"123456789",	7,	{ START_ACCUM, EOA } },
		{ 7,	"0123456789",	7,	{ ACCUM, EOA } },
		{ 7,	"\n",			-1,	{ SET_NEW_TO, EOA } },
		{ -1,	(char *) NULL,	-1, { EOA } }	/* sentinal */
	}, *fsmCur;
	int stateCur = 0;
	int ch, accum = 0;
	int *action;
	int len = 0;
	extern char *index();

	do {
		ch = getc(fp);
		if (ch == EOF)
			return EOF;
		len++;
		fsmCur = &fsm[0];
		while (fsmCur->state != -1
				&& (fsmCur->state != stateCur
					|| index(fsmCur->trig, ch) == NULL))
			fsmCur++;
		if (fsmCur->state == -1)
			userErr("illegal syntax in 'diff' output");
		for (action = fsmCur->action; *action != EOA; action++)
			switch (*action) {

			case START_ACCUM:
				accum = ch - '0';	/* assume ASCII */
				break;				

			case ACCUM:
				accum = 10*accum + (ch - '0');	/* assume ASCII */
				break;
				
			case SET_CMD:
				ddscr->cmd = ch;
				break;
				
			case SET_NEW_FROM:
				ddscr->lnNewFrom = accum;
				break;
				
			case SET_NEW_TO:
				ddscr->lnNewTo = accum;
				break;
				
			case SET_OLD_FROM:
				ddscr->lnOldFrom = accum;
				break;
				
			case SET_OLD_TO:
				ddscr->lnOldTo = accum;
				break;
			}
		stateCur = fsmCur->stateNext;
	} while (stateCur != -1);
	
	return len;
}

/* isRoffCmd -- return TRUE iff the next line begins with a *roff command */
bool isRoffCmd(fp)
	FILE *fp;	/* in: file to examine for *roff command */
{
	int ch;
	
	if ((ch = getc(fp)) == EOF)
		userErr("unexpected EOF");
	(void) ungetc(ch, fp);
	return (ch == '.');	/* don't know about ".cc" command */
}

/* markDiff -- note a changed section of a file; return # of lines copied from fpNew */
void markDiff(ddscr, fpDiff, fpNew)
	diffdescr *ddscr;	/* in: descriptor of the differing sectinos */
	FILE *fpDiff;		/* in: the diff file */
	FILE *fpNew;		/* in: the new file */
{
	int nLnNew = ddscr->lnNewTo - ddscr->lnNewFrom + 1;
	int nLnOld = ddscr->lnOldTo - ddscr->lnOldFrom + 1;
	int nLnNewCopy = nLnNew;

	switch (ddscr->cmd) {

	case ADD:
		if (!flagAll) {
			while (nLnNewCopy > 0 && isRoffCmd(fpNew)) {
				copyLinesOrElse(1, 0, fpNew);
				nLnNewCopy--;
			}
		}
		if (nLnNewCopy > 0) {
			beginChbar('+');
			copyLinesOrElse(nLnNewCopy, 0, fpNew);
			endChbar();
		}
		skipLinesOrElse(nLnNew, fpDiff);
		return;
		
	case CHANGE:
		if (!flagAll) {
			while (nLnNewCopy > 0 && isRoffCmd(fpNew)) {
				copyLinesOrElse(1, 0, fpNew);
				nLnNewCopy--;
			}
		}
		if (nLnNewCopy > 0) {
			beginChbar('|');
			copyLinesOrElse(nLnNewCopy, 0, fpNew);
			endChbar();
		}
		skipLinesOrElse(nLnNew + 1 + nLnOld, fpDiff);	/* allow for "---" in diff */
		return;
		
	case DELETE:
		beginChbar('-');
		if (flagDelete) {
			putchar('[');
			putchar('[');
			putchar('\n');
			copyLinesOrElse(nLnOld, 2, fpDiff);	/* strip off the "< " */
			putchar(']');
			putchar(']');
			putchar('\n');
		} else
			skipLinesOrElse(nLnOld, fpDiff);
		endChbar();
		return;
	}
}

/* nrchbar -- produce *roff change bar file */
void nrchbar(fpDiff, fpNew)
	FILE *fpDiff;	/* in: diff file */
	FILE *fpNew;	/* in: original file */
{
	int lnNew = 1;
	int nLines;
	diffdescr ddscr;

	while (getDiff(fpDiff, &ddscr) != EOF) {
		nLines = ddscr.lnNewFrom - lnNew;
		if (ddscr.cmd == DELETE)
			nLines++;
		copyLinesOrElse(nLines, 0, fpNew);
		markDiff(&ddscr, fpDiff, fpNew);
		lnNew = ddscr.lnNewTo + 1;
	}
	while (copyLines(1, 0, fpNew))
		;
	return;
}

/* pipeDiffs -- return a pipe with a diff run on the other end */
FILE *pipeDiffs(fnOld, fnNew)
	char *fnOld;	/* in: old file name */
	char *fnNew;	/* in: new file name */
{
	char *cmd;
	FILE *fpPipe;

	cmd = (char *) malloc((unsigned int) (strlen(DIFFPROG) + 1 + strlen(fnOld) + 1 + strlen(fnNew) + 1));
	(void) sprintf(cmd, "%s %s %s", DIFFPROG, fnOld, fnNew);
	fpPipe = popen(cmd, "r");
	if (fpPipe == NULL) {
		(void) fprintf(stderr, "%s: can't open pipe for \"%s\"\n",
			progname, cmd);
		exit(1);
	}
	free(cmd);
	return fpPipe;
}

/* skipLinesOrElse -- skip a given number of lines in a file; exit if errors */
void skipLinesOrElse(nLn, fp)
	int nLn;	/* in: skip this many lines ... */
	FILE *fp;	/* in: ... in this file */
{
	int ch;

	while (nLn-- > 0) {
		do {
			if ((ch = getc(fp)) == EOF)
				userErr("unexpected EOF");
		} while (ch != '\n');
	}
	return;
}

/* userErr -- note an error and politely exude */
void userErr(msg)
	char *msg;
{
	(void) fprintf(stderr, "%s: %s\n", progname, msg);
	exit(1);
}
\Rogue\Monster\
else
  echo "will not over write ./nrchbar.c"
fi
if `test ! -s ./nrchbar.1`
then
echo "writing ./nrchbar.1"
cat > ./nrchbar.1 << '\Rogue\Monster\'
.\" $Header: nrchbar.1,v 1.2 87/03/31 16:33:02 bobl Exp $
.TH NRCHBAR 1 "March 19, 1987"
.SH NAME
nrchbar \- insert n/troff-style change bars in a file
.SH SYNOPSIS
\fBnrchbar\fP [ \fB\-a\fP ] [ \fB\-b\fP ] [ \fB\-d\fP ] [ \fIoldfile\fP ] \fInewfile\fP
.SH DESCRIPTION
\fINrchbar\fP inserts commands suitable for
.IR nroff (1)
or
.IR troff (1)
(hereafter, "\fI*roff\fP") to produce "change bars" denoting
differences between \fIoldfile\fP and \fInewfile\fP.
It sends the result to standard output.
.LP
If \fIoldfile\fP is defaulted, \fInrchbar\fP assumes that its standard
input will contain differences between \fIoldfile\fP and \fInewfile\fP
in
.IR diff (1)
format.
.LP
By default, \fInrchbar\fP ignores changes to sections containing nothing
but \fI*roff\fP commands, which it takes to be any lines beginning with
".".
You can override this with the \fB\-a\fP option.
.LP
\fINrchbar\fP inserts \fI*roff\fP ".mc" commands to provide the change
bars.
This will place a character to the far right of each changed section.
This character is a "+" for added sections, a "|" for
modified sections, and a "-" for deleted sections.
.LP
By default, the text of deleted sections does not appear, but the
\fB\-d\fP option will cause it to be inserted, surrounded by "[[" and "]]".
.LP
Also by default, \fInrchbar\fP puts a break (".br") command after each
changed section.
This is the only way to guarantee that deletions and small changes
get flagged.
The \fB\-b\fP option directs the program not to insert these breaks.
It makes the text look more like \fI*roff\fPed
\fInewfile\fP, but means that some change bars won't appear where
they should.
.SH PRONUNCIATION
NERCH-bar
.SH ETYMOLOGY
NRoff CHange BAR
.SH OPTIONS
.IP \fB\-a\fP
Put change bars around all changed sections, even if they consist of
nothing but \fI*roff\fP commands.
.IP \fB\-b\fP
Do not insert breaks after changed sections.
.IP \fB\-d\fP
Show deleted text by preceding it with "[[" and following it with "]]".
.SH SEE\ ALSO
nroff(1), troff(1)
.SH BUGS
"." is always assumed to denote \fI*roff\fP commands.
\fINrchbar\fP knows nothing about the ".cc" command.
.LP
The need for the \fB\-b\fP hack would disappear if there were a way
to tell \fI*roff\fP "Turn off the change bar character at
immediately after you put out this one.".
A horizontal ".wh", perhaps?
.LP
The ".mc" command doesn't always work, especially with diversions.
This is actually a \fI*roff\fP bug, but you ought to know about it.
.LP
\fINrchbar\fP may mess up tables, lists, or displays.
For a really polished change bar document, you may want to edit
its output before sending it to \fI*roff\fP.
\Rogue\Monster\
else
  echo "will not over write ./nrchbar.1"
fi
echo "Finished archive 1 of 1"
exit
-- 

Rich $alz			"Anger is an energy"
Cronus Project, BBN Labs	rsalz@bbn.com
Moderator, comp.sources.unix	sources@uunet.uu.qual