Guide · Operating Systems
Records in a file: a CRUD skeleton for the assignment
How a record keeper over a text file is built: a struct and its TAB-separated line, reading records with fscanf, unique ids, line-based input that cannot overflow, and modify and delete through a temporary file and rename – the base of the first take-home assignment.
The first take-home assignment (beadandó) is a record keeper: a C program with a menu that stores records in a text file and can add, list, modify and delete them – CRUD, for create, read, update, delete. The second assignment continues the same program and extends it with processes and inter-process communication, so the structure you choose now is the one you will build on.
This guide walks through a complete skeleton of that kind – a register of drivers – and explains each decision: the file format, how ids stay unique, how input is read without overflowing a buffer, and why modify and delete go through a temporary file. Your topic and fields will differ; the decisions are the same.
A record and its line
A record is a struct. Fixed-size char arrays keep it simple: the struct can be copied with =, and – useful later – sent through a pipe or a message queue as it is, because it contains no pointers.
crud - full.c – the record
struct driver
{
unsigned int id;
char name[50];
char email[50];
char phone[50];
unsigned int exp; // years of experience
char working_days[60];
};In the file, one record is one line, and the fields are separated by a TAB character (Fig. 1). A space would be a bad separator: names, phone numbers and working days contain spaces. A TAB doesn't occur in normal input, and the program makes sure it can't – it replaces any TAB typed by the user.
the two formats – they must describe the same line
/* write: one record = one line, TAB between the fields */
fprintf(outfile, "%u\t%s\t%s\t%s\t%u\t%s\n",
a.id, a.name, a.email, a.phone, a.exp, a.working_days);
/* read: %49[^\t] = at most 49 characters up to the next TAB – spaces allowed */
int read_fields = fscanf(file, "%u\t%49[^\t]\t%49[^\t]\t%49[^\t]\t%u\t%59[^\n]",
&actual->id, actual->name, actual->email,
actual->phone, &actual->exp, actual->working_days);%49[^\t] reads at most 49 characters that are not a TAB – 49, because the array has 50 bytes and the closing '\0' needs one. The TAB in the format string matches the separator. The last field reads up to the newline instead.
The structure of the program
main is only a menu loop: it reads a choice and calls one function. Every operation is a function that opens the file itself, in the mode its job needs, and closes it before returning (Fig. 2). No FILE * stays open between two menu choices, so what is on disk is always the current state.
crud - full.c – the menu
read_in_console_messages("", command, sizeof(command));
if (command[1] != '\0')
command[0] = '?'; // "12" or "1a" is not a menu item
switch (command[0]) {
case '1': driver_creation(); break;
case '4': modify(read_number("Id:\n")); break;
/* ... */
default: printf("Invalid command\n");
}| Mode | Meaning | Used for |
|---|---|---|
"r" | read; fails if the file does not exist | listing, searching, ids |
"a" | write at the end; creates the file if needed | adding a record, creating an empty database |
"w" | empty the file (or create it) and write | only the temporary file – never database.txt itself |
Reading records
One function reads one record. It returns a freshly allocated struct, or NULL at the end of the file – and also when a line doesn't match the format, because fscanf returns the number of fields it filled.
file_read
struct driver *file_read(FILE *file)
{
struct driver *actual = malloc(sizeof(struct driver));
if (!actual)
return NULL;
int read_fields = fscanf(file, /* the format above */ ...);
if (read_fields != 6) { // EOF, or a line that doesn't match
free(actual);
return NULL;
}
return actual; // the caller frees it
}Every operation that reads uses the same loop: read, use, free.
list – the loop
struct driver *a = NULL;
while ((a = file_read(infile)) != NULL) {
printf("Id=%u Name=%s ...\n", a->id, a->name /* , ... */);
free(a);
}
fclose(infile);Unique ids
Each record gets an id that no other record has. At start-up, id_setup reads the whole file and takes the largest id plus one; after that, every new record takes the current value and increments it.
id_setup
id = 0;
while ((actual = file_read(infile)) != NULL) {
if (actual->id > id) // the largest, not the last one read
id = actual->id;
free(actual);
}
id++; // the next new record gets thisTaking the largest, not the last id read, keeps this correct even if the lines are not in id order – for example after the file has been edited by hand. And because the counter is only ever incremented while the program runs, an id freed by a delete is not handed out again in the same run.
Safe input
Console input is where C programs break: scanf("%s") writes past the end of the array, a leftover newline becomes the answer to the next question, and a closed input stream turns a menu loop into an endless loop. The skeleton avoids all three with one rule: every input reads exactly one whole line (Fig. 3).
read_in_console_messages – the core
if (fgets(result, size, stdin) == NULL) { // end of input (Ctrl+D)
printf("\nEnd of input\n");
exit(1);
}
size_t len = strcspn(result, "\n");
if (result[len] == '\n')
result[len] = '\0'; // the whole line fit: cut the newline
else
discard_line(); // too long: throw away the rest of the lineThe text fields are read straight into the struct's arrays, with the array's own size, so no field can overflow. Numbers are read as a line too, and converted with strtol, which tells you where the number ended – if that is not the end of the line, the input wasn't a number:
read_number – the check
read_in_console_messages(console_text, line, sizeof(line));
long value = strtol(line, &end, 10);
if (*end == '\0' && value >= 0) // the whole line was a number, and not negative
return (unsigned int)value;
printf("Please give a non-negative whole number\n"); // ... and ask againAdd: append a line
Adding is the simplest operation: read the fields, assign the next id, open the file in append mode and write one line.
driver_creation
driver_input(&a, NULL); // all fields from the console
a.id = id++; // a new, unique id
FILE *outfile = fopen("database.txt", "a"); // "a": write at the end
if (!outfile) { fprintf(stderr, "Cannot open file!\n"); exit(1); }
fprintf(outfile, /* the line format */ ...);
fclose(outfile);Modify and delete: temp file and rename
A text file can't be edited in the middle: a modified line may be longer or shorter than the old one, and a deleted line leaves a gap. So both operations rewrite the file: every record is copied to temp.txt, the target is left out (delete) or replaced (modify) on the way, and the new file takes the old one's place (Fig. 4).
One function does both jobs; replacement decides which:
rewrite_without_or_with – the copy
while ((a = file_read(infile)) != NULL) {
const struct driver *out = a;
if (a->id == target_id) {
found = 1;
out = replacement; // NULL when deleting: nothing is written
}
if (out && fprintf(outfile, /* the line format */ ...) < 0)
write_error = 1;
free(a);
}rewrite_without_or_with – the swap
fclose(infile);
if (fclose(outfile) != 0) // the last buffered bytes are written here
write_error = 1;
if (!found || write_error || rename("temp.txt", "database.txt") != 0) {
remove("temp.txt"); // database.txt is untouched
return 0;
}
return 1;rename replaces an existing target file in one step: at every moment there is either the complete old database.txt or the complete new one. Calling remove("database.txt") first – a common habit – opens a window in which the database doesn't exist at all, and if the rename then fails, the data is gone.
Modify: look first, ask, then write
Modify finds the record first and changes nothing until the user has entered every new value. Then it writes the modified record back in its place – with the same id.
modify
struct driver *for_modification = find_driver(mod_id);
if (!for_modification) { printf("No driver found to modify\n"); return; }
struct driver modified = *for_modification; // keeps the id
driver_input(&modified, for_modification); // "-" keeps a field
rewrite_without_or_with(mod_id, &modified); // replace in place
free(for_modification);The input function knows the original record, so the user can keep a field by typing - (and keep the experience with 0):
driver_input – keeping a field
read_in_console_messages("Name:\n", a->name, sizeof(a->name));
if (original && strcmp(a->name, "-") == 0)
strcpy(a->name, original->name);Report what really happened. Deleting an id that doesn't exist is not "Delete was successful". The rewrite function returns whether it found the record, and the message follows from that.
From here to the second assignment
The second assignment builds on this program with the tools of the later sessions: processes, signals, pipes, message queues, shared memory and semaphores. The skeleton is already shaped for that:
- The record is a plain struct without pointers – it can travel through a pipe or a message queue as raw bytes.
- Each file operation is a self-contained function that opens and closes the file – it can be called from any process.
- Input and file handling are separate (driver_input vs rewrite_without_or_with) – a record can come from the keyboard or from another process.
- Records are found by id – an id is the natural thing to send in a message.
Practice
Task 1
Search by name
Add a menu item that asks for a piece of text and lists every driver whose name contains it. An empty result should say so.
Approach
The same read loop as list, with a condition. strstr returns a pointer to the first occurrence, or NULL. Count the matches to print the "nothing found" message after the loop.
if (strstr(a->name, pattern) != NULL) // pattern occurs in the name
printf(...);Task 2
A new field
Add a field to the record, for example the licence category. Existing records should keep working after the change – decide what that means for the file.
What has to change
The struct, the fprintf format, the fscanf format and its expected count, the input function, the list output. Six places – that is why the format is decided first. Old lines now have one field too few and no longer match the new format: either convert the file once, or read each line with fgets and parse it with sscanf – there the returned count tells you whether the new field is present.
Task 3
Delete all below a limit
Add an operation that deletes every driver with less experience than a number the user gives, and reports how many were deleted. The file must be rewritten once, not once per deleted record.
Approach
The rewrite loop with a different condition: instead of comparing the id with one target, test a->exp < limit and count. The swap at the end – check, rename or remove – stays exactly the same.
Summary
- One record = one struct = one line; TAB between fields; the write and read formats mirror each other.
- Read fields that may contain spaces with
%49[^\t], and check thatfscanffilled every field. - New id = largest id in the file + 1; never reuse an id in the same run.
- Read every input as one whole line with
fgets; convert numbers withstrtol; stop at end of input. - Modify and delete: copy to
temp.txt, thenrenameover the original – noremovebefore it.