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;
Explanation 1:
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 [0] := 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 [0] := CHR (ORD (ch) - ORD ('@'));
option [1] := 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.
More sourcecode
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 [0] = ' ' THEN substr [0] := '0' END;
Append (str, substr); Append (str, ':');
CardToString (secs, substr); IF substr [0] = ' ' THEN substr [0] := '0' END;
Append (str, substr)
END
END MakeDate;
PROCEDURE Init;
BEGIN
IF ConfigureSoup () = FALSE THEN
MaxLine := maxLINE; TopMargin := topMARGIN;
MaxPos := maxPOS; LeftMargin := leftMARGIN;
PrReset [0] := ASCII.ESC; Append (PrReset, 'E'); (* Printer reset *)
PrSetup [0] := 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. |
| CFG file |
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. |
|
| ReadLine |
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. |
|
| Init |
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,
Page equipped with FroogleBuster technology