What is Actually EOF?

Source: "You Suck at Programming" YouTube channel — Dave

Part 1: The stdio Layer (fgetc)

Setup

A simple C program that opens a file, reads one character at a time, and prints each one.

fgetc.c — initial structure
#include <stdio.h>
#include <stdbool.h>

int main() {
    FILE *fp = fopen("file.ext", "r");

    if (fp == NULL) {
        // TODO: better error checking
        return 1;
    }

    // read the file
    int c;
    while (true) {
        c = fgetc(fp);

        if (c == EOF) {
            printf("EOF reached, exiting loop\n");
            break;
        }

        printf("Read character: %c\n", c);
    }

    fclose(fp);
    return 0;
}

Key Observations

  • man fopen — tells you to include <stdio.h>. The man page is your documentation.

  • FILE *fpfopen() returns a pointer to a FILE struct. Check for NULL before using.

  • int c — not char. Must be int because fgetc() returns either a character value (0-255) or EOF (-1). A char can’t hold both.

  • fgetc(fp) — reads one character from the file. Returns EOF when there’s nothing left.

  • EOF is a symbolic constant from <stdio.h>. It’s the value -1.

  • <stdbool.h> — needed for true on some compilers (clang on macOS is stricter than gcc on Linux).

Compilation and output

Compile and run
cc fgetc.c -o fgetc
./fgetc
Output
Read character: H
Read character: e
Read character: l
Read character: l
Read character: o
Read character:

Read character: W
Read character: o
Read character: r
Read character: l
Read character: d
Read character:

EOF reached, exiting loop

Printing as integers

Change %c to %d to see the actual integer values:

Output with integer values
Read int: 104    <-- 'H' (hex 0x68)
Read int: 101    <-- 'e'
Read int: 108    <-- 'l'
Read int: 108    <-- 'l'
Read int: 111    <-- 'o'
Read int: 10     <-- newline (0x0a)
Read int: 87     <-- 'W'
...
Read int: -1     <-- EOF

Verify with hex dump:

xxd shows the raw bytes — no EOF character exists in the file
xxd file.ext

EOF is -1. It’s not a byte in the file. It’s the return value of fgetc() when there’s nothing left to read.

Idiomatic form

The verbose while(true) + break pattern is educational. In production, you’d write:

Compact form — assignment inside while condition
int c;
FILE *fp = fopen("file.ext", "r");
while ((c = fgetc(fp)) != EOF) {
    putchar(c);
}
fclose(fp);

Part 2: The Syscall Layer (read)

One layer below stdio. No buffering, no EOF constant. Raw operating system calls.

Setup

read.c — using open/read/close syscalls directly
#include <fcntl.h>
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>

int main() {
    int fd = open("file.ext", O_RDONLY);

    if (fd == -1) {
        // TODO: better error checking
        return 1;
    }

    char buf[1];
    ssize_t n;

    while (true) {
        n = read(fd, buf, 1);

        if (n == -1) {
            printf("Error reading from file\n");
            break;
        }

        if (n == 0) {
            printf("Nothing left to read — EOF\n");
            break;
        }

        // n > 0: we read data
        char c = buf[0];
        printf("Read %zd byte(s): %c\n", n, c);
    }

    close(fd);
    return 0;
}

Key Differences from stdio

Concept stdio (fgetc) syscall (read)

Open

FILE *fp = fopen("file", "r")

int fd = open("file", O_RDONLY)

Returns

Pointer to FILE struct

Integer file descriptor (a number)

Read

fgetc(fp) — returns the char or EOF

read(fd, buf, n) — returns bytes read

EOF signal

EOF constant (-1) from <stdio.h>

read() returns 0 — no constant

Error signal

fgetc() also returns EOF on error

read() returns -1 on error

Header

<stdio.h>

<fcntl.h> (open) + <unistd.h> (read, close)

Close

fclose(fp)

close(fd)

Man page discovery

Each function tells you what header to include:

Man pages guide you to the right headers
man 3 fopen    # stdio.h
man 2 open     # fcntl.h
man 2 read     # unistd.h
man 2 close    # unistd.h
man 3 printf   # stdio.h  (man 1 printf is the bash command — section 3 is the C function)

ssize_t

read() returns ssize_t (signed size):

  • Positive — number of bytes actually read

  • Zero — nothing left to read (EOF)

  • -1 — error occurred

There is no EOF constant at this level. EOF is just "we read zero bytes."

Idiomatic form

Compact form — while loop with read
ssize_t n;
char buf[1];
while ((n = read(fd, buf, 1)) > 0) {
    putchar(buf[0]);
}
// Here: n == 0 (EOF) or n == -1 (error)

Part 3: Standard Input

File descriptor 0 is already open when your program starts. No need to call open().

stdin.c — reading from standard input (your own cat)
#include <stdio.h>
#include <stdbool.h>
#include <unistd.h>

int main() {
    char buf[1];
    ssize_t n;

    while (true) {
        n = read(0, buf, 1);  // fd 0 = stdin

        if (n == -1) {
            printf("Error reading\n");
            break;
        }
        if (n == 0) {
            printf("Nothing left to read — EOF\n");
            break;
        }

        printf("Read %zd byte(s): %c\n", n, buf[0]);
    }

    return 0;
}
Usage — pipe, redirect, or interactive
echo "hello" | ./stdin          # pipe
./stdin < file.ext              # redirect
./stdin                         # interactive — type, then Ctrl+D

What Ctrl+D actually does

Ctrl+D does not send an EOF signal. It tells the terminal to flush its buffer to the program. If the buffer is empty (you just hit Enter or haven’t typed anything), the program receives a read() of zero bytes — which it interprets as EOF.


Part 4: Bash Heredocs

EOF in heredocs is just a convention — any marker works
cat << EOF
Hello World
EOF

This is identical to:

cat << HELLO_LOL
Hello World
HELLO_LOL

Or even:

cat << ___
Hello World
___

EOF is convention. Bash reads everything between the two markers and pipes it to cat via stdin. The marker can be anything — it’s just a delimiter, not the C EOF constant.


Summary

Layer EOF Mechanism How it works

stdio (fgetc)

EOF constant = -1

Library abstraction. fgetc() returns -1 when the file is done. Defined in <stdio.h>.

syscall (read)

Returns 0 bytes

No constant. read() returns 0 — "successfully read zero bytes." You interpret that as done.

terminal (Ctrl+D)

Flushes empty buffer

Terminal sends what it has buffered (nothing) → read() gets 0 bytes → program exits.

bash (heredoc)

Delimiter convention

EOF is just a string marker. Could be anything. Bash matches the opening and closing delimiter.

EOF is not a character in the file. It’s not a signal from the OS. It’s "we tried to read and got nothing back."