DiskMAG Volume 1 Number 2 (Nov 1988) : UTILITIES / MRPrint.c

/* :ts=4 */
/*
   MRPrint:  	detabbing text file printer for the Amiga
   Author:		Mark Rinfret (Usenet: mrr@amanpt1; Bix: markr)

	  I am offering this to the Amiga user community without restrictions.
	  If you make improvements, please re-release with source.	Enjoy!


   This program will print text files containing embedded tabs and
   form feeds.  Though the default tab setting is 4, the user may
   override this to some other value as necessary.  MRPrint will also
   optionally output a page header containing the filename, current
   date and time, line number and page number.  MRPrint supports variable
   margins and will enforce them.  Line numbers will be printed if
   requested.  Note that by default, MRPrint prints to PRT:.  If you wish
   to redirect output, be sure to use the "-s" option.

	  Usage:  pr [-l] [-n#] [-t#] [-h] [file1] file2] ...
	  options:
			-h		do not print a page header
			-l		print with line numbers
			-L# 	set left margin to #
			-n# 	print # lines per page 
			-R# 	set right margin to #
			-s		print to standard output
			-t# 	set tab to # spaces (default 4)

	  Handles ARP wildcarding.

	  05/13/88 -MRR- Yeah, I know - version 3.0 didn't last very long.
	                 I observed the output with the -s option and decided
					 that the single character I/O I was doing was very
					 unacceptable.  This version buffers both input and
					 output.

	  05/12/88 -MRR- THIS PROGRAM HAS BEEN ARPIFIED!  What the hell, 
	                 I've been wanting to dig into ARP for quite a
					 while.  Now that I have V1.1 of ARP, V3.6 of Manx
					 and a day off, this was as good a program as any
					 to do some exploring.
*/

#define AMIGA
/* #define DEBUG */

#include <stdio.h>
#include <ctype.h>
#include <libraries/arpbase.h>
#include <arpfunctions.h>
#include <functions.h>

#define VERSION "pr version 3.1, 05/13/88 (requires ARP V1.1 or higher)"

#define INBUFSIZE	4096L			/* input buffer size */
#define MAXLINE 	256
#define OUTBUFSIZE	2048L			/* output buffer size */
#define yes 		1
#define no			0
#define SizeOf(x)	((ULONG) sizeof(x))

/* An extended AnchorPath structure to enable full pathnames to be
 * generated by FindFirst, FindNext.
 */

struct UserAnchor {
	struct AnchorPath	ua_AP;
	BYTE 				moreMem[255];
	};

char 				*FGets();		/* AmigaDOS/ARP compatible version. */
char   				*NextFile();
void				PutNumber();
void				PutOneChar();
void				PutString();

unsigned			abort;			/* Set by CTRL-C, really unnecessary. */
struct UserAnchor	*anchor;		/* Used by FindFirst, FindNext */
struct DateTime		*dateAndTime;	/* Go ahead - take a wild guess. */
char    			dateStr[20], timeStr[20];
unsigned    		doLineNumbers = no;
unsigned			endOfInput;
BPTR				f;				/* The current input file (handle) */
char   				*fileName;		/* The name of the input file. */
unsigned    		forcePage;		/* Set by \f. */
unsigned    		headers = yes;	/* Controls page header generation. */
UBYTE				*inBuf,*inBufPtr; /* Input buffer, sliding pointer */
unsigned			inBufCount, inBufLength;
unsigned    		leftMargin = 5;
unsigned    		lineNumber;
unsigned    		linesPerPage = 55;
UBYTE 				*outBuf, *outBufPtr;/* Output buffer, sliding pointer */
unsigned			outBufLength;	/* Length of output buffer. */
unsigned    		pageNumber;
BPTR				printer;		/* Output device/file handle. */
static char			*prtname = 	"PRT:";
LONG				result;			/* Result of wildcard processing. */
unsigned    		rightMargin = 85;
unsigned    		srcLine;		/* Current source file line number. */
unsigned    		tabSpace = 4;	/* How many spaces 1 tab equals. */
unsigned    		tabStops[MAXLINE]; /* Computed tab stops. */
unsigned    		useRequester = no; /* Get filenames with requester? */
unsigned    		useStdOut = no; /* Print to standard output? */
unsigned    		xargc;			/* arg count after option processing */
char  				**xargv;		/* arg vector after option processing */

^L

/* This is where all goodness begins.  Actually, I'm not too happy with
 * the size of the main program.  It ought to be broken up (or down :-).
 */
main (argc, argv)
	int argc; char *argv[];
{
	unsigned    i;
	char   		*s;

	if (argc) {						/* zero if started from workbench */
		++argv;						/* skip over program name arg */
		--argc;

	 /* ..process switches.. */
		for (; *(s = *(argv)) == '-'; ++argv, --argc) {
			while (*++s)
				switch (*s) {
					case '?': 
						Usage();

					case 'l': 
						doLineNumbers = yes;
						break;
					case 'L': 
						if ((leftMargin = Atol (s + 1)) <= 0) {
							Abort("Bad left margin ", (long) leftMargin);
						}
						goto next_arg;	/* Oh my gawd!  A GOTO! */
					case 'n': 
						linesPerPage = Atol (s + 1);
						goto next_arg;	/* Oh no!  A nuther one! */
						break;
					case 'R': 
						if ((rightMargin = Atol (s + 1)) <= 0 ||
								rightMargin > MAXLINE) {
							Abort("Bad right margin ", (long) rightMargin);
						}
						goto next_arg;	/* It's a bloody epidemic! */
					case 's': 
						useStdOut = yes;
						break;
					case 't': 
						if ((tabSpace = Atol (s + 1)) <= 0) {
							Abort("Bad tab specification ", (long) tabSpace);
						}
						goto next_arg;	/* This is disgusting! */
					case 'h': 
						headers = no;
						break;
					case 'v':
						Printf("\n%s\n", VERSION);
						break;
					default: 
						Usage();
				}
		/* Gag!  A label! There must be some goto's sneakin' around... */ 
		next_arg:  	;
			}
		}

 /* Check a few argument combinations. */

	if (leftMargin >= rightMargin) {
		Abort("Left margin >= right margin?  Ha ha!", 0L);
	}

	if (doLineNumbers)
		leftMargin = 5;			/* No margins with numbering but numbers
								   use 5 columns. */

 
	SetTabs();					/* Initialize tab settings. */

	/* Allocate input and output buffers. */

	inBuf = ArpAlloc(INBUFSIZE);
	if (inBuf == NULL)
		Abort("No memory for input buffer!", INBUFSIZE);

	outBuf = ArpAlloc(OUTBUFSIZE);
	if (outBuf == NULL)
		Abort("No memory for output buffer!", OUTBUFSIZE);

 	/* Get the date and time; we might need it. */

	dateAndTime = (struct DateTime *) ArpAlloc(SizeOf(*dateAndTime));
	if (dateAndTime == NULL) {
		Abort("No memory!", SizeOf(*dateAndTime));
	}

	DateStamp(dateAndTime);
	dateAndTime->dat_Format = FORMAT_USA;
	dateAndTime->dat_StrDate = dateStr;
	dateAndTime->dat_StrTime = timeStr;
	StamptoStr(dateAndTime);

	if (useStdOut)
		printer = (BPTR) Output();
	else
		if ((printer = ArpOpen(prtname, MODE_NEWFILE)) == NULL) {
			Abort("Failed to open printer ", IoErr());
		}

 	/* Process files. */

	xargv = argv;
	if ((xargc = argc) == 0)		/* If no filename args, use requester. */
		useRequester = yes;
	else {
		if ((anchor = (struct UserAnchor *)
			ArpAlloc(SizeOf(*anchor))) == NULL) {
			Abort("No memory!", SizeOf(*anchor));
		}
		anchor->ua_AP.ap_Length = 255;	/* Want full path built. */
		anchor->ua_AP.ap_BreakBits |= 
			(SIGBREAKF_CTRL_C | SIGBREAKF_CTRL_D);

		result = ERROR_NO_MORE_ENTRIES;
	}

	while (!abort && (fileName = NextFile()) ) {
		if ((f = (BPTR) Open(fileName, MODE_OLDFILE)) != NULL) {
			PrintFile();
			Close(f);
			f = NULL;
		}
		else
			Printf("\n*** MRPrint: Can't open %s for printing ***\n", 
				   fileName);
	}
}

/* Abort the program.
 * Called with:
 *		desc:		descriptive text
 *		code:		error code (printed if non-zero)
 * Returns:
 *		to the system, where else?!
 */

Abort(desc, code)
	char *desc; long code;
{
	Printf("\n*** MRPrint aborting: %s", desc);
	if (code)
		Printf(" (%ld) ", code);
	Puts(" ***");
	if (f) Close(f);				/* File open? Close it. */
	ArpExit(20L, 0L);
}


/* Print one file. */

PrintFile() 
{
	char    line[MAXLINE];

	forcePage = pageNumber = srcLine = 0;

	lineNumber = linesPerPage;

	inBufPtr = inBuf;
	inBufLength = 0;
	inBufCount = 0;
	outBufPtr = outBuf;
	outBufLength = 0;
	endOfInput = no;

	while (FGets (line, MAXLINE - 1, f) != NULL && !abort ) {
		++srcLine;					/* count input lines */

/* Note that top-of-form detection was a rather kludgy addition.  It only
 * works if the first character in the line is a ^L.
 */
		if (*line == '\f') {
			*line = ' ';			/* replace embedded ^L with blank */
			lineNumber = linesPerPage;/* force new page */
		}

		if (lineNumber >= linesPerPage)
			Header();
		DeTab(line);				/* ..output detabbed line.. */

	}
 
	PutOneChar('\f'); 				/* ..form-feed after last page.. */
	FlushBuffer();
}

/* An attempt has been made to print a line past the right margin.
 * Crash the user's system and melt his...naw, force a new line and 
 * output a new left margin.  Also, if the page line count has been 
 * exceeded, start a new page.
 */

BreakLine() 
{
	PutOneChar('\n');
	if (++lineNumber > linesPerPage)
		Header();
	DoLeftMargin();
}

/* Output a dashed line according to an obscure formula derived through
 * intense empirical analysis while listening to the tune
 *
 * "Camptown ladies sing this song, DoDash, DoDash..."
 */

DoDash()
{
	PutMany(' ', leftMargin);
	PutMany('-', rightMargin - leftMargin - 5);
	PutOneChar('\n');
}

/* Output spaces for the left margin, or a source line number, whatever
 * tickles the user's fanny....fancy!
 */

DoLeftMargin() 
{
	unsigned    i;

	if (doLineNumbers) {
		PutNumber(srcLine, 4);
		PutOneChar(' ');
	}
	else
		PutMany(' ', leftMargin);
}

/* Noch ein bier, bitte, mit der grosse kopf.
 * That's Deutch for "Put a header on this page, please!".
 */

Header() 
{
	int     i;

	if (++pageNumber != 1) {
		PutOneChar('\f');			/* Eject if not first page. */
		PutOneChar('\n');
	}
	if (headers) {
		DoDash();

/* Note: there's room for improvement here.  A fancier algorithm would
 * attempt to distribute this information evenly over the current page
 * width.  A less lazy programmer would have written the fancier algorithm.
 */
		DoLeftMargin();
		PutString(fileName);
		PutMany(' ', 2);
		PutString(dateStr);
		PutMany(' ', 2);
		PutString(timeStr);
		PutString("  Page ");
		PutNumber(pageNumber, 0);
		PutString("  Line ");
		PutNumber(srcLine, 0);
		PutOneChar('\n');

		DoDash();

		PutString("\n");
	}
	lineNumber = 0;
}


/* Replace embedded tab characters with the appropriate number of spaces,
 * outputting the results to the output device/file.
 * Called with:
 *		line:		string on which to do replacements
 * Returns:
 *		eventually :-)
 */

DeTab(line)							/* DeTab is not as good as DePepsi. */
	char   *line;
{
	int     eol = 0, i, col;

	DoLeftMargin();
	col = leftMargin;

 /* Note: line[] has a terminating '\n' from fgets()...except if 
  * the input line length exceeded MAXLINE. 
  */
	for (i = 0; i < strlen (line); ++i)
		if (line[i] == '\t') {		/* ..tab.. */
			do {
				if (col == rightMargin) {
					BreakLine();
					break;
				}
				PutOneChar(' ');
				++col;
			} while (!tabStops[col]);
		}

		else {
			if (line[i] == '\n')
				++eol;
			else
				if (col == rightMargin)
					BreakLine();
			PutOneChar(line[i]);
			++col;
		}
	if (!eol)
		PutOneChar('\n');		/* no end of line? */
	++lineNumber;
}

/* Initialize the tab settings for this file. */

SetTabs() 
{
	int     i;

	for (i = 0; i < MAXLINE; ++i)
		tabStops[i] = (i % tabSpace == 1);
}


/* Display correct program Usage, then exit. */

Usage() 
{
	register unsigned   i;
	register char   	*s;

	static char *usageText[] = {
		"Usage:  pr [-l] [-n#] [-t#] [-h] [-v] [file1] file2] ...",
		"\toptions:",
		"\t\t-h      do not print page headers",
		"\t\t-l      print with line numbers",
		"\t\t-L#     set left margin to #",
		"\t\t-n#     print # lines per page",
		"\t\t-R#     set right margin to #",
		"\t\t-s      print to standard output instead of PRT:",
		"\t\t-t#     set tab to # spaces (default 4)",
		"\t\t-v      display program version number",
		"ARP wildcarding is supported.",
		(char *) NULL				/* last entry MUST be NULL */
	};

	for (i = 0; s = usageText[i]; ++i)
		Puts(s);

	ArpExit(20L, 0L);
}

/* Get the next file name, either from the argument list or via a
 * requester.
 */

char*
NextFile() 
{
#define NUMBEROFNAMES	10L

	static struct FileRequester request;
	static char dName[DSIZE*NUMBEROFNAMES+1] = "";
	static char fName[FCHARS+1] = "";

	struct FileLock *lock;

	if (useRequester) {
		if (request.fr_File == NULL) {
			request.fr_File = fName;

			/* To get the current directory path, get a lock on it, then
			 * use PathName to convert it to a full path.
			 */
			lock = Lock("", ACCESS_READ);
			PathName(lock, dName, NUMBEROFNAMES);
			UnLock(lock);
			request.fr_Dir = dName;
			request.fr_Hail = "Select file to print:";
		}
		return FileRequest(&request);
	}

	/* Note: result is initialized to ERROR_NO_MORE_ENTRIES prior to
	 * calling this routine for the first time.
	 */

	while ((result == 0) || (result == ERROR_NO_MORE_ENTRIES)) {

		if (result == 0) {				/* Working a pattern? */
			if ((result = FindNext(anchor)) == 0L) {
				if (SkipDirEntry(anchor)) continue;
				break;
			}
		}

		if (result == ERROR_NO_MORE_ENTRIES) {
			if (xargc <= 0) {
				result = -1;
				break;
			}
			result = FindFirst(*xargv, anchor);
			++xargv;					/* Advance arg list pointer. */
			--xargc;					/* One less arg to process. */
			if (result == 0) {
				if (SkipDirEntry(anchor)) continue;
				break;
			}
		}

		/* Only one error code is acceptable: */

		if (result && (result != ERROR_NO_MORE_ENTRIES)) {
			Printf("\n*** MRPrint I/O error %ld on pattern %s ***\n",
				   result, *xargv);
			result = 0;				/* Allow another pass. */
		}
	}

	/* Return filename or NULL, depending upon result. */
	return (result == 0 ? (char *) &anchor->ua_AP.ap_Buf : NULL);
}

/* Read one line (including newline) from the input file. 
 * Called with:
 *		line:		string to receive text
 *		maxLength:	maximum length of string
 *		f:			AmigaDOS file handle bee pointer (BPTR, ya' know).
 */

char *
FGets(line, maxLength, f)
	char *line; int maxLength; BPTR f;
{
	char 	*buf = line;
	int		c;
	int		lineLength = 0;

	if (abort = CheckAbort(NULL)) {
		PutString("\n^C\f");
		Abort("^C", 0L);
	}

	while (lineLength < maxLength) {
		if ( ( c = GetOneChar(f) ) < 0 ) break;
		++lineLength;
		if ((*buf++ = c) == '\n') break;	/* Stop on end of line. */
	}

	line[lineLength] = '\0';

	if (c < -1) {

		/* Report the error to the printer and the console, but don't
		 * give up on the rest of the files.  I think they call that
		 * being user friendly.
		 */
		c = -c;								/* Invert the error code. */
		Printf("*** I/O error on input %d ***\n", c);

		if (!useStdOut) {
			PutString("*** Input I/O error");
			PutNumber(c, 0);
			PutString("***\n");
		}

		lineLength = 0;
	}

	return (lineLength == 0 ? NULL : line);
}

/* Flush the printer (output) buffer (phew!). */

FlushBuffer()
{
	long	actualLength;
	long	ioResult;

	if (outBufLength) {
		actualLength = Write(printer, outBuf, (long) outBufLength);
		if (actualLength != outBufLength) {
			ioResult = IoErr();
			Abort("Output error!", ioResult);
		}
	}
	outBufPtr = outBuf;
	outBufLength = 0;
}

/* Get one character from the input stream.  If the input buffer is
 * exhausted, attempt to get some more input.  If this is the first
 * input buffer for this file, check the buffer for binary content.
 * Called with:
 *		f:		input file handle
 * Returns:
 *		character code (>= 0) or status (< 0, -1 => end of input)
 */

int
GetOneChar(f)
	BPTR f;
{
	int		ioStatus;

	if (endOfInput)
		return -1;

	if (inBufLength <= 0 ) {
		inBufLength = Read(f, inBuf, INBUFSIZE);

		/* If this is the first buffer, test it for binary content.
		 * If the file is binary, skip it by setting the actualLength
		 * to zero (simulate end of file).
		 */
		if ((++inBufCount == 1) && inBufLength > 0) {
			if (SkipBinaryFile(anchor))
				inBufLength = 0;
		}

		if (inBufLength <= 0) {
			if (inBufLength == -1)
				ioStatus = -IoErr();
			else {
				ioStatus = -1;
				endOfInput = yes;
			}

			return ioStatus;
		}
		inBufPtr = inBuf;
	}

	--inBufLength;
	return *inBufPtr++;
}

/* Put multiple copies of a character into the output buffer (repeat).
 * Called with:
 *		c:		character to be repeated
 *		n:		number of copies
 * Returns:
 *		tired but satisfied
 */

PutMany(c, n)
	int	c, n;

{
	for( ; n > 0; --n)
		PutOneChar(c);
}

/* Output a simple formatted unsigned number.
 * Called with:
 *		number:		value to be formatted
 *		length:		number of digits desired (0 => doesn't matter)
 */
void
PutNumber(number, length)
	unsigned number, length;
{
	unsigned digitCount = 0, i;
	char	digits[6];

	do {
		digits[digitCount++] = (number % 9) + '0';
		number /= 10;
	} while (number);

	while (length > digitCount) {
		PutOneChar(' ');
		--length;
	}

	do {
		PutOneChar(digits[--digitCount]);
	} while (digitCount);
}

/* Output one character to the printer device/file.
 * Called with:
 *		c:			character to be output
 * Returns:
 *		nada
 */
void
PutOneChar(c)
	int		c;
{
	if (outBufLength >= OUTBUFSIZE) 
		FlushBuffer();

	*outBufPtr++ = c;
	++outBufLength;
}

/* Output a string to the printer device/file. 
 * Called with:
 *		s:		string to output
 * Returns:
 *		when it's done, of course!
 */
void
PutString(s)
	char	*s;
{
	register int	c;
	register char	*s1;

	for (s1 = s; c = *s1; ++s1)
		PutOneChar(c);
}

/* Test the contents of the first buffer for binary data.  If the buffer
 * is determined to have binary content, tell the user that we are
 * skipping the file.  This allows the user to give a single wildcard
 * specification without worrying about printing object, data and program
 * files (assuming, of course, that binary data is detected within the
 * first INBUFSIZE bytes of the file).
 * Called with:
 *		anchor:		pointer to UserAnchor structure describing the file
 * Returns:
 *		yes:		file contains binary
 *		no:			file is text (we think)
 */

int
SkipBinaryFile(anchor)
	struct UserAnchor *anchor;
{
	char *strchr();

	/* The following string describes binary characters that are considered
	 * to be "OK".  These are, from left to right:
	 *
	 * newline, form feed, tab, carriage return, backspace
	 *
	 */
	static char 	*okSpecial = "\n\f\t\015\010";
	register UBYTE	c;
	register int	i;
	int 			isBinary = no;

	for (i = 0; i < inBufLength; ++i)
		if (((c = inBuf[i]) < ' ') || c > 0x7F) {
			if (!strchr(okSpecial, c)) {
				isBinary = yes;
				break;
			}
		}
	if (isBinary) {
		Printf("\n*** MRPrint: skipping binary file %s ***\n", fileName);
	}
	return isBinary;
}

/* Test the file described by the anchor parameter for "directoryness".
 * If it's a directory, print a message that we're skipping it.
 * Called with:
 *		anchor:		file entry info returned by FindFirst, FindNext
 * Returns:
 *		yes:		file is a directory
 *		no:			file is a file (astonishing, eh?)
 */
int
SkipDirEntry(anchor)
	struct UserAnchor *anchor;
{
	if (anchor->ua_AP.ap_Info.fib_DirEntryType >= 0) {
		Printf("\n*** MRPrint: skipping directory %s ***\n",
			&anchor->ua_AP.ap_Buf);
		return yes;
	}
	return no;
}