COBOL 104: The Complete Zero-Bloat To-Do Application
Over the last few posts, we have dismantled the modern web development fever dream. We threw away the JavaScript frameworks, the ORMs, and the gigabytes of node_modules dependencies. Instead, we went back to the absolute fundamentals of enterprise computing: fixed-width bytes, sequential files, and the Cassette Tape Pattern for managing data.
Many of you asked for the complete, unified script so you don't have to stitch the previous tutorials together manually.
Here it is. This is the complete, fully functional COBOL CRUD (Create, Read, Update, Delete) application. It handles user input, checks the system clock to flag overdue tasks, and safely manipulates the filesystem to edit and delete records using a temporary file buffer.
The Complete Source Code Save the following code into a single file named todo.cbl.
Note: Ensure your spacing is correct. In COBOL, major headers like Divisions and Paragraph names start in Area A (Column 8), while executable statements start in Area B (Column 12).
IDENTIFICATION DIVISION.
PROGRAM-ID. TODO-APP.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
* OPTIONAL creates the file if it doesn't exist
SELECT OPTIONAL TODO-FILE ASSIGN TO "todos.dat"
ORGANIZATION IS LINE SEQUENTIAL.
SELECT OPTIONAL TEMP-FILE ASSIGN TO "temp.dat"
ORGANIZATION IS LINE SEQUENTIAL.
DATA DIVISION.
FILE SECTION.
FD TODO-FILE.
01 TODO-RECORD.
05 TODO-DUE-DATE PIC 9(8).
05 TODO-TEXT PIC X(50).
FD TEMP-FILE.
01 TEMP-RECORD.
05 TEMP-DUE-DATE PIC 9(8).
05 TEMP-TEXT PIC X(50).
WORKING-STORAGE SECTION.
01 WS-CHOICE PIC 9 VALUE 0.
01 WS-EOF PIC X VALUE 'N'.
01 WS-TARGET-NUM PIC 9(3).
01 WS-COUNTER PIC 9(3).
01 WS-CURRENT-DATE PIC 9(8).
01 WS-TEMP-DATE-TIME PIC X(21).
01 WS-OVERDUE-MSG PIC X(10) VALUE SPACES.
PROCEDURE DIVISION.
MAIN-PARAGRAPH.
* Grab the current system date (YYYYMMDD) for overdue checks
MOVE FUNCTION CURRENT-DATE TO WS-TEMP-DATE-TIME.
MOVE WS-TEMP-DATE-TIME(1:8) TO WS-CURRENT-DATE.
PERFORM UNTIL WS-CHOICE = 5
DISPLAY " "
DISPLAY "--- BARE-METAL TO-DO LIST ---"
DISPLAY "1. ADD A TASK"
DISPLAY "2. VIEW TASKS"
DISPLAY "3. EDIT A TASK"
DISPLAY "4. DELETE A TASK"
DISPLAY "5. EXIT"
DISPLAY "CHOICE: " WITH NO ADVANCING
ACCEPT WS-CHOICE
EVALUATE WS-CHOICE
WHEN 1 PERFORM ADD-TASK
WHEN 2 PERFORM VIEW-TASKS
WHEN 3 PERFORM EDIT-TASK
WHEN 4 PERFORM DELETE-TASK
WHEN 5 DISPLAY "SYSTEM HALTED. GOODBYE."
WHEN OTHER DISPLAY "INVALID CHOICE."
END-EVALUATE
END-PERFORM.
STOP RUN.
ADD-TASK.
OPEN EXTEND TODO-FILE.
DISPLAY "ENTER DUE DATE (YYYYMMDD): " WITH NO ADVANCING.
ACCEPT TODO-DUE-DATE.
DISPLAY "ENTER TASK DESCRIPTION: " WITH NO ADVANCING.
ACCEPT TODO-TEXT.
WRITE TODO-RECORD.
CLOSE TODO-FILE.
DISPLAY "[+] TASK SAVED TO DISK.".
VIEW-TASKS.
DISPLAY " "
DISPLAY "--- YOUR TASKS ---".
OPEN INPUT TODO-FILE.
MOVE 'N' TO WS-EOF.
MOVE 1 TO WS-COUNTER.
PERFORM UNTIL WS-EOF = 'Y'
READ TODO-FILE
AT END
MOVE 'Y' TO WS-EOF
NOT AT END
IF TODO-DUE-DATE < WS-CURRENT-DATE
MOVE "[OVERDUE]" TO WS-OVERDUE-MSG
ELSE
MOVE " " TO WS-OVERDUE-MSG
END-IF
DISPLAY "[" WS-COUNTER "] " TODO-DUE-DATE
" | " TODO-TEXT " " WS-OVERDUE-MSG
ADD 1 TO WS-COUNTER
END-READ
END-PERFORM.
IF WS-COUNTER = 1
DISPLAY " (NO TASKS FOUND)"
END-IF.
CLOSE TODO-FILE.
EDIT-TASK.
DISPLAY "ENTER TASK NUMBER TO EDIT: " WITH NO ADVANCING.
ACCEPT WS-TARGET-NUM.
OPEN INPUT TODO-FILE.
OPEN OUTPUT TEMP-FILE.
MOVE 'N' TO WS-EOF.
MOVE 1 TO WS-COUNTER.
PERFORM UNTIL WS-EOF = 'Y'
READ TODO-FILE
AT END
MOVE 'Y' TO WS-EOF
NOT AT END
IF WS-COUNTER = WS-TARGET-NUM
DISPLAY "NEW DUE DATE (YYYYMMDD): "
WITH NO ADVANCING
ACCEPT TEMP-DUE-DATE
DISPLAY "NEW TASK DESCRIPTION: "
WITH NO ADVANCING
ACCEPT TEMP-TEXT
WRITE TEMP-RECORD
DISPLAY "[+] TASK UPDATED."
ELSE
MOVE TODO-RECORD TO TEMP-RECORD
WRITE TEMP-RECORD
END-IF
ADD 1 TO WS-COUNTER
END-READ
END-PERFORM.
CLOSE TODO-FILE TEMP-FILE.
PERFORM COPY-TEMP-TO-MAIN.
DELETE-TASK.
DISPLAY "ENTER TASK NUMBER TO DELETE: " WITH NO ADVANCING.
ACCEPT WS-TARGET-NUM.
OPEN INPUT TODO-FILE.
OPEN OUTPUT TEMP-FILE.
MOVE 'N' TO WS-EOF.
MOVE 1 TO WS-COUNTER.
PERFORM UNTIL WS-EOF = 'Y'
READ TODO-FILE
AT END
MOVE 'Y' TO WS-EOF
NOT AT END
IF WS-COUNTER NOT = WS-TARGET-NUM
MOVE TODO-RECORD TO TEMP-RECORD
WRITE TEMP-RECORD
ELSE
DISPLAY "[+] TASK DELETED."
END-IF
ADD 1 TO WS-COUNTER
END-READ
END-PERFORM.
CLOSE TODO-FILE TEMP-FILE.
PERFORM COPY-TEMP-TO-MAIN.
COPY-TEMP-TO-MAIN.
OPEN INPUT TEMP-FILE.
OPEN OUTPUT TODO-FILE.
MOVE 'N' TO WS-EOF.
PERFORM UNTIL WS-EOF = 'Y'
READ TEMP-FILE
AT END
MOVE 'Y' TO WS-EOF
NOT AT END
MOVE TEMP-RECORD TO TODO-RECORD
WRITE TODO-RECORD
END-READ
END-PERFORM.
CLOSE TEMP-FILE TODO-FILE.
Compiling and Running
If you have GnuCOBOL installed (sudo apt install gnucobol or via Homebrew on macOS), building this application takes exactly one command:
cobc -x -o todo todo.cbl
Run it:
./todo
Why This Matters
Take a moment to appreciate what you just compiled.
You have built a fully functional database application without actually using a database. The entire state of your application is managed by moving pointers across the physical sectors of your hard drive.
When you type a task and hit enter, you aren't invoking an API that translates JSON to a backend controller, which triggers an ORM, which generates a SQL query, which invokes a database engine. You are taking 58 bytes from your keyboard and writing them directly into a text file in a perfectly aligned, predictable block.
It is fast, it is incredibly reliable, and it demystifies how computers actually handle data at rest. When you understand how to implement the Cassette Tape Pattern yourself, you stop viewing modern software layers as "magic" and start seeing them for what they are: layers of abstraction.
Sometimes, those abstractions are necessary. But mostly, they are just bloat.