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
	soup <infile | lpr
	soup infile | lpr
From '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 *)


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:

may be used intermixed since Mocka says that these types are compatible. Which is the case, since all these VAR's take up 32 bits.
Apart from the CARDINAL / INTEGER and the LONGCARD / LONGINT there are the SHORTCARD and the SHORTINT which take up a mere 16 bits. Such a SHORTxxx of course is neat, but why use 16 bit values with the possibility of an overflow and a wrap around if 32 bit arithmatic is available free of charge?

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);

   PutString (StdErr, mess);
   PutLn (StdErr);    	    	PutBf (StdErr)
END ErrorMessage;

PROCEDURE GetCtrlStr (infile     : File; VAR str     : ARRAY OF CHAR);

VAR   ch     	     : CHAR;

   str [0] := 0C;		(* Clear string		*)
      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
            GetChar (infile, ch)
	 UNTIL ch > ' ';
	 option [0] := CHR (ORD (ch) - ORD ('@'));
	 option [1] := 0C;
	 Append (str, option)
         Append (str, option)
END GetCtrlStr;

PROCEDURE ConfigureSoup () : BOOLEAN;

VAR   InFile		: File;

   OpenInput (InFile, '/usr/local/soup/soup.rc');
   IF Done () = TRUE THEN
         GetString (InFile, option)
      UNTIL StrEq (option, 'BEGIN');
      	 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)
	    ErrorMessage ('Invalid parameter in "/usr/local/soup/soup.rc".');
	    ErrorMessage ('Reading aborted, assuming HP Laserjet IIP.');
      Close (InFile)
      ErrorMessage ("No file '/usr/local/soup/soup.rc'. Assuming HP Laserjet IIP.");
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.
If the soup.rc file exists, it is opened and read. The active part of soup.rc is enclosed between a 'BEGIN' and an 'END'. Everything else (and there's a lot) is comment and explanation.

More sourcecode

PROCEDURE Spaces (n  : CARDINAL); 		 (* Print n spaces  *)

VAR    i	     : CARDINAL;

    FOR i := 1 TO n DO
    	Write (' ');
	INC (pos)
END Spaces;


VAR    i  	     : CARDINAL;
       ch 	     : CHAR;

   FOR i := 0 TO HIGH (str) DO  str [i] := ' '  END;   
   i := HIGH (str);
      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 CardToString;


    pos := 0;
    Spaces (LeftMargin);        WriteString (FirstLine);	
    Spaces (FillUp);    	WriteString ('page :');		WriteCard (page, 5);
    Spaces (FillUp);    	WriteString (DateString);    	Write (ASCII.FF);

    SomethingPrinted := FALSE; 	INC (page);    	line := 0;
    LineFeed (TopMargin)
END Skip;


    pos := 0;
    WHILE n > 0 DO
      INC (line);
      IF line > MaxLine THEN  Skip  END;
      DEC (n)
END LineFeed;


VAR   i            : CARDINAL;
      ch           : CHAR;

   i := 0;
      Read (ch);
      str [i] := ch;
      INC (i)
   IF i <= HIGH (str) THEN str[i] := 0C END
END ReadLine;


VAR   i             : CARDINAL;
      ch            : CHAR;
   Spaces (LeftMargin);
   i := 0;
      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)
   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

Each of the latter 6 produces a string with the required number.
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;

   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  *)
      IF year MOD 4 = 0 THEN Dity := 366 ELSE Dity := 365 END;
      IF days > Dity THEN
      	 DEC (days, Dity);
	 INC (year)
   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    |
         IF year MOD 4 = 0 THEN
	    Ditm := 29
	    Ditm := 28
      IF days > Ditm THEN
      	 DEC (days, Ditm);
	 INC (month)
   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')	|
	        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);
      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 MakeDate;


    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')
    WriteString (PrReset);	       WriteString (PrSetup);
    line := 0;
    page := 1;
    pos  := 0;
    LineFeed (TopMargin);
    SomethingPrinted := FALSE;
    MakeDate (Date, DateString)
END Init;


   IF SomethingPrinted = TRUE  THEN
      LineFeed (MaxLine - line);
   Write (ASCII.ESC);   	Write ('E');		(*  Printer reset  *)
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 *)

   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  *)

     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;
     MakeDate (Date, string);		WriteString (string);		WriteLn;
     MakeDate (Time, string);		WriteString (string);		WriteLn;
END TestMakeDate;    			*)

   ReadLine (FirstLine);
   WriteLine (FirstLine);
   FillUp := (MaxPos - 12 - Length (FirstLine) - Length (DateString)) DIV 2;
      ReadLine (buffer);
      WriteLine (buffer);
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.


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.


TopMargin	  1
LeftMargin	  8
MaxLine		 82
MaxPos		120

PrReset		<ESC> E

PrSetup		<ESC> (10U
		<ESC> (s0p16.67h8.5v0s0b0T
		<ESC> &l8D


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


In case of problems, contact the maintainer of this executable.

Page created 2003,