Fájlkezelés, mappák

Czirkos Zoltán · 2021.08.24.

Röviden a fájlkezelésről azoknak, akik nem akarják megvárni az erről szóló előadást.

1. Fájlok

C-ben nagyon egyszerű a szövegfájlok kezelése. A szabványos bemenetet és kimenetet kezelő scanf() és printf() függvényeknek is van olyan változata, amelyik fájlból és fájlba dolgozik. Ezek az fscanf() és az fprintf(). Ezek első paraméterként megkapják a fájlt, amellyel dolgozniuk kell, amúgy pedig a használatuk teljesen megegyezik az előbb említett függvényekkel.

Fájlt megnyitni, létrehozni az fopen() nevű függvénnyel lehet. Ennek visszatérési értéke egy ún. file handle, amellyel a megnyitott fájlra hivatkozunk (mert egyszerre több fájllal is dolgozhatunk). A használat nagyon röviden az alábbi programban látszik. Ez a klasszikus „helló, világ” program, azzal a különbséggel, hogy nem a képernyőre, hanem a hello_world.txt fájlba írja az üzenetet.

#include <stdio.h>

int main(void) {
    /* Az fp változóval hivatkozunk majd a nyitott fájlra. */
    FILE* fp;
    
    /* Létrehozzuk a fájlt, w = write = írni fogunk bele. */
    fp = fopen("hello_world.txt", "w");
    
    /* Beleírjuk a "Helló, világ!" szöveget. */
    fprintf(fp, "Helló, világ!\n");
    
    /* Végeztünk, bezárjuk a fájlt. */
    fclose(fp);

    return 0;
}

A megnyitás sikerességét egyébként ellenőrizni kell, mert előfordulhat, hogy nem sikerül valamilyen okból létrehozni a fájlt (pl. rossz helyen próbáljuk, nincs oda írási jogunk, és így tovább). A hibát úgy látjuk, hogy az fopen() függvény NULL értéket ad vissza. Ilyenkor a perror()-ral szokás hibaüzenetet kiírni, mert az egyből kiírja a sikertelenség okát is. És természetesen ilyenkor a fájlműveleteket (írás, zárás) nem végezhetjük el, mert nincs értelme.

#include <stdio.h>

int main(void) {
    FILE* fp;
    fp = fopen("szamok.txt", "w");  /* file-open, w = write */
    if (fp != NULL) {
        for (int i = 1; i <= 10; ++i)
            fprintf(fp, "%d\n", i); /* file-printf */
        fclose(fp);                 /* file-close */
    }
    else {
        perror("Nem sikerült megnyitni a fájlt");
    }

    return 0;
}

Az olvasás ugyanez; w helyett r (mert read), és fprintf() helyett fscanf(). Beolvasás közben a fájlból sorban kapjuk az adatokat, az elejétől végéig; mintha a fájl tartalmát a felhasználó folyamatosan gépelné be.

Fájl beolvasásánál gyakori az, hogy nem közvetlenül a fájlból fscanf()-elünk, hanem komplett sorokat olvasunk be, és utána a beolvasott sorokból, sztringekből vesszük ki az adatokat. Ez azért előnyös, mert így könnyebb kezelni a hibás fájlokat: tudjuk, hogy mekkora egységeket olvasunk be a fájlból, nem a sor közepén akad el hiba esetén a beolvasás. A beolvasott sor tartalma alapján pedig bonyolultabb esetszétválasztásokat is meg tudunk csinálni. A módszer ehhez hasonló:

FILE* fp;
fp = fopen("fajl.txt", "r");  /* r = read */

/* ... */

char sor[101];
fgets(sor, 101, fp);
/* beolvasott sor kezelése... */
if (baj_van) {
    printf("Hibás sor: %s", sor);
}

/* ... */

A beolvasott soron akár sscanf(), strtok() vagy más sztringkezelő függvények is használhatók.

2. Mappák

A mappák kezelésére elvileg nincsen szabványosított módszer. Azonban a gyakorlatban szinte minden környezetben rendelkezésre áll a dirent.h nevű fejlécfájl, és annak opendir(), readdir() és closedir() nevű függvényei.

Ha egy mappa tartalmát listázni szeretnénk, előbb meg kell nyitni azt az opendir() nevű függvénnyel. Ez egy DIR * típusú mutatót ad vissza; az fopen()-hez hasonlóan NULL értékkel jelzi, ha hiba volt. Ha nem, akkor a readdir() függvény sorozatos hívásaival kapjuk meg az egyes bejegyzések adatait. Ha elfogytak, akkor ez is NULL értéket ad. Végül pedig bezárhatjuk a mappát a closedir() függvénnyel.

A readdir() függvény egy struct dirent * típusú mutatót ad arra a struktúrára, amelyből a fájl adatai kiolvashatóak. Ennek tartalma operációs rendszertől függő, de ami biztosan van, az a d_name nevű adattag: ebben van a fájl (vagy épp mappa) neve.

A három függvény használatát az alábbi kód mutatja be:

#include <dirent.h>
#include <stdio.h>

int main(void) {
    DIR *d = opendir(".");  // melyik mappát
    if (d == NULL) {
        perror("Nem sikerült megnyitni");
    } else {
        struct dirent *de;
        while ((de = readdir(d)) != NULL) {
            printf("%s\n", de->d_name);
        }
        closedir(d);
    }
    return 0;
}

Ügyelni kell arra, hogy a listában benne lehet az aktuális mappát jelképező ., és a szülő mappát jelképező .. is. Ha ezekre nincsen szükség, akkor ki kell őket szűrni a listázás közben.

Az egyes listázott fájlok többféle típusúak lehetnek, pl. rendes fájlok, vagy további mappák is. Egy fájl tulajdonságai, elérési jogai és egyéb információk a stat() nevű függvénnyel kérdezhetőek le, amelyik egy struct stat típusú változóba írja be az adatokat. Legfontosabb ezek közül az st_mode, a fájl típusa, és az st_size, a fájl mérete. Az st_mode adattag tartalmát az S_ISREG() és S_ISDIR() makrók segítenek értelmezni: rendes fájl-e, mappa-e. Az st_size pedig csak fájlnál hasznos információ, mappánál nem igazán.

Példa a stat() használatára:

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>

#include <stdio.h>

int main(void) {
    struct stat s;
    if (stat("pelda.txt", &s) < 0) {  // melyik fájlt/mappát
        perror("Hiba, stat() sikertelen");
    } else {
        if (S_ISREG(s.st_mode))
            printf("Ez egy fajl, merete: %ld bajt", s.st_size);
        else if (S_ISDIR(s.st_mode))
            printf("Ez egy mappa");
        else
            printf("Egyeb");
    }
    return 0;
}