/* :ts=4 */ /* * MRPrint: detabbing text file printer for the Amiga * Author: Mark Rinfret (Usenet: mrr@amanpt1.ZONE1.COM; 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]...filen] * 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. * * 08/30/89 -MRR- V3.4: Fixed bug in line numbering. * * 11/12/88 -MRR- Changed default margins to 1, 80, lines per page to 62. * * 08/26/88 -MRR- When MRPrint detected a binary file, it printed a blank page * to "commemorate" the event. Ugh! * * 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 #include #include #include #include #define VERSION "pr version 3.4, 08/29/89 (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 = 1; unsigned lineNumber; unsigned linesPerPage = 60; 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 = 80; 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 */ /* * 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.. */ } if (srcLine) { /* We printed something? */ 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 algorithm 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); } /* * Print a header. */ 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. */ PutString(" "); /* Don't call 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] == 0x08) { /* backspace? */ if (col > 1) { PutOneChar(line[i]); --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 % 10) + '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, ESCape * */ static char *okSpecial = "\n\f\t\015\010\033"; 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; }