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.
| |
The input example needs a value on standard input. Add the interactive -i flag and pipe a number in with echo:
| |
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
PUTandGETare the universal I/O verbs — they write to and read from the console by default, or from a named file when you addFILE(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 likeA,A(w),F(w),F(w,d), andX(n)to control exact field widths, decimal places, and spacing for aligned tables. PUT SKIPadvances to a new line before writing; aPUTwithoutSKIPcontinues on the current line. LeavingSKIPoff the first statement avoids a stray leading blank line.- File I/O uses a
FILE STREAMconstant opened withOPEN FILE(f) TITLE('name'), associating the PL/I file with a physical filename, and released withCLOSE 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 OUTPUTorSTREAM 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
Comments
Loading comments...
Leave a Comment