The Question File
Jeopardy![1] is a quiz show created by Merv Griffin in 1964. The format is inverted: the answer is given first, and contestants respond in the form of a question. A clue reads "The capital of France" — the winning response is "What is Paris?" It became one of the most recognised game show formats in television history.
By 1988, Griffin had sold his production company, Merv Griffin Enterprises, to Coca-Cola, and then to Columbia Pictures Entertainment. That corporate handover left a strange trace in the NES ROM: a small Coca-Cola logo sits at offset F08B, never displayed, never used. Coca-Cola had wanted to advertise its ownership before the game shipped; the sale to Columbia happened first.[2]

The NES cartridge (1988) shipped with roughly 50 questions baked into ROM — no more, no less. The Amiga version (1989) loaded questions from floppy disk one at a time as the player progressed. The cartridge could not grow without a new cartridge. The disk could — but only if the program could read a file one line at a time without pulling everything into RAM at once.


The NES version knew how many questions it had before the program ran.
The Amiga version did not — the question file could be as long as the disk
allowed. tci_getline is what makes that possible.
The last chapter was about output — writing formatted text to a file descriptor. This one is the input side of the same pair.
The implementation pages build tci_getline from the ground up — the
read() syscall first, then the static buffer that persists across calls,
then the line extractor. tciu_split follows: once you can read a line from
a file, you need to parse it into fields. This chapter also introduces
libtciutil, the second library. Start at Setup.
What Happens to the Leftovers?
The first thing I tried was the obvious approach: call read(fd, buf, 1) in
a loop, one byte at a time, until I hit '\n'. It worked. Then I thought
about how many system calls it made. Reading a 100-line file at one byte per
call means thousands of trips across the user/kernel boundary. System calls
are not free — each one interrupts the process and switches to kernel mode.
Reading in chunks is not an optimisation; it is the point.
So the read size becomes a compile-time parameter: BUFFER_SIZE. The caller
decides. The function adapts. A single read(fd, buf, BUFFER_SIZE) call
returns up to BUFFER_SIZE bytes at once. The problem is that those bytes
may span multiple lines, or contain no newline at all.
After the first read(), I scanned the buffer for '\n' and returned
whatever preceded it. That worked for the first line. The second call broke
immediately — I had discarded the bytes after the '\n', which were the
beginning of the next line. They had to go somewhere.
The answer was a static variable at file scope. A local variable
vanishes when the function returns. A global works but any translation
unit can touch it. A file-scope static persists between calls and
remains private to this translation unit — no other source file can
name it:
static char *leftover;On the first call, leftover is NULL. After extracting a line, the bytes
remaining after the '\n' are stored in leftover. On the next call,
leftover is checked before read() is called again. tci_strchr from
c01/03 scans it for '\n';
if found, the line is extracted without another read. A private static
strjoin helper accumulates new chunks into leftover when the buffer
does not yet contain a full line.
At EOF, read() returns 0. If leftover is non-empty, it is the last line
— a file with no trailing '\n'. Return it. If leftover is empty, the
file is fully consumed. Return NULL.
tciu_split came from the same practical need. Once you have a line like
"one|two|three", you need to split it on '|'. There is no split in
the standard library — so tciu_split does not belong in libtci. It lives
in libtciutil: a second library for original utilities with no standard
equivalent. The function makes three passes: count the fields, allocate a
NULL-terminated char ** with tci_calloc, fill each entry with
tci_strndup of the substring. Any character can serve as the separator.
The Project
Add tci_getline to libtci and tciu_split to the new libtciutil library:
| Function | Library | File |
|---|---|---|
char *tci_getline(int fd) | libtci | tci_getline.c |
char **tciu_split(char const *s, char sep) | libtciutil | tciu_split.c |
tci_getline rules:
- Returns the next line from
fd, including the trailing'\n'if present - Returns NULL at EOF or on a read error
- Must work for any compile-time
BUFFER_SIZE ≥ 1 - Supports multiple file descriptors open simultaneously, each with its own saved state
tciu_split rules:
- Returns a NULL-terminated array of strings split on
sep - NULL input returns NULL
- Consecutive separators count as one — no empty strings in the result
- The caller owns the returned array and all strings in it
Compile with: gcc -Wall -Wextra -g -std=c99 -D BUFFER_SIZE=32
The Tester
The companion repo contains test.sh. Clone it once, copy test.sh into
your working directory, and run it:
git clone https://github.com/thecodingidiot-com/c03-the-reader.git
cp c03-the-reader/test.sh ~/c03-practice/
bash test.shTwo suites run sequentially. The tci_getline suite feeds known files through
your implementation and checks the output line by line — short file, long
file, no trailing newline, empty file, multi-fd interleave. The
tciu_split suite passes known strings and checks the resulting arrays.
The tester recompiles with multiple BUFFER_SIZE values including 1.
The Companion Repo
The reference solution is at
github.com/thecodingidiot-com/c03-the-reader.
The solution/ directory contains tci_getline.c, tciu_split.c, the updated
libtci.h, libtciutil.h, a Makefile, test.sh, and test fixture files.