COBOL sequential files: read a fixed-width file to end of file
SELECT, FD, OPEN, the priming READ and AT END: a complete program reads a 60-byte customer file to end of file and prints control totals; data and copybook ship.
Reading a file to end of file is the first thing every batch program does, and the COBOL shape of it has not changed in decades: a SELECT that names the file, an FD that describes the record, an OPEN, a priming READ, a loop that runs until AT END fires, and a CLOSE. This tutorial does that for a 60-byte fixed-width customer file, with the file, its copybook and a Python script that generated it all shipped, so the whole thing runs as a unit. The COBOL was desk-checked against the GnuCOBOL 3.2 Programmer's Guide and not compiled here; the control totals in the worked example come from the Python script, which we did run. The record is a group item as described in group items and elementary items, and it is pulled in with COPY, which inserts the copybook's text into the program before compilation.
The program
>>SOURCE FORMAT IS FREE
*> cobol-read-sequential-file.cob — read a fixed-width customer file
*> to end of file and print control totals
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked against the GnuCOBOL 3.2 Programmer's Guide;
*> NOT compiled (no cobc on the authoring machine)
*> Inputs : datasets/cobol-read-sequential-file.dat (LINE SEQUENTIAL,
*> LF line endings, 60 bytes per record, 12 records)
*> code/cobol-read-sequential-file.cpy (the record layout)
*> Outputs: one DISPLAY line per record plus four control-total lines
*> Build : cobc -x -I code -o read-customers code/cobol-read-sequential-file.cob
*> (run from the folder that holds code/ and datasets/)
*> Run : ./read-customers (Windows: read-customers.exe)
IDENTIFICATION DIVISION.
PROGRAM-ID. READ-CUSTOMERS.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT Customer-File
ASSIGN TO "datasets/cobol-read-sequential-file.dat"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-File-Status.
DATA DIVISION.
FILE SECTION.
FD Customer-File.
COPY "cobol-read-sequential-file.cpy".
WORKING-STORAGE SECTION.
77 WS-File-Status PIC XX.
01 WS-Flags.
05 WS-EOF-Flag PIC X VALUE "N".
88 End-Of-File VALUE "Y".
77 WS-Record-Count PIC 9(5) VALUE 0.
77 WS-Debit-Total PIC 9(9)V99 VALUE 0.
77 WS-Credit-Total PIC 9(9)V99 VALUE 0.
77 WS-Net PIC S9(9)V99 VALUE 0.
77 WS-Show-Amount PIC Z(6)9.99.
77 WS-Show-Total PIC Z(8)9.99.
77 WS-Show-Net PIC -(9)9.99.
77 WS-Show-Count PIC ZZZZ9.
PROCEDURE DIVISION.
Main-Paragraph.
OPEN INPUT Customer-File
IF WS-File-Status NOT = "00"
DISPLAY "Open failed, file status " WS-File-Status
STOP RUN
END-IF
*> Priming read, then loop while the last read succeeded.
PERFORM Read-Next-Customer
PERFORM UNTIL End-Of-File
PERFORM Process-Customer
PERFORM Read-Next-Customer
END-PERFORM
CLOSE Customer-File
PERFORM Show-Totals
STOP RUN.
Read-Next-Customer.
READ Customer-File
AT END
SET End-Of-File TO TRUE
NOT AT END
ADD 1 TO WS-Record-Count
END-READ
IF WS-File-Status NOT = "00" AND WS-File-Status NOT = "10"
DISPLAY "Read failed, file status " WS-File-Status
STOP RUN
END-IF.
Process-Customer.
MOVE Cust-Balance TO WS-Show-Amount
DISPLAY Cust-Id " " Cust-Name " " Cust-City " "
WS-Show-Amount " " Cust-DC
EVALUATE Cust-DC
WHEN "D"
ADD Cust-Balance TO WS-Debit-Total
WHEN "C"
ADD Cust-Balance TO WS-Credit-Total
WHEN OTHER
DISPLAY "Unexpected flag [" Cust-DC "] on " Cust-Id
END-EVALUATE.
Show-Totals.
MOVE WS-Record-Count TO WS-Show-Count
DISPLAY "Records read : " WS-Show-Count
MOVE WS-Debit-Total TO WS-Show-Total
DISPLAY "Debit total : " WS-Show-Total
MOVE WS-Credit-Total TO WS-Show-Total
DISPLAY "Credit total : " WS-Show-Total
COMPUTE WS-Net = WS-Debit-Total - WS-Credit-Total
MOVE WS-Net TO WS-Show-Net
DISPLAY "Net owed : " WS-Show-Net.
The copybook, shipped as /code/cobol-read-sequential-file.cpy, is the record description that COPY pulls into the FD:
01 Customer-Record.
05 Cust-Id PIC 9(6).
05 Cust-Name PIC X(25).
05 Cust-City PIC X(15).
05 Cust-Balance PIC 9(7)V99.
05 Cust-DC PIC X.
05 FILLER PIC X(4).
Record layout
| Column | Positions | PIC | Meaning |
|---|---|---|---|
| Cust-Id | 1 to 6 | 9(6) | Customer number |
| Cust-Name | 7 to 31 | X(25) | Name, left-justified, space-padded |
| Cust-City | 32 to 46 | X(15) | City, left-justified, space-padded |
| Cust-Balance | 47 to 55 | 9(7)V99 | Balance in cents as nine digits, no point, no sign |
| Cust-DC | 56 | X | D if the customer owes the balance, C if they are in credit |
| FILLER | 57 to 60 | X(4) | Reserved, spaces |
Every record is exactly 60 bytes followed by a line feed. The balance carries no sign on purpose. A signed S9(7)V99 field in a file would need an agreed sign encoding, either an overpunched last digit, which differs between ASCII and EBCDIC, or a separate sign character, and the person producing the file in another language would have to get that exactly right. A separate debit-or-credit flag in its own byte is unambiguous, easy to produce from Python or anything else, and easy to test in COBOL with EVALUATE. Many real interface files are designed this way for the same reason.
Line by line
The SELECT lives in the FILE-CONTROL paragraph of the INPUT-OUTPUT SECTION. Section 5.2.1 of the Programmer's Guide says the statement "creates a definition of a file and links that COBOL definition to the external operating system environment." ASSIGN TO with a literal gives the path relative to the directory the program runs in. ORGANIZATION IS LINE SEQUENTIAL matters: section 5.2.1.2 explains that these files "consist of records terminated by an end-of-record delimiter character or character sequence" and "may be created with any standard text-editing or word processing software." Plain ORGANIZATION IS SEQUENTIAL (section 5.2.1.1) means records with no delimiters, located by byte offset, which is what a mainframe fixed-block dataset looks like and what a text editor cannot produce. IBM's ORGANIZATION clause page documents both and notes that a line sequential record "can consist only of printable characters." Since the Python script writes ordinary text lines, line sequential is the honest choice. FILE STATUS names a two-character field that every I/O statement updates.
The FD introduces the file's record area, and the COPY statement drops the copybook's 01 level there. The READ statement, per section 7.8.30.1, places each record "into the 01-level record structure(s) that immediately follow the file's FD", so after each successful READ the fields Cust-Id, Cust-Name and the rest simply contain the current record. There is no parsing step; the layout is the parser.
OPEN INPUT is checked immediately. Section 7.8.28 lists the open modes, and INPUT is the one under which "only the CLOSE, READ, START and UNLOCK statements will be allowed." If the file is missing, the status is 35, listed in the guide's status table as "File not found", and the program says so and stops instead of failing on the first READ.
The loop uses the priming-read pattern. Read-Next-Customer runs once before the loop, and the loop body processes the record then reads the next one. That ordering is what keeps AT END from processing an empty buffer: when the read hits end of file, it sets the 88-level flag, the PERFORM UNTIL End-Of-File test sees it, and the loop exits without a further Process-Customer. Section 7.8.30.1 describes AT END as the clause that detects "an end-of-file (i.e. no more records) condition" and NOT AT END as the branch taken on a status of 00; the record count is incremented in the NOT AT END branch so it counts exactly the records processed. After the READ, the paragraph also checks the status field for anything other than 00 or 10 (10 is end of file), which is how a permanent I/O error is caught; IBM's programming guide recommends the same habit, checking the key "after each input or output statement."
Process-Customer prints the record with the balance passed through an edited picture and adds the balance to one of two totals depending on the flag. Show-Totals prints the count and the totals through edited fields and computes the net through a signed work field, because the debit and credit totals are both unsigned and the difference could in principle be negative.
Compile and run
python code/cobol-read-sequential-file.py
cobc -x -I code -o read-customers code/cobol-read-sequential-file.cob
./read-customers
Run all three from the folder that contains code/ and datasets/. The -I code option tells cobc where to find the copybook named in the COPY statement, and the -o option names the executable so the path in the ASSIGN clause resolves from the same working directory. The Python script rewrites the data file identically each time and prints the totals the COBOL program should reproduce; it is shipped as /code/cobol-read-sequential-file.py.
Worked example
The Python generator, which we ran, reported twelve records, a debit total of 28645.80, a credit total of 2463.45 and a net of 26182.35. The COBOL program's expected output, derived by hand from the listing and those figures, is:
100001 MARGARET OKONKWO GALWAY 1250.75 D
100002 TOMAS LINDQVIST MALMO 0.00 D
100003 AISHA RAHMAN LEEDS 318.40 C
100004 DIEGO FERNANDEZ SOTO SEVILLA 9875.00 D
100005 HANNAH BRENNAN CORK 42.19 D
100006 KWAME MENSAH ACCRA 1500.00 C
100007 YUKI TANAKA OSAKA 77.77 D
100008 LUCAS MOREIRA PORTO 2200.10 D
100009 ELENA PETROVA VARNA 5.05 C
100010 SAMUEL ADEYEMI LAGOS 12000.00 D
100011 NORA HAUGEN BERGEN 640.00 C
100012 PRIYA NAIR KOCHI 3199.99 D
Records read : 12
Debit total : 28645.80
Credit total : 2463.45
Net owed : 26182.35
The first data line in the file is 100001MARGARET OKONKWO GALWAY 000125075D, and you can match each field to the record layout by counting positions: the balance "000125075" at positions 47 to 55 is 1250.75 once the implied decimal point is honoured, and the D at position 56 sends it to the debit total.
File status values to know
| Status | Meaning in the Programmer's Guide table | When you will see it here |
|---|---|---|
| 00 | Success | Every good OPEN and READ |
| 10 | End of file reached | The final READ, which triggers AT END |
| 35 | File not found | OPEN INPUT when the data file is missing |
| 30 | Permanent I/O error | Hardware or permission trouble during READ |
The guide warns that its list "is not definitive" and that tests "should include one for non zeros as a catch all", which is what the second check in Read-Next-Customer does.
Limitations
A line sequential read pads short lines with spaces and truncates long ones, per section 5.2.1.2, so a malformed line does not raise an error; it produces a record with spaces in numeric fields, and arithmetic on those is undefined. This program trusts its input because the generator guarantees the layout; a production program would put a NUMERIC class test on each amount before the ADD, as the REDEFINES lesson shows. Line endings are another difference between platforms: the guide says GnuCOBOL built on MinGW or native Windows writes carriage return plus line feed, while Unix builds write a line feed, and on reading both are accepted as delimiters; the shipped file uses a line feed only. Finally, the balance is unsigned in the file by design, which means a genuinely negative balance cannot be represented except through the flag; if your data carries signs, the record layout must say how, and SIGN IS LEADING SEPARATE is the readable option.
Index terms cobol, gnucobol, sequential file, read, file status