Revisiting Files

By R. S. Doiel, 2021-11-22

In October I had an Email exchange with Algojack regarding a buggy example in Oberon-07 and the file system. The serious bug was extraneous non-printable characters appearing a plain text file containing the string “Hello World”. The trouble with the example was a result of my misreading the Oakwood guidelines and how Files.WriteString() is required to work. The Files.WriteString() procedure is supposed to write every element of a string to a file. This includes the trailing Null character. The problem for me is Files.WriteString() litters plain text files with tailing nulls. What I should have done was write my own WriteString() and WriteLn(). The program HelloworldFile below is a more appropriate solution to writing strings and line endings than relying directly on Files. In a future post I will explorer making this more generalized in a revised “Fmt” module.

MODULE HelloworldFile;

IMPORT Files, Strings;

CONST OberonEOL = 1; UnixEOL = 2; WindowsEOL = 3;

VAR
  (* holds the eol marker type to use in WriteLn() *)
  eolType : INTEGER;
  (* Define a file handle *)
    f : Files.File;
  (* Define a file rider *)
    r : Files.Rider;

PROCEDURE WriteLn(VAR r : Files.Rider);
BEGIN
  IF eolType = WindowsEOL THEN
    (* A DOS/Windows style line ending, LFCR *)
    Files.Write(r, 13);
    Files.Write(r, 10);
  ELSIF eolType = UnixEOL THEN
     (* Linux/macOS style line ending, LF *)
     Files.Write(r, 10);
  ELSE
    (* Oberon, RISC OS style line ending, CR *)
    Files.Write(r, 13);
  END;
END WriteLn;

PROCEDURE WriteString(VAR r : Files.Rider; s : ARRAY OF CHAR);
  VAR i : INTEGER;
BEGIN
  i := 0;
  WHILE i < Strings.Length(s) DO
    Files.Write(r, ORD(s[i]));
    INC(i);
  END;
END WriteString;

BEGIN
  (* Set the desired eol type to use *)
  eolType := UnixEOL;
  (* Create our file, New returns a file handle *)
  f := Files.New("helloworld.txt"); ASSERT(f # NIL);
  (* Register our file with the file system *)
  Files.Register(f);
  (* Set the position of the rider to the beginning *)
  Files.Set(r, f, 0);
  (* Use the rider to write out "Hello World!" followed by a end of line *)
  WriteString(r, "Hello World!");
  WriteLn(r);
  (* Close our modified file *)
  Files.Close(f);
END HelloworldFile.

I have two new procedures “WriteString” and “WriteLn”. These mimic the parameters found in the Files module. The module body is a bit longer.

Compare this to a simple example of “Hello World” using the Out module.

MODULE HelloWorld;

IMPORT Out;

BEGIN
  Out.String("Hello World");
  Out.Ln;
END HelloWorld.

Look at the difference is in the module body. I need to setup our file and rider as well as pick the type of line ending to use in “WriteLn”. The procedures doing the actual work look very similar, “String” versus “WriteString” and “Ln” versus “WriteLn”.

Line ends vary between operating systems. Unix-like systems usually use a line feed. DOS/Windows systems use a carriage return and line feed. Oberon Systems use only a carriage return. If we’re going to the trouble of re-creating our “WriteString” and “WriteLn” procedures it also makes sense to handle the different line ending options. In this case I’ve chosen to use an INTEGER variable global to the module called “eolType”. I have a small set of constants to indicate which line ending is needed. In “WriteLn” I use that value as a guide to which line ending to use with the rider writing to the file.

The reason I chose this approach is because I want my writing procedures to use the same procedure signatures as the “Files” module. In a future post I will explore type conversion and a revised implementation of my “Fmt” module focusing on working with plain text files.

Aside from our file setup and picking an appropriate end of line marker the shape of the two programs look very similar.

References and resources

You can see a definition of the Files at Karl Landström’s documentation for his compiler along with the definitions for In and Out.

Next & Previous