﻿# 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.

Source: https://laszlonemes.com/university/operating-systems/crud-skeleton · Author: László Nemes

Guide · Operating Systems

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 read `Struct ↔ line format` `Reading with fscanf` `Unique ids` `Line-based input` `Temp 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.

After this session you can

- design a struct and a one-line-per-record text format that can be written and read back exactly
- read records with `fscanf` and check that a whole record was read
- generate unique ids from the file at start-up
- read console input line by line with `fgets`: no overflow, no leftover newline, no endless loop at end of input
- modify and delete safely: write a temporary file, then `rename` it over the original

## 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

```c
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.

> **Fig. 1** · Same order, same separator, both ways: write and read must mirror each other.
>
> *Diagram:* One struct driver in memory is one line in database.txt. The fields are written in a fixed order, separated by a TAB, and the line ends with a newline – so a field may contain spaces, but never a TAB or a newline.

the two formats – they must describe the same line

```c
/* 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.

Common mistake

**%s stops at the first space**

Reading a field with `%49s` works until someone types a phone number like `+36 30 123 4567`. Then `%s` stops after `+36`, the rest of the line no longer matches the format, `fscanf` returns fewer than 6 – and every record after it seems to be gone. Worse: a delete that rewrites the file would drop them for real. Any field that may contain a space needs `%[^\t]`.

In your assignment

**Decide your format first**

Before writing any function, write down your struct and one example line of your file. For each field: can it contain spaces? How long can it be? Is it a number? The array sizes, the `%49[^\t]` widths and the input buffer sizes all follow from these answers – and they must agree.

## 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.

> **Fig. 2** · One function per operation; each opens and closes the file itself.
>
> *Diagram:* The menu loop only reads a choice and calls one function per operation. Each function opens the file itself, in the mode its job needs, and closes it before it returns: append to add, read to list, and a rewrite through a temporary file to modify or delete.

crud - full.c – the menu

```c
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

```c
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

```c
struct driver *a = NULL;
while ((a = file_read(infile)) != NULL) {
    printf("Id=%u Name=%s ...\n", a->id, a->name /* , ... */);
    free(a);
}
fclose(infile);
```

Good to know

**Why not feof()?**

`while (!feof(file))` checks whether a previous read already hit the end – so the loop runs once more after the last record, with a failed read. Testing the result of the read itself, as above, ends the loop at exactly the right moment.

## 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

```c
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).

> **Fig. 3** · One line in, one value out – whatever the user typed.
>
> *Diagram:* Every input reads exactly one whole line with fgets into a buffer of known size. A line that fits loses its newline; a longer line is cut and the rest is thrown away, so it cannot become the answer to the next question; an empty line is asked again.

read_in_console_messages – the core

```c
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

```c
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
```

Common mistake

**Mixing scanf and getchar**

`scanf("%u", &n)` leaves the newline in the input. The next `getchar()` returns it, the next `%[^\n]` reads an empty line, and an input like `abc` is never consumed at all – the loop asks forever. And a loop like `while (getchar() != '\n');` never ends once the input is closed, because `getchar` returns `EOF`, not `'\n'`. Reading whole lines removes the whole family of bugs.

## 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

```c
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).

> **Fig. 4** · Copy, then swap. The original is replaced only by a complete new file.
>
> *Diagram:* Modify and delete never edit database.txt in place. Every record is copied to temp.txt – the target is left out or replaced on the way – and only when everything was written does rename put temp.txt in place of database.txt in one step. If anything fails, temp.txt is deleted and the original is untouched.

One function does both jobs; `replacement` decides which:

rewrite_without_or_with – the copy

```c
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

```c
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

```c
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

```c
read_in_console_messages("Name:\n", a->name, sizeof(a->name));
if (original && strcmp(a->name, "-") == 0)
    strcpy(a->name, original->name);
```

Common mistake

**Modify as delete + add**

Implementing modify as "delete the record, then add a new one" is tempting – both functions already exist. But the record gets a new id, it moves to the end of the file, and if the input fails half-way (the user presses Ctrl+D, the program crashes), the record is lost: it was deleted before the new values existed.

> **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.

In your assignment

**Keep the menu thin**

Resist putting logic into the `switch` in `main`. A case should read its arguments and call one function. When the second assignment needs the same operation from a child process, you call the function – you don't copy code out of the menu.

## 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.

```c
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.
