COBOL Textbook

COBOL copybooks: COPY, REPLACING, and one layout in three programs

One 39-byte copybook in three GnuCOBOL programs: plain COPY, REPLACING LEADING, and REPLACE, with where GnuCOBOL and IBM differ on search paths and nesting.

Published
Updated
Dialect
GnuCOBOL 3.x, free format
Status
Compiled and run with GnuCOBOL 3.2
Length
2,898 words, 13 min

A copybook is the file-layout contract of a COBOL shop. The record description lives in one file, every program that touches the data brings it in with COPY, and when the layout changes the programs are recompiled rather than re-edited. This tutorial builds that arrangement at small scale: one 39-byte account record, and three programs that use it three different ways. ACCT-POST reads last month's account master and writes this month's, so it needs the layout twice and renames it with REPLACING LEADING. ACCT-LIST reads the new master with a plain COPY. ACCT-AUDIT reads both masters side by side and does its renaming with the REPLACE statement instead. The file handling is the loop from reading a sequential file to end of file, and the record is the kind of structure described in group items and elementary items.

The three programs were desk-checked line by line against the GnuCOBOL Programmer's Guide and the IBM Enterprise COBOL 6.4 Language Reference, and on 29 October 2026 we compiled and ran all three with GnuCOBOL 3.2; their output matched the expected output below line for line. Every output number quoted below comes from the Python companion, /code/cobol-copybook-copy-replacing.py, which we ran first: it reads the copybook to find the field positions, writes the data file, and replays all three programs. Where the two compilers' documents disagree, or where the GnuCOBOL guide is silent and the project's own test suite says something, we name the source for each side.

The copybook

       01  Acct-Record.
           05  Acct-Id             PIC X(6).
           05  Acct-Name           PIC X(20).
           05  Acct-Branch         PIC X(3).
           05  Acct-Status         PIC X.
               88  Acct-Open                   VALUE "O".
               88  Acct-Closed                 VALUE "C".
           05  Acct-Balance        PIC 9(7)V99.
Listing 3.4-1. Lines 13 to 20 of cobol-copybook-copy-replacing.cpy.

The shipped file, /code/cobol-copybook-copy-replacing.cpy, adds a comment block naming the three programs that use it. The data file, /datasets/cobol-copybook-copy-replacing.dat, holds 18 records in this layout with LF line endings and no header:

Table 3.4-1
Field Positions PIC Meaning
Acct-Id 1 to 6 X(6) Account number; both masters are in this order
Acct-Name 7 to 26 X(20) Account holder (the names are invented)
Acct-Branch 27 to 29 X(3) Branch code: NTH, STH or EST
Acct-Status 30 X O for open, C for closed, with an 88-level name for each
Acct-Balance 31 to 39 9(7)V99 Balance: nine digits, implied decimal point, no sign

Two decisions in it matter for everything that follows. First, every name begins with the same prefix, Acct-, including the 01 group name and the two condition names. That makes the whole record renameable with one phrase. Second, the text sits between columns 8 and 72, with the 01 level starting in column 8, even though the programs that copy it are written in free format. The guide's description of fixed format puts the level numbers 01 and 77, like division headers, in Area A, and IBM's Language Reference (p. 690) says library text "must conform to the rules for the 85 COBOL Standard format". What we could not find in the guide is any statement about whether a >>SOURCE FORMAT IS FREE directive in the program carries into a copybook it copies. The GnuCOBOL 3.x test suite does contain a test, "COPY: fixed/free format", in which a copybook switches itself to fixed format with its own directive inside a free-format compile. Rather than depend on either behaviour, this copybook reads correctly in both formats.

The guide gives the reason to bother in its discussion of record sequential files: use a copybook to describe the record layouts of any file, because "this guarantees that multiple programs accessing that file will “see” the same record sizes and layouts".

What COPY does before compilation starts

COPY is not a runtime include. Section 3.2 of the guide: "All COPY statements are located and the contents of the corresponding copybooks inserted into the program source code before the actual compilation process begins." IBM describes the same effect on p. 688: the library text is copied into the compilation unit, "logically replacing the entire COPY statement, beginning with the word COPY and ending with the period, inclusive". The period is mandatory in both. The guide adds that it is required even inside the scope of an IF, and that it "does not, however, affect the statement scope in which the COPY occurs".

Because the substitution happens first, you can look at the result. The GnuCOBOL Manual documents cobc -E as "Preprocess only: compiler directives are executed, comment lines are removed and COPY statements are expanded", with the output saved in a .i file. When a REPLACING phrase does something unexpected, that file is the first place to look.

ACCT-LIST is the plain case. Its FD is two lines:

FD  Account-File.
    COPY "cobol-copybook-copy-replacing.cpy".
Listing 3.4-2. Lines 27 to 28 of cobol-copybook-list.cob.

After that the program uses Acct-Id, Acct-Balance, and the condition names Acct-Open and Acct-Closed exactly as if they had been typed in. The full listing is /code/cobol-copybook-list.cob.

ACCT-POST: one layout, two record areas

A program that reads an old master and writes a new one needs two copies of the same record, and they cannot both be called Acct-Record. The REPLACING phrase settles that at copy time:

       >>SOURCE FORMAT IS FREE
*> cobol-copybook-copy-replacing.cob - ACCT-POST, the first of three
*>          programs sharing one record layout
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked; compiled and run with GnuCOBOL 3.2 on 2026-10-29
IDENTIFICATION DIVISION.
PROGRAM-ID. ACCT-POST.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
    SELECT Old-Master
        ASSIGN TO "datasets/cobol-copybook-copy-replacing.dat"
        ORGANIZATION IS LINE SEQUENTIAL
        FILE STATUS IS WS-Old-FS.
    SELECT New-Master
        ASSIGN TO "acct-master-new.dat"
        ORGANIZATION IS LINE SEQUENTIAL
        FILE STATUS IS WS-New-FS.
DATA DIVISION.
FILE SECTION.
FD  Old-Master.
    COPY "cobol-copybook-copy-replacing.cpy"
        REPLACING LEADING ==Acct-== BY ==Old-==.
FD  New-Master.
    COPY "cobol-copybook-copy-replacing.cpy"
        REPLACING LEADING ==Acct-== BY ==New-==.
WORKING-STORAGE SECTION.
77 WS-Old-FS             PIC XX.
77 WS-New-FS             PIC XX.
01 WS-Flags.
   05 WS-EOF-Flag        PIC X VALUE "N".
      88 End-Of-Old-Master     VALUE "Y".
77 WS-Monthly-Rate       PIC V9999    VALUE .0025.
77 WS-Interest           PIC 9(7)V99  VALUE 0.
77 WS-Interest-Total     PIC 9(7)V99  VALUE 0.
77 WS-Read               PIC 9(5)     VALUE 0.
77 WS-Written            PIC 9(5)     VALUE 0.
77 WS-Credited           PIC 9(5)     VALUE 0.
77 WS-Show-Count         PIC ZZ,ZZ9.
77 WS-Show-Money         PIC Z,ZZZ,ZZ9.99.
PROCEDURE DIVISION.
Main-Paragraph.
    OPEN INPUT Old-Master
    IF WS-Old-FS NOT = "00"
       DISPLAY "Cannot open the old master, status " WS-Old-FS
       STOP RUN
    END-IF
    OPEN OUTPUT New-Master
    IF WS-New-FS NOT = "00"
       DISPLAY "Cannot create acct-master-new.dat, status " WS-New-FS
       CLOSE Old-Master
       STOP RUN
    END-IF
    PERFORM Read-Old
    PERFORM UNTIL End-Of-Old-Master
       PERFORM Post-One-Account
       PERFORM Read-Old
    END-PERFORM
    CLOSE Old-Master New-Master
    MOVE WS-Read TO WS-Show-Count
    DISPLAY "ACCT-POST  records read     " WS-Show-Count
    MOVE WS-Written TO WS-Show-Count
    DISPLAY "ACCT-POST  records written  " WS-Show-Count
    MOVE WS-Credited TO WS-Show-Count
    DISPLAY "ACCT-POST  accounts credited" WS-Show-Count
    MOVE WS-Interest-Total TO WS-Show-Money
    DISPLAY "ACCT-POST  interest posted  " WS-Show-Money
    STOP RUN.

Read-Old.
    READ Old-Master
       AT END
          SET End-Of-Old-Master TO TRUE
    END-READ
    IF WS-Old-FS NOT = "00" AND WS-Old-FS NOT = "10"
       DISPLAY "Old master read failed, status " WS-Old-FS
       STOP RUN
    END-IF.

Post-One-Account.
    ADD 1 TO WS-Read
    MOVE Old-Record TO New-Record
    IF Old-Open AND Old-Balance > 0
       COMPUTE WS-Interest ROUNDED = Old-Balance * WS-Monthly-Rate
       ADD WS-Interest TO New-Balance
       ADD WS-Interest TO WS-Interest-Total
       ADD 1 TO WS-Credited
    END-IF
    WRITE New-Record
    IF WS-New-FS NOT = "00"
       DISPLAY "Write failed for account " Old-Id ", status " WS-New-FS
       CLOSE Old-Master New-Master
       STOP RUN
    END-IF
    ADD 1 TO WS-Written.

The shipped listing carries a fuller header comment and two explanatory comments; the code is identical.

LEADING ==Acct-== BY ==Old-== changes the start of every text word that begins with Acct-. IBM's comparison rules on p. 692 define the match: with LEADING, partial-word-1 matches only if its characters equal "an equal number of contiguous characters that start with the leftmost character position of a library text word". The companion applies the same substitution to the copybook's data lines and prints what the first COPY produces:

       01  Old-Record.
           05  Old-Id             PIC X(6).
           05  Old-Name           PIC X(20).
           05  Old-Branch         PIC X(3).
           05  Old-Status         PIC X.
               88  Old-Open                   VALUE "O".
               88  Old-Closed                 VALUE "C".
           05  Old-Balance        PIC 9(7)V99.

The condition names are renamed along with the fields, so ACCT-POST tests Old-Open, and the whole record crosses to the output area with one group MOVE of 39 bytes, MOVE Old-Record TO New-Record. Only New-Balance changes after that. We named the file status fields WS-Old-FS and WS-New-FS rather than anything ending in -Status, because the renaming already produces Old-Status and New-Status for the account status byte, and a reader should not have to work out which is which.

Both documents describe the LEADING form. The guide's example is LEADING ==0100-== BY ==020-==; IBM's REPLACE example on p. 701 strips a prefix with LEADING ==XX-== BY ====. We wrote the prefix in the same letter case in the copybook and in every REPLACING operand, since both documents describe matching as character for character and we did not want the programs to depend on case folding inside pseudo-text.

Whole words, :TAG:, and a prefix you cannot remove

Without LEADING or TRAILING, a REPLACING operand matches whole text words. A phrase such as ==PIC 9(7)V99== BY ==PIC S9(7)V99== would give one program a signed balance and leave the copybook alone, and IBM (p. 691) advises that when replacing a picture, "the entire PICTURE clause, including the keyword PICTURE or PIC, should be specified in pseudo-text-1". When several operands could match the same text, IBM's rule is that the comparison tries each operand in turn until one matches. One of the GnuCOBOL tests, "COPY: replacement order", checks the same thing: two operands both name TEST-VAR, and the first one wins.

The older way of renaming part of a word is a placeholder such as :TAG: inside the copybook, replaced with pseudo-text. Here the documents part company. IBM documents it among the comparison rules on p. 693, and the usage note on its worked example (p. 694) says "Colons and parenthesis are the only valid delimiters". The GnuCOBOL guide's description of the whole-word form says it "cannot be used to replace part of a word, identifier or literal", and it never mentions the placeholder convention. Yet the 3.x test suite has a test named "COPY: partial replacement" whose copybook declares :TEST:-VAR and (TEST)-VAR, replaces ==:TEST:== and ==(TEST)==, and expects the program to display both values. The compiler's own tests exercise something the guide says cannot be done. We used LEADING because it is the form both manuals document.

It is also tempting to strip the prefix entirely with LEADING ==Acct-== BY ====. With this copybook that produces Record, Id, Name and Status, and all four appear in the GnuCOBOL guide's reserved word list, of which it says "none may be used as any user-defined name". The prefix is doing more work than it looks.

ACCT-AUDIT: the same renaming with REPLACE

REPLACE is the statement form of the same idea. Instead of a phrase attached to one COPY, it acts on the source text that follows it. The FILE SECTION of /code/cobol-copybook-audit.cob:

FILE SECTION.
REPLACE LEADING ==Acct-== BY ==Old-==.
FD  Old-Master.
    COPY "cobol-copybook-copy-replacing.cpy".
REPLACE LEADING ==Acct-== BY ==New-==.
FD  New-Master.
    COPY "cobol-copybook-copy-replacing.cpy".
REPLACE OFF.

This only works if REPLACE sees the text that COPY brought in. IBM says so on p. 701: "The compiler processes REPLACE statements in source text after the processing of any COPY statements." The GnuCOBOL guide does not say it, but the NEWS entry for 3.2 does: cobc "now uses a two-pass preprocessing algorithm, where replacements for COPY-REPLACING are done in a first pass, and the replacements for REPLACE are done in a second pass". The test "COPY and REPLACE in same file" shows the result in preprocessed output: a REPLACE statement rewrites a data name that exists only after a copybook's own REPLACING has run. If some build did not apply REPLACE to copied text, ACCT-AUDIT would fail to compile with Old-Id undefined, a loud failure rather than a silent one.

The second REPLACE needs no OFF before it. IBM says a REPLACE is in effect "until the next occurrence of a REPLACE statement or the end of the separately compiled program", and the guide says a REPLACE without ALSO discards the specifications in effect and substitutes its own. Past that point the dialects differ. The guide documents REPLACE ALSO, which stacks a new specification on the current one, and REPLACE LAST OFF, which pops it; IBM's 6.4 formats on p. 700 have neither, only REPLACE OFF. The guide also ends Replace Mode at "the end of currently compiling program source file", where IBM stops at the end of the separately compiled program, which matters when one source file holds several programs. Closing with REPLACE OFF sidesteps both differences.

The audit reads the two masters in step and counts a problem if the account numbers differ, a status changed, a balance fell, or a closed account gained money:

Compare-Pair.
    ADD 1 TO WS-Pairs
    ADD Old-Balance TO WS-Old-Total
    ADD New-Balance TO WS-New-Total
    IF New-Id NOT = Old-Id
       ADD 1 TO WS-Problems
       DISPLAY "Out of step: old " Old-Id " new " New-Id
    END-IF
    IF New-Status NOT = Old-Status
       ADD 1 TO WS-Problems
       DISPLAY "Status changed on account " Old-Id
    END-IF
    IF New-Balance > Old-Balance
       ADD 1 TO WS-Raised
       COMPUTE WS-Movement = WS-Movement + New-Balance - Old-Balance
       IF New-Closed
          ADD 1 TO WS-Problems
          DISPLAY "Closed account credited: " Old-Id
       END-IF
    ELSE
       IF New-Balance = Old-Balance
          ADD 1 TO WS-Unchanged
       ELSE
          ADD 1 TO WS-Problems
          DISPLAY "Balance fell on account " Old-Id
       END-IF
    END-IF.
Listing 3.4-3. Lines 131 to 157 of cobol-copybook-audit.cob.

Its last check is the control total an operator actually looks at: the old total plus the movement must equal the new total.

Where the compiler finds the copybook, and other dialect differences

Nothing in a COPY statement says which folder the file is in, and this is where the two compilers differ most. The rows below come from section 10.1.4 of the guide, the 3.x NEWS file and test suite for GnuCOBOL, and the 6.4 Language Reference for IBM:

Table 3.4-2
Question GnuCOBOL IBM Enterprise COBOL 6.4
Search order the folder named by IN or OF; the folder holding the program; each -I folder; each folder in COBCPY; the folder in COB_COPY_DIR from JCL: the ddname named by IN or OF, else SYSLIB, then COPYLOC locations. Under cob2: current directory, -I directories, the SYSLIB environment variable, then COPYLOC (p. 697)
What IN or OF names a folder; since 3.1 an environment variable COB_COPY_LIB_ plus the library name is tried too, and a library that yields nothing is ignored with a warning from JCL, a ddname for a concatenation of data sets; under cob2, a literal is a directory and a word is an environment variable holding a colon-separated list (p. 697)
Extensions tried .CPY .CBL .COB .cpy .cbl .cob, then the bare name; -ext adds one; since 3.1 a name that already has an extension gets no other under cob2, for a name that is not a literal: .cpy .CPY .cbl .CBL .cob .COB (p. 697)
Copybook names treat as case-sensitive; -ffold-copy=UPPER or LOWER folds them 1 to 30 characters; only the first eight are used for a PDS member; a literal in a JCL compile may not contain a period (pp. 688-689)
Nested COPY expanded repeatedly until none remain; recursive inclusion is an error; an outer REPLACING reaches the nested text (tests "COPY: recursive" and "COPY: recursive replacement") allowed without recursion; only one COPY in a nested chain may carry REPLACING, a second is severe error IGYLI0078-S with return code 12; that REPLACING applies to all nested text (pp. 689, 691)
SUPPRESS accepted but "non-functional" the library text is left off the source listing (p. 690)
Partial words LEADING and TRAILING documented; :TAG: not documented, but tested LEADING, TRAILING, and :TAG: or (TAG) all documented (pp. 692-695)
REPLACE stacking REPLACE ALSO and REPLACE LAST OFF REPLACE OFF only (p. 700)

Two consequences for these programs. The COPY names are quoted literals with an extension, which suits GnuCOBOL and a z/OS UNIX compile but would not work as a PDS member name in a JCL compile, where the name has to fit in eight characters without a period. And because the guide searches the folder holding the program before any -I folder, the -I code on the build lines below is not strictly needed, since the copybook sits beside the three sources. It is there because real shops keep copybooks in a library folder of their own.

We found nothing in the guide about how many REPLACING phrases a nested chain may carry, and no test for that case among the ones we read, so IBM's one-per-chain restriction is the only documented rule and GnuCOBOL's behaviour there is unknown to us. A shop that has to compile under both should keep to one.

What comes out

ACCT-POST credits 0.25 per cent a month to open accounts with a positive balance, rounded to the cent. Two of the fourteen credits land exactly on a half cent: 2,450.00 × 0.0025 = 6.1250, and 322.00 × 0.0025 = 0.8050. The guide says ROUNDED with no MODE "is the same as specifying ROUNDED MODE IS NEAREST-AWAY-FROM-ZERO", and IBM's ROUNDED phrase (p. 296) raises the last digit "whenever the most significant digit of the excess is greater than or equal to 5", so both give 6.13 and 0.81. Rounding modes have their own lesson, COMPUTE, ROUNDED and ON SIZE ERROR. The output of the three programs, run in order, as printed by python code/cobol-copybook-copy-replacing.py and, line for line, by the compiled programs:

ACCT-POST  records read         18
ACCT-POST  records written      18
ACCT-POST  accounts credited    14
ACCT-POST  interest posted        264.12
ID     NAME                 BR  STATUS      BALANCE
100104 ADEYEMI TOBI         NTH open       2,456.13
100118 BECKER ANNA          NTH open      13,240.37
100125 CHAVEZ LUIS          STH open           0.00
100131 DUBOIS CLAIRE        EST closed         0.00
100147 EKSTROM LARS         STH open         990.09
100152 FITZGERALD NORA      NTH open      45,112.50
100160 GUPTA ARJUN          EST open       1,237.59
100173 HALVORSEN INGRID     STH open         322.81
100189 IBARRA SOFIA         NTH closed         0.00
100194 JANSSEN PIETER       EST open       7,797.21
100206 KOWALSKI MAREK       STH open         563.81
100211 LINDQVIST EMMA       NTH open      20,967.37
100229 MORAN DECLAN         EST open         100.24
100235 NAKAMURA KEN         STH open       3,609.00
100248 OKAFOR CHIDI         NTH open       1,052.73
100250 PETROVA ELENA        EST closed         0.00
100263 QUINTERO MATEO       STH open          12.37
100277 RASMUSSEN JONAS      NTH open       8,441.65
Open accounts       15  balance   105,903.87
Closed accounts      3
ACCT-AUDIT pairs compared       18
ACCT-AUDIT balances raised      14
ACCT-AUDIT balances same         4
ACCT-AUDIT old total          105,639.75
ACCT-AUDIT movement               264.12
ACCT-AUDIT new total          105,903.87
ACCT-AUDIT result           balanced

The three programs agree with each other, which is the point of sharing the layout. ACCT-POST credits 14 accounts with 264.12 in total; ACCT-AUDIT finds 14 balances raised by a movement of 264.12, and 105,639.75 + 264.12 = 105,903.87, the new total. The four unchanged balances are the three closed accounts and one open account at zero. ACCT-LIST's open balance is the same 105,903.87 because every closed account holds zero. The companion asserts each of those equalities, and before printing any label it checks that the label exists as a literal in the matching .cob file, so a label edited in one place and not the other stops the script.

Compile and run

python code/cobol-copybook-copy-replacing.py
cobc -x -I code -o acct-post code/cobol-copybook-copy-replacing.cob
cobc -x -I code -o acct-list code/cobol-copybook-list.cob
cobc -x -I code -o acct-audit code/cobol-copybook-audit.cob
./acct-post
./acct-list
./acct-audit

Run all of it from the folder that holds code/ and datasets/. ACCT-POST writes acct-master-new.dat into that folder, and the other two read it from there, so ACCT-LIST and ACCT-AUDIT report a file status and stop if they run first.

Limitations

The listings were compiled and run with GnuCOBOL 3.2 only; nothing here was run on IBM Enterprise COBOL. The Programmer's Guide describes itself as documenting "the July 2020 build of GnuCOBOL 3.1 RC-1", so for later behaviour we relied on the NEWS file and the test suite. The test suite we read is the head of the 3.x branch in the project's repository, where the planned 3.3 is being developed, so a test there describes the branch as it stands, not necessarily every 3.x release. The 3.2 NEWS entry that reports "several bugs in COPY REPLACING / REPLACING were fixed along with adding support for exensions related to REPLACING LEADING / TRAILING" is a reason to use 3.2 or later for these programs.

The same NEWS file's planned 3.3 section lists a change that could bite: "in COPY REPLACING and REPLACE, spaces around the replacement pseudo-text between == are now removed, conforming to the standard and existing dialects". A replacement written as == Old- == may behave differently from one release to the next, which is why every operand in these listings is written with no space inside the delimiters.

ACCT-POST's ADD has no ON SIZE ERROR. A balance within a quarter of a per cent of 9,999,999.99 would overflow PIC 9(7)V99; the data stays far below that, and a production posting run would trap it. Finally, the copybook is only as good as the agreement to use it. Nothing stops a fourth program from hand-typing a 40-byte layout for the same file, and no compiler will notice. The audit program is the kind of check that does.

Correction, 29 October 2026: the second and third build commands named the sources cobol-copybook-copy-replacing-list.cob and cobol-copybook-copy-replacing-audit.cob, and GnuCOBOL 3.2's cobc refuses a source file whose base name is longer than 31 characters ("invalid file base name ... length exceeds maximum"), even with -o, so they failed as printed. Those two listings are now shipped as cobol-copybook-list.cob and cobol-copybook-audit.cob, and the old download addresses redirect there; ACCT-POST keeps its name. The same day we compiled and ran all three programs with GnuCOBOL 3.2, and their output matched the output shown here line for line, so the page now says that instead of calling the listings desk-checked only.

Index terms cobol, gnucobol, copybook, copy replacing, replace