SOUP: the SOUrce Printer
When developing new source code for programs, I need hardcopy now and then. This hardcopy serves many
purposes. For one: backups. If you loose the source in electronic form, you always have the paper backup which
can be entered by typing.
Also when debugging, it can be very relaxing (to me) to have pages of paper on the table so that I can skim through them and keep several pages next to eachother to compare them. This makes me find bugs faster. But then, I'm an old fashioned man. Young people just don't ever need printed sources....
Anyway, I made SOUP to overcome this lack of hardcopy in Unix. It's syntax or usage is as follows:
soup <infile >outfile or soup <infile | lpr or soup infile | lprFrom 'soup05' onwards, the program is smart enough to take it's input by default from StdIn, unless there is a commandline option. If this is the case, soup will try to open the file with that name and take it's input from there. The output is still sent to StdOut.
So, all of the following commands will be valid:
|soup <infile >outfile||this command will take it's input from 'infile' and send the output to 'outfile'|
|soup infile | less||this will read the input again from the specified file and the output is sent to 'StdOut', which uses the PIPE parameter to feed the processed data to the 'less' command.|
|cat soup05.mi | soup | lpr||cat starts with feeding the contents of file 'soup05.mod' into the pipe that feeds 'soup'. soup then processes that data and it will pipe the new data to the input of the 'lpr' command.|
Soup sets up the printer to condensed mode (80x120) and then spits out line after line with a fixed indentation. At the end of a page, it adds a footer, including sourcename, pagenumber and date.
Soup assumes that the source is a Modula-2 file. Of course it will print out C and other language source files
as well, but the first line of every Modula-2 program contains the name of the MODULE we're working on.
So if you are editing a sourcefile from another language, make sure the first line contains a reference to the name of the actual module you're working on.
I have now a working and stable version of soup ready. It is soup05 and can be downloaded via the navigator bar. The package contains a Linux-ELF executable, the source and an example 'soup.rc' file. The soup.rc configurationfile must be located in the /usr/local/prut/ directory. Probably you will have to make that directory (as root).
The example source in this webpage is still of soup version 04. I am not going to rewrite this full page for soup05 (at least not now) so if you want to find out what's different in soup05, please look at the bottom of this page for an overview and next download the soup05.tar.gz package to get full sources.
The source of soup
MODULE soup04; (* This software is released under the rules of the GNU GPL. Please feel free to improve this software, if needed. You may even port this source to a lesser language like C. This software comes without any warranty of any kind. *) (* 02 : Get a working program, no frills. Apr 20, 2003 *) (* 03 : Added MakeDate, CardToString Apr 25, 2003 *) (* We have an odd bug, causing one extra formfeed after the last page. Solved: In 'WrapUp', the linefeeds were one too far, which triggered an extra call to 'Skip' Apr 29, 2003 *) (* 04 : Add /usr/local/soup/soup.rc configuration file May 9, 2003 *) IMPORT ASCII; FROM InOut IMPORT Read, Write, WriteString, WriteLn, WriteBf, WriteCard, EOF; FROM Strings IMPORT Append, Assign, EmptyString, Length, StrEq; FROM SysLib IMPORT exit, time; FROM Arguments IMPORT ArgTable, GetArgs, GetEnv; FROM NumConv IMPORT Str2Num; FROM TextIO IMPORT File, PutString, PutLn, OpenInput, Close, GetChar, Done, GetCard, GetString, PutBf; TYPE ChronosMode = (Date, Time, Seconds, Minutes, Hours, Day, Month, Year); ControlString = ARRAY [0..63] OF CHAR; CONST maxLINE = 82; maxPOS = 120; topMARGIN = 1; leftMARGIN = 8; StdErr = 0; VAR line, page, pos, LeftMargin, MaxLine, MaxPos, TopMargin : CARDINAL; SomethingPrinted : BOOLEAN; FirstLine : ARRAY [0..39] OF CHAR; DateString : ARRAY [0..15] OF CHAR; buffer : ARRAY [0..255] OF CHAR; FillUp : CARDINAL; string : ARRAY [0..15] OF CHAR; PrReset, PrSetup, option : ControlString;
This is all rather straightforward. It's just Modula-2 like with other compilers. Which is typical for Mocka: it complies very strict to Wirth's books. There are some symplifications, like the interchangability of 4 byte variable types:
Please take a look at the 'Reference' file via the navigator frame. It contains an overview of the datatypes and sizes of the Mocka compiler.
The first function to come up, is ErrorMessage. It writes a string to StdErr (the error output). The funny part here is that Unix StdErr is handle 2, whereas I had to write to handle 0 in Mocka Modula-2. But it works, and that's what matters most.
After ErrorMessage, the configuration file readers are defined. GetCtrlStr is used to read the printer setup
string from the /usr/local/soup/soup.rc file.
Since the weirdest control sequences exist for many printers, I could not use simple ways to construct ESCape sequences. Therefore I had to invent the tricks with Escape, Space and Control. See below and in the file soup.rc.
Errormessages and so forth
PROCEDURE ErrorMessage (mess : ARRAY OF CHAR); BEGIN PutString (StdErr, mess); PutLn (StdErr); PutBf (StdErr) END ErrorMessage; PROCEDURE GetCtrlStr (infile : File; VAR str : ARRAY OF CHAR); VAR ch : CHAR; BEGIN str  := 0C; (* Clear string *) LOOP GetString (infile, option); IF StrEq (option, 'END') THEN EXIT END; IF StrEq (option, '<ESC>') THEN Append (str, 33C) ELSIF StrEq (option, '<_>') THEN Append (str, ' ') ELSIF StrEq (option, '<Ctrl>') THEN REPEAT GetChar (infile, ch) UNTIL ch > ' '; option  := CHR (ORD (ch) - ORD ('@')); option  := 0C; Append (str, option) ELSE Append (str, option) END END END GetCtrlStr; PROCEDURE ConfigureSoup () : BOOLEAN; VAR InFile : File; BEGIN OpenInput (InFile, '/usr/local/soup/soup.rc'); IF Done () = TRUE THEN REPEAT GetString (InFile, option) UNTIL StrEq (option, 'BEGIN'); LOOP GetString (InFile, option); IF StrEq (option, 'END') = TRUE THEN EXIT ELSIF StrEq (option, 'PrReset') = TRUE THEN GetCtrlStr (InFile, PrReset) ELSIF StrEq (option, 'PrSetup') = TRUE THEN GetCtrlStr (InFile, PrSetup) ELSIF StrEq (option, 'LeftMargin') = TRUE THEN GetCard (InFile, LeftMargin) ELSIF StrEq (option, 'TopMargin') = TRUE THEN GetCard (InFile, TopMargin) ELSIF StrEq (option, 'MaxPos') = TRUE THEN GetCard (InFile, MaxPos) ELSIF StrEq (option, 'MaxLine') = TRUE THEN GetCard (InFile, MaxLine) ELSE ErrorMessage ('Invalid parameter in "/usr/local/soup/soup.rc".'); ErrorMessage ('Reading aborted, assuming HP Laserjet IIP.'); RETURN FALSE END END; Close (InFile) ELSE ErrorMessage ("No file '/usr/local/soup/soup.rc'. Assuming HP Laserjet IIP."); RETURN FALSE END; RETURN TRUE END ConfigureSoup;ConfigureSoup was the main addition of version 4. It tries to open the rc file and if that fails, it sends an errormessage and falls back to HP LJ mode.
PROCEDURE Spaces (n : CARDINAL); (* Print n spaces *) VAR i : CARDINAL; BEGIN FOR i := 1 TO n DO Write (' '); INC (pos) END END Spaces; PROCEDURE CardToString (x : CARDINAL; VAR str : ARRAY OF CHAR); VAR i : CARDINAL; ch : CHAR; BEGIN FOR i := 0 TO HIGH (str) DO str [i] := ' ' END; i := HIGH (str); LOOP str [i] := CHR ((x MOD 10) + ORD ('0')); IF i = 0 THEN EXIT END; DEC (i); x := x DIV 10; IF x = 0 THEN EXIT END END END CardToString; PROCEDURE Skip; BEGIN WriteLn; pos := 0; Spaces (LeftMargin); WriteString (FirstLine); Spaces (FillUp); WriteString ('page :'); WriteCard (page, 5); Spaces (FillUp); WriteString (DateString); Write (ASCII.FF); WriteBf; SomethingPrinted := FALSE; INC (page); line := 0; LineFeed (TopMargin) END Skip; PROCEDURE LineFeed (n : CARDINAL); BEGIN pos := 0; WHILE n > 0 DO WriteLn; INC (line); IF line > MaxLine THEN Skip END; DEC (n) END END LineFeed; PROCEDURE ReadLine (VAR str : ARRAY OF CHAR); VAR i : CARDINAL; ch : CHAR; BEGIN i := 0; LOOP Read (ch); IF ch = ASCII.LF THEN EXIT END; str [i] := ch; INC (i) END; IF i <= HIGH (str) THEN str[i] := 0C END END ReadLine; PROCEDURE WriteLine (str : ARRAY OF CHAR); VAR i : CARDINAL; ch : CHAR; BEGIN Spaces (LeftMargin); i := 0; LOOP ch := str [i]; IF ch = 0C THEN EXIT END; Write (ch); INC (i); IF i > HIGH (str) THEN EXIT END; INC (pos); IF pos > MaxPos THEN LineFeed (1); Spaces (LeftMargin) END END; SomethingPrinted := TRUE; LineFeed (1) END WriteLine;
Convert Unix time to ASCII
What follows is a rather flexible function: MakeDate. MakeDate will produce a text output date string in several formats. The options here are
PROCEDURE MakeDate (Kind : ChronosMode; VAR str : ARRAY OF CHAR); VAR Dity, Ditm, days, year, secs, mins, hour, month : CARDINAL; tix : INTEGER; substr : ARRAY [0..1] OF CHAR; yrstr : ARRAY [0..3] OF CHAR; BEGIN time (tix); secs := tix MOD 60; mins := (tix MOD 3600) DIV 60; hour := (tix MOD 86400) DIV 3600; days := (tix DIV 86400) + 1; (* 1-1-70 = day 0... *) year := 1970; (* Dity = Days in this year *) LOOP IF year MOD 4 = 0 THEN Dity := 366 ELSE Dity := 365 END; IF days > Dity THEN DEC (days, Dity); INC (year) ELSE EXIT END END; (* year is now correct *) month := 1; LOOP (* Ditm = Days in this month *) CASE month OF 1, 3, 5, 7, 8, 10, 12 : Ditm := 31 | 4, 6, 9, 11 : Ditm := 30 | ELSE IF year MOD 4 = 0 THEN Ditm := 29 ELSE Ditm := 28 END END; IF days > Ditm THEN DEC (days, Ditm); INC (month) ELSE EXIT END END; (* month is now correct *) CASE Kind OF Hours : CardToString (hour, str) | Minutes : CardToString (mins, str) | Seconds : CardToString (secs, str) | Day : CardToString (days, str) | Month : CASE month OF 1 : Assign (str, 'Jan') | 2 : Assign (str, 'Feb') | 3 : Assign (str, 'Mar') | 4 : Assign (str, 'Apr') | 5 : Assign (str, 'May') | 6 : Assign (str, 'Jun') | 7 : Assign (str, 'Jul') | 8 : Assign (str, 'Aug') | 9 : Assign (str, 'Sep') | 10 : Assign (str, 'Oct') | 11 : Assign (str, 'Nov') | 12 : Assign (str, 'Dec') | ELSE Assign (str, 'ERR') (* Just to be sure... *) END | Year : CardToString (year, str) | Date : MakeDate (Month, str); Append (str, ' '); CardToString (days, substr); Append (str, substr); Append (str, ', '); CardToString (year, yrstr); Append (str, yrstr); ELSE EmptyString (str); CardToString (hour, substr); Append (str, substr); Append (str, ':'); CardToString (mins, substr); IF substr  = ' ' THEN substr  := '0' END; Append (str, substr); Append (str, ':'); CardToString (secs, substr); IF substr  = ' ' THEN substr  := '0' END; Append (str, substr) END END MakeDate; PROCEDURE Init; BEGIN IF ConfigureSoup () = FALSE THEN MaxLine := maxLINE; TopMargin := topMARGIN; MaxPos := maxPOS; LeftMargin := leftMARGIN; PrReset  := ASCII.ESC; Append (PrReset, 'E'); (* Printer reset *) PrSetup  := ASCII.ESC; Append (PrSetup, '(10U'); Append (PrSetup, ASCII.ESC); Append (PrSetup, '(s0p16.67h8.5v0s0b0T'); Append (PrSetup, ASCII.ESC); Append (PrSetup, '&l8D') END; WriteString (PrReset); WriteString (PrSetup); line := 0; page := 1; pos := 0; LineFeed (TopMargin); SomethingPrinted := FALSE; MakeDate (Date, DateString) END Init; PROCEDURE WrapUp; BEGIN IF SomethingPrinted = TRUE THEN LineFeed (MaxLine - line); Skip END; Write (ASCII.ESC); Write ('E'); (* Printer reset *) WriteBf END WrapUp;
Debugging routines (* commented out *).
The two upcoming PROCEDURES are commented out. They were only needed for debugging purposes.
After these two redundant functions, the main loop starts.
(* PROCEDURE ShowLoading; (* Show the values loaded by ConfigureSoup *) BEGIN WriteCard (MaxLine, 6); WriteLn; WriteCard (MaxPos, 6); WriteLn; WriteCard (LeftMargin, 6); WriteLn; WriteCard (TopMargin, 6); WriteLn; WriteString (PrReset); WriteLn; WriteString (PrSetup); WriteLn; END ShowLoading; *) (* PROCEDURE TestMakeDate; (* Procedure for testing MakeDate functionality *) BEGIN MakeDate (Year, string); WriteString (string); WriteLn; MakeDate (Month, string); WriteString (string); WriteLn; MakeDate (Day, string); WriteString (string); WriteLn; MakeDate (Hours, string); WriteString (string); WriteLn; MakeDate (Minutes, string); WriteString (string); WriteLn; MakeDate (Seconds, string); WriteString (string); WriteLn; WriteLn; MakeDate (Date, string); WriteString (string); WriteLn; MakeDate (Time, string); WriteString (string); WriteLn; END TestMakeDate; *) BEGIN Init; ReadLine (FirstLine); WriteLine (FirstLine); FillUp := (MaxPos - 12 - Length (FirstLine) - Length (DateString)) DIV 2; LOOP ReadLine (buffer); WriteLine (buffer); IF EOF () = TRUE THEN EXIT END END; WrapUp; END soup04.
Changes in soup versions above 04.
|Soup05||Formfeed||If a line contains the phrase '<FormFeed>', that line WILL be printed, but it will also induce a SKIP operation.|
The new home of soup.rc is in the /usr/local/prut directory.
This is done since I want to make some more PRinter UTillities. Future projects will store their own configuration files in that directory too.
The ReadLine PROCEDURE was adapted to accomodate reading from file or from StdIn.
The function now takes up twice the amount of lines of source, but I wanted to make reading from either device as fast as possible. If you feel that I made a sloppy program, please improve it. And keep the changes to yourself.
The Init PROCEDURE initializes some new BOOLEAN variables like 'FileIO' and 'exhausted'. The first one
signals to ReadLine that input is from File or from stream.
The second one is needed by the MAIN loop since a file EOF is noticed different from a stream EOF. Therefore ReadLine signals either EOF in it's own READ function and signals the rest of the program via this variable.
|MAIN loop||In the MAIN loop are two changes. The first one deals with FormFeed detection and the second one is that the 'LOOP' / 'END' construct is replace by a 'REPEAT' / 'UNTIL exhausted' control statement.|
|Soup06||Place a marker||Soup now inserts a marker (an ASCII.CR) at the end of the text section of the souped file, to mark where the start of the printer reset string starts. Since UNIX ignores the ASCII.CR, this has no effect on the printing, should the CR survice EMIT.|
|Project name||If FileIO = TRUE then the projectname (the name printed in the footer of each page) is derived from the filename. If not, the first line of the input stream is used.|
|Soup07||Handle non-UNIX files||
Files from older operating systems like DOS and WinDOS are almost always terminated by a CR/LF pair. Unix
just ignores the ASCII.CR but EMIT doesn't....
Therefore soup strips all CR's from the source file. The ASCII.CR which is placed as a marker remains.
The soup.rc configuration file.
Below you see the file soup.rc listed. It is full with comments, so I won't explain anything. It should be obvious after some thoughts.
Soup.rc This is the configuration file for the program 'soup'. All parameters in this file are entered between the 'BEGIN' and 'END' statements below. All text before 'BEGIN' and after 'END' will be ignored. Please retain the syntax of the parameter settings. The strings can be of arbitrary length for different printers. Therefore the parameters PrReset and PrSetup are considered as 'compound statements'. The keyword starts the interpretation of the string and it is terminated by the first encountered 'END' statement. Do not place comments in the section between BEGIN/END. If you need to supply comments, clarifications or (even worse): names, please do so outside the 'payload' of this configuration file. BEGIN TopMargin 1 LeftMargin 8 MaxLine 82 MaxPos 120 PrReset <ESC> E END PrSetup <ESC> (10U <ESC> (s0p16.67h8.5v0s0b0T <ESC> &l8D END END END TopMargin nr of lines to skip at top of page LeftMargin nr of spaces to insert at start of each line MaxLine when this line is reached, soup initiates a SKIP procedure MaxPos if a very long line is encountered, and this horizontal position is reached, then a line wrap is forced, but WITH the specified indentation PrReset Freeform string to initiate a printer RESET by software This string must be ended by an END statement. PrSetup Free form string to setup the printer for the desired font and lettertype. This string can be any length and hence must be ended by an END statement. If you need special tokens, make a choice from the following list: <ESC> = ASCII Escape character (27) <_> = ASCII space (32) <Ctrl> X = Control character where X = '@..Z' and '[\]^_' The <Ctrl> must be entered as such in this file. The 'X' is just an example. <Ctrl> @ = 0 <Ctrl> H = 8 <Ctrl> P = 16 <Ctrl> X = 24 <Ctrl> A = 1 <Ctrl> I = 9 <Ctrl> Q = 17 <Ctrl> Y = 25 <Ctrl> B = 2 <Ctrl> J = 10 <Ctrl> R = 18 <Ctrl> Z = 26 <Ctrl> C = 3 <Ctrl> K = 11 <Ctrl> S = 19 <Ctrl> [ = 27 <Ctrl> D = 4 <Ctrl> L = 12 <Ctrl> T = 20 <Ctrl> \ = 28 <Ctrl> E = 5 <Ctrl> M = 13 <Ctrl> U = 21 <Ctrl> ] = 29 <Ctrl> F = 6 <Ctrl> N = 14 <Ctrl> V = 22 <Ctrl> ^ = 30 <Ctrl> G = 7 <Ctrl> O = 15 <Ctrl> W = 23 <Ctrl> _ = 31 DO make sure there is a SPACE between the '<Ctrl>' prefix and the control code. The space is ESSENTIAL. Also around the <ESC> and <_> special tokens. I know this is a lot of fuzz, but I have to take into account that the makers of printer control codes used every conceivable character for some silly printer command. So I have to revert to this kind of measure. If you enter a string like: <ESC> E the printer command string will consist of just TWO characters: ESC and E. If you enter a string like <ESC> ( <Ctrl> P <_> <Ctrl> @ <Ctrl> @ the setup string will contain 6 tokens: 27, '(', 16, ' ', 0, 0 This is the end of the configuration file for soup, which should be located as /usr/local/prut/soup.rc In case of problems, contact the maintainer of this executable.
Page created 2003,