The question file is a plain text file — one question per line, seven
fields separated by |. load_questions opens it, reads it line by
line using tci_getline, and parses each line using tciu_split.
The result is a heap-allocated array of question_t * pointers.
This is precisely why those two functions were worth writing.
Opening the file
load_questions takes a file path and a pointer to an int where it
will store the count. It returns a question_t ** — an array of
pointers to individually allocated question structs:
question_t **load_questions(const char *path, int *count)
{
int fd;
char *line;
char *nl;
char **fields;
question_t **questions;
question_t *q;
int capacity;
int i;
fd = open(path, O_RDONLY);
if (fd < 0) {
tci_printf("error: cannot open '%s'\n", path);
return (NULL);
}
questions = NULL;
*count = 0;
capacity = 0;open is the same syscall used in
c03/01.
O_RDONLY is in <fcntl.h>, which game.h already includes.
Handling the || escape
tciu_split splits on every |, unconditionally. The file format
handles literal pipes with || — the same character doubled, the same
way tci_printf handles %%. A preprocessing step collapses || to
a safe placeholder before the split, and a second step restores it
afterwards.
Add both helpers as static functions above load_questions:
static void encode_pipes(char *s)
{
char *r;
char *w;
r = s;
w = s;
while (*r) {
if (r[0] == '|' && r[1] == '|') {
*w++ = '\x01'; /* C0 control byte — cannot appear in valid UTF-8 text */
r += 2;
} else {
*w++ = *r++;
}
}
*w = '\0'; /* loop exits before the null terminator is copied */
}
static void decode_pipes(char **fields)
{
int i;
char *p;
for (i = 0; fields[i]; i++) {
for (p = fields[i]; *p; p++) {
if (*p == '\x01')
*p = '|';
}
}
}encode_pipes is a compaction loop: r reads ahead, w writes
behind. When r sees ||, w stores the sentinel byte '\x01' and
r skips two characters; otherwise both advance together. '\x01' is
a control byte that cannot appear in a UTF-8[1] question file.
decode_pipes walks every field after splitting and restores each
'\x01' to '|'.
Reading and parsing
The read loop calls tci_getline on every iteration. Each call
returns one line including the trailing '\n', or NULL at EOF:
while ((line = tci_getline(fd)) != NULL) {
nl = tci_strchr(line, '\n');
if (nl)
*nl = '\0';
encode_pipes(line);
fields = tciu_split(line, '|');
free(line); /* tciu_split copied every field; line is no longer needed */
if (!fields || !fields[0] || !fields[4]) {
if (fields)
free(fields);
continue;
}
decode_pipes(fields);The tci_strchr call strips the trailing newline before splitting.
encode_pipes must run before tciu_split — once the line is split,
field boundaries are already set. The guard on fields[4] skips any
line that does not have at least five fields. decode_pipes runs
immediately after validation, so every string that reaches question_t
already has '|' where the file had ||.
free(line) releases the string returned by tci_getline. It was
allocated inside the function; we own it from the moment it is
returned.
Growing the array
The array of question pointers grows dynamically — the same doubling pattern from f05/01:
if (*count == capacity) {
capacity = capacity ? capacity * 2 : 16;
questions = realloc(questions, capacity * sizeof(question_t *));
}realloc(NULL, n) is equivalent to malloc(n), so the first
iteration works correctly with questions initialised to NULL.
Read man 3 realloc — it documents the return value on failure and
the exact behaviour when the pointer or size is zero. Any function
from the standard library that you use for the first time is worth
a man lookup before you assume you understand it.
Filling question_t
Once the array has room, allocate the struct and copy each field:
q = tci_calloc(1, sizeof(question_t));
q->text = tci_strdup(fields[0]);
q->opts[0] = tci_strdup(fields[1]);
q->opts[1] = tci_strdup(fields[2]);
q->opts[2] = tci_strdup(fields[3]);
q->opts[3] = tci_strdup(fields[4]);
q->answer = fields[5] ? fields[5][0] - '0' : 0; /* '0'==48, '1'==49 … */
q->hint = (fields[6] && fields[6][0]) /* absent or empty → NULL */
? tci_strdup(fields[6])
: NULL;
questions[(*count)++] = q;tci_calloc zeroes the struct, so any field not explicitly set starts
as NULL or 0. fields[5][0] - '0' converts the single-digit answer
character to an integer index. If no hint field is present or it is
empty, q->hint stays NULL — the game loop generates one at runtime.
Freeing the split result
tciu_split returns a NULL-terminated char **. Each string in the
array is a separate allocation. Free them all, then free the array:
i = 0;
while (fields[i])
free(fields[i++]);
free(fields);
}
close(fd);
return (questions);
}free_questions
Every allocation made in load_questions is released here:
void free_questions(question_t **questions, int count)
{
int i;
int j;
if (!questions)
return;
for (i = 0; i < count; i++) {
free(questions[i]->text);
for (j = 0; j < 4; j++)
free(questions[i]->opts[j]);
free(questions[i]->hint);
free(questions[i]);
}
free(questions);
}free(NULL) is defined as a no-op in C99, so the free(questions[i]->hint)
call is safe even when hint was never set.
Wire it into main
Replace the main.c stub with a version that loads questions and
confirms the count:
#include "game.h"
int main(int argc, char **argv)
{
question_t **questions;
int count;
if (argc < 2) {
tci_printf("usage: %s <questions.txt>\n", argv[0]);
return (1);
}
questions = load_questions(argv[1], &count);
if (!questions)
return (1);
tci_printf("loaded %d questions\n", count);
free_questions(questions, count);
return (0);
}Create a minimal questions.txt with one line and test:
Which command lists files and directories in a Unix terminal?|ls|dir|list|show|0|The answer is A: ls.make re
./game questions.txtloaded 1 questionsThe loader works. The full question file comes on the last page.