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


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:

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

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

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

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;

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,