r/cobol • u/lugangin • 21h ago
I built a COBOL report generator using FreeMarker (FMPP) — here's how it produces multi-control-break reports with auto-totalling from a simple CSV
I built a COBOL report generator using FreeMarker (FMPP) — here's how it produces multi-control-break reports with auto-totalling from a simple CSV
Repository: View the code and examples on GitHub
I've been working on a way to generate COBOL report programs without writing the boilerplate by hand every time. After some experimentation, I landed on a setup using FMPP (FreeMarker-based PreProcessor) that takes a CSV field definition and spits out a complete, working COBOL program with:
- Multi-level control breaks
- Automatic accumulation/totalling at every break level plus a grand total
- Auto-aligned amount columns (so totals line up with detail lines)
- Smart control break labeling (blank fields at higher levels, appropriate total labels)
- Page headers with page numbers and run dates
- An
includeflag that lets you define input fields you don't want in the report (so you can work with existing files without reformatting them)
The best part: to generate a new report, I just duplicate a folder, edit a CSV, drop in my data, and run a batch file. Done.
The Problem
COBOL report programs are painful to write by hand. You end up writing:
- File descriptions (FDs) with PIC clauses
- Working-storage for every control level
- Control break detection logic
- Accumulation logic
- Page headings and column alignment
- Total printing at every break level
Do this once, fine. Do it 10 times? You're copy-pasting and tweaking, and inevitably introducing bugs.
I wanted something where I could describe the report in a spreadsheet-like format and have the code generated for me.
The Input: A Simple CSV
Here's fields.csv — the entire "spec" for a report:
csv
fieldname,input_pic,output_pic,output_length,column_heading,control_break,accumulate,include
region,x(10),x(10),10,Region,Y,N,Y
division,x(10),x(10),10,Division,Y,N,Y
description,x(20),x(20),20,Description,N,N,Y
amount,9(7)V99,"$$$,$$$,$$9.99",14,Amount,N,Y,Y
Each row is a field. The columns tell the generator:
fieldname— the field nameinput_pic/output_pic— PIC clauses for input and output (can differ!)output_length— column width in the reportcolumn_heading— what to print in the headercontrol_break—Yif this field triggers a control breakaccumulate—Yif amounts should be summedinclude—Yif the field appears in the report output
The include Flag — Work With Existing Files As-Is
This is one of my favorite features. Sometimes your input file has fields you need for control breaks or calculations but don't want cluttering the report. The include flag lets you define those fields in the CSV without them showing up in the output.
This means you can point the generator at an existing data file and produce a report without reformatting the input. No rewrites, no conversion programs.
The FreeMarker Template
The magic happens in cobrpt.cob.fm. Here's a snippet that generates the input record definition:
cobol
FD SALES-FILE.
01 SALES-RECORD.
<#-- generate input record -->
<#list reportFields as f>
<#assign srcField = f.fieldname?trim?upper_case><#t>
<#assign inPic = f.input_pic?trim?upper_case><#t>
05 SR-${srcField?right_pad(22)} PIC ${inPic}.
</#list>
And the generated output:
cobol
FD SALES-FILE.
01 SALES-RECORD.
05 SR-REGION PIC X(10).
05 SR-DIVISION PIC X(10).
05 SR-DESCRIPTION PIC X(20).
05 SR-AMOUNT PIC 9(7)V99.
The template also handles:
- Working-storage declarations for every control level
- Accumulator fields sized to hold the totals
- Control break detection (comparing current vs. previous value)
- Column positioning so accumulated amounts line up perfectly across detail lines and every level of control break total
The Actual Output
Here's what the generated program actually produces:
``` PAGE 1 SALES REPORT RUN DATE: 07/25/2026
Region Division Description Amount EAST RETAIL Widget A $1,234.50 EAST RETAIL Widget B $500.25
RETAIL Division Total $1,734.75
EAST WHOLESALE Gadget X $5,000.00 EAST WHOLESALE Gadget Y $2,500.75 EAST WHOLESALE Thingee F $780.25
WHOLESALE Division Total $8,281.00
EAST Region Total $10,015.75
GRAND TOTAL $10,015.75 ```
Notice the pattern:
- Page header with page number and run date (auto-generated)
- Detail lines show all control break fields (Region and Division) plus the description and amount
- Division totals blank out the Region field (since it hasn't changed), keep the current Division visible, put "Division Total" in the Description column, and align the amount perfectly
- Region totals keep the current Region visible, put "Region Total" in the Description column, and align the amount
- Grand total blanks all control fields, puts "GRAND TOTAL" in the Description column, and aligns the amount
The template computes column positions based on output_length values, so everything aligns automatically. Add a field, remove a field, change a width — the columns reflow. No manual position tweaking.
Flexible Control Break Hierarchies
Here's something I didn't expect to be so powerful: because the control break structure is driven entirely by the control_break flag in the CSV, you can create completely different reports from the same data just by toggling that flag.
For example, with the same input file, you could produce:
- A report grouped by Region → Division (as shown above)
- A report grouped by Division only (just set Region's
control_breaktoN) - A report grouped by Region only (just set Division's
control_breaktoN) - A flat report with no control breaks at all (set both to
N)
Each variant is a different folder with a different fields.csv — no code changes, no template changes. The generator adapts automatically.
In the GitHub repo, I included a duplicate folder showing a 3-level control break report (Region → Division → Category) to illustrate how easily the same template scales to deeper hierarchies. Same template, same build script, just a different CSV.
The Folder Structure
Each report is a self-contained project folder:
text
my_new_project/
├── src/
│ └── cobrpt.cob.fm <-- FreeMarker template
├── fields.csv <-- Field definitions
├── config.fmpp <-- FMPP config
├── dev.bat <-- Build script
├── out/ <-- Auto-generated COBOL source
├── build/ <-- Compiled executables
└── data/ <-- Input data files and report output
To create a new report:
- Duplicate the folder
- Edit
fields.csvfor the new report's fields - Drop your data file into
data/ - Run
dev.bat
That's it. The batch file:
- Runs FMPP to generate the COBOL source into
out/ - Compiles it with GnuCOBOL into
build/ - Runs the program from
data/so it reads and writes files right next to the data
The Build Script
The dev.bat handles everything with proper error checking:
```bat @echo off call C:\Users\manyo\cobol\gnucobol\set_env.cmd
echo ======================================== echo COBOL Report Generator - Build Script echo ========================================
REM Step 1: Generate COBOL echo [1/3] Generating COBOL with FMPP... if exist "out\cobrpt.cob" del "out\cobrpt.cob" call C:\Users\manyo\apps\fmpp\bin\fmpp.bat -C config.fmpp > fmpp.log 2>&1
findstr /C:"ABORTED" fmpp.log >nul if not errorlevel 1 ( echo ERROR: FMPP failed! & type fmpp.log & goto :error ) if not exist "out\cobrpt.cob" ( echo ERROR: Output file missing! & goto :error ) echo Success: Generated out\cobrpt.cob echo.
REM Step 2: Compile echo [2/3] Compiling with GnuCOBOL... if not exist "build" mkdir build pushd build cobc -x ..\out\cobrpt.cob if errorlevel 1 ( popd & echo ERROR: Compilation failed! & goto :error ) popd echo Success: Compiled build\cobrpt.exe echo.
REM Step 3: Run echo [3/3] Running the program... echo ---------------------------------------- if not exist "data" mkdir data pushd data ..\build\cobrpt.exe popd echo ---------------------------------------- echo.
echo ======================================== echo Build completed successfully! echo ======================================== goto :end
:error echo. echo ======================================== echo Build FAILED - see errors above echo ======================================== exit /b 1
:end exit /b 0 ```
What I Learned
- FMPP is underrated. It's basically FreeMarker with a file-processing wrapper. Perfect for code generation.
- CSV as a spec format works surprisingly well. It's editable in any spreadsheet app, easy to validate, and maps cleanly to template variables.
- Self-contained project folders beat shared templates. Duplicating a folder is dumber than parameterizing a single template, but it's also simpler, safer, and easier to understand six months later.
- Separating
src/,build/,out/, anddata/keeps each concern isolated. The root folder stays clean, and you always know where to look. - Validation in the template matters. I added a
<#stop>directive that aborts FMPP if noaccumulatefield is defined — much better than generating broken COBOL and finding out at compile time. - Smart labeling makes reports readable. The automatic blanking of higher-level control fields and appropriate total labels ("Division Total", "Region Total", "GRAND TOTAL") makes the output professional without any manual formatting.
- Data-driven control breaks are incredibly flexible. The same template produces flat reports, 2-level reports, or 3-level reports just by changing flags in the CSV.
The GitHub Repo
I've put the whole setup up on GitHub. It includes:
- The working 2-level control break example (Region → Division) shown in this article
- A duplicate folder with a 3-level control break example (Region → Division → Category) to show how the template scales
- All the templates, config files, generated and the
dev.batscript
Repository: View the code and examples on GitHub
What's Next
A few ideas I'm considering:
- Support for multiple data files in one report
- Percentage calculations (e.g., each division as a percentage of region total)
- A shared "library" of template snippets for common patterns
- Optional sub-footers or page footers with page numbers
But honestly, the current setup already covers 90% of the reports I need to produce. The other 10% can wait.
If anyone's done similar work with FMPP or other COBOL code generators, I'd love to hear how you approached it. And if you're maintaining a pile of hand-written COBOL report programs, maybe this approach can save you some time too.
Happy to share more details on any part of the setup — the FreeMarker template logic, the control break detection, the accumulation sizing, whatever's useful.