Intermediate

I/O Operations in PL/I

Read and write console and file data in PL/I using stream I/O - PUT LIST, PUT EDIT, GET LIST, STREAM files, and the ENDFILE condition with the Iron Spring compiler

Input and output are where PL/I’s 1964 ambition really shows. Where FORTRAN forced you to write dense FORMAT statements and COBOL leaned on verbose DISPLAY verbs, PL/I gave programmers a layered I/O model: list-directed I/O for quick, human-readable work, and edit-directed I/O for precise, column-aligned formatting. The same statements — PUT and GET — write to the console or to files depending on whether you name a file.

As an imperative, procedural language, PL/I treats I/O as a sequence of explicit statements against files. A file is any named source or sink of data: the standard output is a file, the keyboard is a file, and a dataset on disk is a file. This tutorial covers console output beyond the simple PUT LIST you saw in Hello World, reading input with GET, writing and reading disk files with STREAM files, formatted output with edit-directed format items, and handling end-of-file with PL/I’s ON ENDFILE condition.

You’ll build four small programs, each demonstrating a distinct piece of the I/O model, and run them all with the Iron Spring PL/I compiler in Docker.

Console Output: LIST vs EDIT

PUT LIST is the friendly path — it converts any value to text and inserts spacing automatically. PUT EDIT is the precise path — you pair a data list with a format list where items like F(w) (fixed-point in a field of width w) and F(w,d) (fixed-point with d decimal places) give you exact control over column width and alignment.

Create a file named console_io.pli:

CONSOLE: PROCEDURE OPTIONS(MAIN);

   DECLARE NAME  CHARACTER(20) VARYING;
   DECLARE COUNT FIXED DECIMAL(4);
   DECLARE PRICE FIXED DECIMAL(7,2);

   NAME  = 'Widget';
   COUNT = 12;
   PRICE = 4.50;

   /* List-directed: concatenate into one clean string */
   PUT LIST('Product: ' || NAME);

   /* Edit-directed: F(w) and F(w,d) control the numeric fields */
   PUT SKIP EDIT('Quantity:', COUNT)       (A, F(6));
   PUT SKIP EDIT('Price:',    PRICE)       (A, F(9,2));
   PUT SKIP EDIT('Total:',    COUNT*PRICE) (A, F(9,2));

END CONSOLE;

The format list (A, F(9,2)) says: write the first item as characters (A), then write the second item as a fixed-point number right-justified in a 9-character field with 2 decimal places. Because the first PUT LIST has no SKIP, it stays on line one; each PUT SKIP advances to a new line before writing.

Console Input with GET

GET LIST reads whitespace-delimited values from standard input and converts them to the target’s type. Reading a number is unambiguous: PL/I parses the token and stores the numeric value.

Create a file named doubler.pli:

DOUBLER: PROCEDURE OPTIONS(MAIN);

   DECLARE N FIXED DECIMAL(5);

   PUT LIST('Enter a number: ');
   GET LIST(N);
   PUT SKIP EDIT('Double is:', N * 2) (A, F(6));

END DOUBLER;

The prompt is printed with PUT LIST (no trailing newline), then GET LIST(N) waits for a value on standard input. Since this program is non-interactive in Docker, we pipe a value into it below.

Writing and Reading Files

Disk I/O uses a file constant declared with FILE STREAM. You associate it with a physical filename using the TITLE option on OPEN, then PUT FILE(...) to write and GET FILE(...) to read. Reading stops at end-of-file, which PL/I signals through the ON ENDFILE condition — an early example of the structured exception handling PL/I pioneered.

This program writes three records to a file, then reads them back, echoes each, and totals the scores.

Create a file named scores.pli:

SCORES: PROCEDURE OPTIONS(MAIN);

   DECLARE OUTFILE FILE STREAM OUTPUT;
   DECLARE INFILE  FILE STREAM INPUT;
   DECLARE NAME    CHARACTER(20) VARYING;
   DECLARE SCORE   FIXED DECIMAL(5);
   DECLARE TOTAL   FIXED DECIMAL(7) INIT(0);
   DECLARE EOF     BIT(1) INIT('0'B);

   /* ---- Write three records to scores.txt ---- */
   OPEN FILE(OUTFILE) TITLE('scores.txt');
   PUT FILE(OUTFILE) SKIP LIST('Alice', 95);
   PUT FILE(OUTFILE) SKIP LIST('Bob',   82);
   PUT FILE(OUTFILE) SKIP LIST('Carol', 78);
   CLOSE FILE(OUTFILE);

   /* ---- Read them back, echo each, total the scores ---- */
   PUT LIST('Score Report');

   ON ENDFILE(INFILE) EOF = '1'B;

   OPEN FILE(INFILE) TITLE('scores.txt');
   GET FILE(INFILE) LIST(NAME, SCORE);        /* priming read */
   DO WHILE (EOF = '0'B);
      PUT SKIP EDIT(NAME, SCORE) (A(6), F(5));
      TOTAL = TOTAL + SCORE;
      GET FILE(INFILE) LIST(NAME, SCORE);     /* next record */
   END;
   CLOSE FILE(INFILE);

   PUT SKIP EDIT('Total score:', TOTAL) (A, F(5));

END SCORES;

The ON ENDFILE(INFILE) EOF = '1'B; statement registers a handler before any read happens. This is the classic priming read loop: read one record, then loop as long as the file isn’t exhausted, processing the current record and reading the next at the bottom. When the final GET hits end-of-file, the ON unit sets EOF to '1'B and the loop exits cleanly.

Formatted Tables with A, X, and F

Edit-directed I/O shines when building aligned tabular output. Beyond A (character) and F (fixed-point), the X(n) format item inserts n spaces, letting you lay out columns precisely.

Create a file named table.pli:

TABLE: PROCEDURE OPTIONS(MAIN);

   DECLARE NAME  CHARACTER(8) VARYING;
   DECLARE SCORE FIXED DECIMAL(3);

   NAME = 'Alice'; SCORE = 95;
   PUT EDIT(NAME, SCORE) (A(8), X(3), F(5));

   NAME = 'Bob';   SCORE = 82;
   PUT SKIP EDIT(NAME, SCORE) (A(8), X(3), F(5));

END TABLE;

Here A(8) writes the name left-justified in an 8-character field (padded with blanks), X(3) inserts a 3-space gutter, and F(5) right-justifies the score in a 5-character field. The result is a clean two-column layout that stays aligned regardless of the name’s length.

Running with Docker

Pull the image once, then compile and run each program with the plicc wrapper (which handles both compile and link). The executable is named after the source file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the PL/I compiler image
docker pull codearchaeology/pli:latest

# Console output example
docker run --rm --security-opt seccomp=unconfined -v $(pwd):/app -w /app codearchaeology/pli:latest sh -c 'plicc console_io.pli && ./console_io'

# File write/read example
docker run --rm --security-opt seccomp=unconfined -v $(pwd):/app -w /app codearchaeology/pli:latest sh -c 'plicc scores.pli && ./scores'

# Formatted table example
docker run --rm --security-opt seccomp=unconfined -v $(pwd):/app -w /app codearchaeology/pli:latest sh -c 'plicc table.pli && ./table'

The input example needs a value on standard input. Add the interactive -i flag and pipe a number in with echo:

1
2
# Console input example (pipe "21" to standard input)
echo '21' | docker run --rm -i --security-opt seccomp=unconfined -v $(pwd):/app -w /app codearchaeology/pli:latest sh -c 'plicc doubler.pli && ./doubler'

The --security-opt seccomp=unconfined flag is required for the 32-bit Iron Spring compiler on Docker Desktop, exactly as in the Hello World tutorial. After running scores, you’ll also find a scores.txt file in your directory — the file the program created.

Expected Output

console_io produces:

Product: Widget
Quantity:    12
Price:     4.50
Total:    54.00

doubler (with 21 piped to input) produces:

Enter a number: 
Double is:    42

scores produces:

Score Report
Alice    95
Bob      82
Carol    78
Total score:  255

table produces:

Alice         95
Bob           82

Key Concepts

  • PUT and GET are the universal I/O verbs — they write to and read from the console by default, or from a named file when you add FILE(name). The same statements serve both.
  • List-directed I/O (LIST) is automatic — it converts values to text, inserts spacing, and skips whitespace on input. It’s ideal for quick output and reading simple delimited values.
  • Edit-directed I/O (EDIT) is precise — pair a data list with a format list of items like A, A(w), F(w), F(w,d), and X(n) to control exact field widths, decimal places, and spacing for aligned tables.
  • PUT SKIP advances to a new line before writing; a PUT without SKIP continues on the current line. Leaving SKIP off the first statement avoids a stray leading blank line.
  • File I/O uses a FILE STREAM constant opened with OPEN FILE(f) TITLE('name'), associating the PL/I file with a physical filename, and released with CLOSE FILE(f).
  • ON ENDFILE(f) handles end-of-file as a condition, not a return code — register the handler before reading, then use a priming-read loop to process records until the file is exhausted.
  • Declare direction on the file (STREAM OUTPUT or STREAM INPUT) so the compiler enforces correct usage — writing to an input file or reading an output file is caught as an error.

Running Today

All examples can be run using Docker:

docker pull codearchaeology/pli:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining