Intermediate

I/O Operations in Modula-2

Read and write console and file data in Modula-2 with gm2 using StrIO, NumberIO, StdIO, and the FIO file module - formatted output, input, and file handling.

Input and output are where a program meets the outside world. As an imperative, procedural language, Modula-2 treats I/O as a set of library procedures rather than built-in statements — you import exactly the procedures you need and call them in sequence. This keeps the core language small while still giving you full control over terminals and files.

Modula-2 has no single “print” keyword. Instead, the standard library splits I/O across focused modules, each with a clear responsibility. In the GNU Modula-2 (gm2) base libraries used throughout this series, StrIO handles strings, NumberIO handles integers and cardinals, StdIO handles single characters, and FIO provides low-level file access with explicit File handles. Because imports are qualified and explicit, any reader of your code can see precisely which I/O operations a module performs.

In this tutorial you’ll go beyond the single WriteString call from Hello World. You’ll produce formatted, aligned console output, read values typed at the keyboard, and write and read text files with FIO. Along the way you’ll see how Modula-2’s strong typing shapes I/O: cardinals and integers have distinct output procedures, strings are character arrays, and file handles are opaque values you pass between calls.

Formatted Console Output

StrIO.WriteString prints text and NumberIO.WriteInt / NumberIO.WriteCard print numbers. The numeric procedures take a second argument — a field width — that right-justifies the value in a column, which makes building aligned tables straightforward. StdIO.Write emits a single character, useful for drawing or character-by-character output.

Create a file named formatted_output.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
MODULE FormattedOutput;

FROM StrIO IMPORT WriteString, WriteLn;
FROM NumberIO IMPORT WriteInt, WriteCard;
FROM StdIO IMPORT Write;

VAR
  i: CARDINAL;

BEGIN
  WriteString("Formatted output in Modula-2");
  WriteLn;
  WriteLn;

  (* Integers right-justified in a field of width 6 *)
  WriteString("Right-justified integers:");
  WriteLn;
  WriteInt(5, 6);    WriteLn;
  WriteInt(42, 6);   WriteLn;
  WriteInt(-100, 6); WriteLn;
  WriteLn;

  (* A cardinal (unsigned) printed with no padding *)
  WriteString("Cardinal value: ");
  WriteCard(65535, 0);
  WriteLn;
  WriteLn;

  (* An aligned table built purely from field widths *)
  WriteString("Squares table:");
  WriteLn;
  FOR i := 1 TO 5 DO
    WriteCard(i, 3);
    WriteString("  ->");
    WriteCard(i * i, 5);
    WriteLn
  END;
  WriteLn;

  (* Character-by-character output with StdIO.Write *)
  WriteString("Row of stars: ");
  FOR i := 1 TO 10 DO
    Write('*')
  END;
  WriteLn
END FormattedOutput.

The second argument to WriteInt and WriteCard is the minimum field width. WriteInt(5, 6) pads 5 with five leading spaces; WriteInt(-100, 6) needs four characters for -100, so it gets two leading spaces. Passing 0 (as in WriteCard(65535, 0)) means “no padding” — print the digits and nothing more.

Reading Console Input

Reading input mirrors output: StrIO.ReadString reads a line of text into a character array, and NumberIO.ReadCard reads an unsigned number. ReadString reads up to the end of the line and consumes the trailing newline, so a following ReadCard starts cleanly on the next value.

Create a file named console_input.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MODULE ConsoleInput;

FROM StrIO IMPORT WriteString, WriteLn, ReadString;
FROM NumberIO IMPORT WriteCard, ReadCard;

VAR
  name: ARRAY [0..49] OF CHAR;
  age: CARDINAL;

BEGIN
  WriteString("Enter your name: ");
  ReadString(name);

  WriteString("Enter your age: ");
  ReadCard(age);

  WriteLn;
  WriteString("Hello, ");
  WriteString(name);
  WriteString("!");
  WriteLn;
  WriteString("Next year you will be ");
  WriteCard(age + 1, 0);
  WriteLn
END ConsoleInput.

Notice that name is declared as ARRAY [0..49] OF CHAR — Modula-2 strings are fixed-size character arrays, so you choose the capacity up front. ReadString stops at the newline (or when the array is full) and NUL-terminates the result, so WriteString(name) prints exactly what was typed.

Writing and Reading Files

For files, the gm2 base library provides the FIO module. Because FIO exports procedures named WriteString and ReadString that clash with the string procedures in StrIO, the cleanest approach is a qualified importIMPORT FIO; — so every file call is written as FIO.OpenToWrite, FIO.WriteString, and so on. This makes it obvious which calls touch the terminal and which touch a file.

A file is opened with FIO.OpenToWrite or FIO.OpenToRead, which return a FIO.File handle. You pass that handle to every read or write, then release it with FIO.Close. FIO.WriteLine writes a newline to the file.

Create a file named file_io.mod:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
MODULE FileIO;

FROM StrIO IMPORT WriteString, WriteLn;
IMPORT FIO;

VAR
  f: FIO.File;
  line: ARRAY [0..79] OF CHAR;

BEGIN
  (* Write three lines to a text file *)
  f := FIO.OpenToWrite("report.txt");
  FIO.WriteString(f, "Modula-2 was created in 1978.");
  FIO.WriteLine(f);
  FIO.WriteString(f, "It introduced true modules.");
  FIO.WriteLine(f);
  FIO.WriteString(f, "gm2 is part of GCC.");
  FIO.WriteLine(f);
  FIO.Close(f);

  WriteString("Wrote 3 lines to report.txt");
  WriteLn;
  WriteLn;

  (* Read the same three lines back *)
  WriteString("Contents of report.txt:");
  WriteLn;
  f := FIO.OpenToRead("report.txt");
  FIO.ReadString(f, line);
  WriteString("  "); WriteString(line); WriteLn;
  FIO.ReadString(f, line);
  WriteString("  "); WriteString(line); WriteLn;
  FIO.ReadString(f, line);
  WriteString("  "); WriteString(line); WriteLn;
  FIO.Close(f)
END FileIO.

FIO.ReadString reads one line at a time into the line array, discarding the newline. Here we read exactly the three lines we wrote. For files of unknown length, FIO also provides FIO.EOF(f), which returns TRUE once you read past the end — the natural condition for a WHILE NOT FIO.EOF(f) DO ... END loop. You can check that a file opened successfully with FIO.IsNoError(f) before reading or writing.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official image
docker pull codearchaeology/modula-2:latest

# Compile and run the formatted output example
docker run --rm -v $(pwd):/app -w /app codearchaeology/modula-2:latest \
  sh -c 'gm2 -o formatted_output formatted_output.mod && ./formatted_output'

# Compile and run the file I/O example (creates and reads report.txt)
docker run --rm -v $(pwd):/app -w /app codearchaeology/modula-2:latest \
  sh -c 'gm2 -o file_io file_io.mod && ./file_io'

# Compile and run the console input example, feeding it input over a pipe
printf 'Ada\n30\n' | docker run --rm -i -v $(pwd):/app -w /app codearchaeology/modula-2:latest \
  sh -c 'gm2 -o console_input console_input.mod && ./console_input'

The -i flag on the input example keeps STDIN open so the piped text reaches the running program. The volume mount (-v $(pwd):/app) means report.txt written by file_io appears in your current directory after the container exits.

Expected Output

formatted_output:

Formatted output in Modula-2

Right-justified integers:
     5
    42
  -100

Cardinal value: 65535

Squares table:
  1  ->    1
  2  ->    4
  3  ->    9
  4  ->   16
  5  ->   25

Row of stars: **********

file_io:

Wrote 3 lines to report.txt

Contents of report.txt:
  Modula-2 was created in 1978.
  It introduced true modules.
  gm2 is part of GCC.

console_input (with Ada and 30 piped in — the prompts print before each read, so they appear consecutively):

Enter your name: Enter your age: 
Hello, Ada!
Next year you will be 31

Key Concepts

  • I/O is library procedures, not keywords — Modula-2 keeps the core language small; you import StrIO, NumberIO, StdIO, and FIO procedures explicitly for the I/O you need.
  • Type-specific output — cardinals (WriteCard) and integers (WriteInt) have separate procedures, reflecting Modula-2’s strong distinction between signed and unsigned numbers.
  • Field widths give alignment for free — the second argument to WriteInt/WriteCard right-justifies values in a column; pass 0 for no padding.
  • Strings are fixed-size character arrays — declare ARRAY [0..N] OF CHAR with enough capacity; ReadString NUL-terminates the result and WriteString prints up to that terminator.
  • Files use explicit FIO.File handles — open with FIO.OpenToWrite/FIO.OpenToRead, pass the handle to every operation, and always FIO.Close when done.
  • Qualified imports avoid name clashes — importing FIO unqualified (IMPORT FIO;) keeps its WriteString/ReadString distinct from the StrIO versions and makes file access visible at every call site.
  • FIO.EOF and FIO.IsNoError handle the edges — loop with WHILE NOT FIO.EOF(f) for files of unknown length, and check FIO.IsNoError(f) after opening to detect failures.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/modula-2:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining