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.


← Back to COBOL & Z Server