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.
#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 *fp—fopen()returns a pointer to aFILEstruct. Check forNULLbefore using. -
int c— notchar. Must beintbecausefgetc()returns either a character value (0-255) orEOF(-1). Acharcan’t hold both. -
fgetc(fp)— reads one character from the file. ReturnsEOFwhen there’s nothing left. -
EOFis a symbolic constant from<stdio.h>. It’s the value-1. -
<stdbool.h>— needed fortrueon some compilers (clang on macOS is stricter than gcc on Linux).
Compilation and output
cc fgetc.c -o fgetc
./fgetc
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:
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 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:
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
#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 |
|
|
Returns |
Pointer to |
Integer file descriptor (a number) |
Read |
|
|
EOF signal |
|
|
Error signal |
|
|
Header |
|
|
Close |
|
|
Man page discovery
Each function tells you what header to include:
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
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().
#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;
}
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
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 ( |
|
Library abstraction. |
syscall ( |
Returns |
No constant. |
terminal (Ctrl+D) |
Flushes empty buffer |
Terminal sends what it has buffered (nothing) → |
bash (heredoc) |
Delimiter convention |
|
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."