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.
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.
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:
| 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".
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.
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:
| 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