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.

11 min readStruct ↔ line formatReading with fscanfUnique idsLine-based inputTemp file + rename

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.

struct driver1idKiss Annanameanna@example.comemail+36 30 123 4567phone5expMon Tue Wedworking_daysdatabase.txt – one line1⇥Kiss Anna⇥anna@example.com⇥+36 30 123 4567⇥5⇥Mon Tue Wed↵⇥ = TAB between fields · ↵ = newline after the last one · one line per record
Fig. 1 · Same order, same separator, both ways: write and read must mirror each other.

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.

main()menu loopdriver_creation()add"a" appendlist(), list_by_exp()read"r" readmodify()update"r" → temp.txt "w" → renamedelete_driver()delete"r" → temp.txt "w" → renameat start:file_creation()id_setup()
Fig. 2 · One function per operation; each opens and closes the file itself.

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");
}
ModeMeaningUsed for
"r"read; fails if the file does not existlisting, searching, ids
"a"write at the end; creates the file if neededadding a record, creating an empty database
"w"empty the file (or create it) and writeonly 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 this

Taking 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).

typedbuffer after fgets (size 50)thenKiss Anna ↵"Kiss Anna\n"cut the \nstrcspn80 characters ↵first 49 characters, no \ndiscard the restuntil \n or EOF↵"\n" → ""ask againfgets returns NULL at end of input (Ctrl+D): stop, don't use the buffer
Fig. 3 · One line in, one value out – whatever the user typed.

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 line

The 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 again

Add: 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).

database.txtopened with "r"for every record (file_read)id ≠ target: copyid = target: skip / replaceremember: found? any write error?temp.txtopened with "w"rename("temp.txt", …)all good: replaces the old filenot found, or a write failedremove("temp.txt")database.txt stays as it was
Fig. 4 · Copy, then swap. The original is replaced only by a complete new file.

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 that fscanf filled 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 with strtol; stop at end of input.
  • Modify and delete: copy to temp.txt, then rename over the original – no remove before it.