Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

sfind-Befehl in C: Eine vereinfachte find-Implementierung für Anfänger

Lerne, wie du eine eigene Version des find-Befehls in C schreibst – perfekt für deine CSCI 493.66 Hausaufgabe. Mit praktischen Beispielen und Erklärungen zu fnmatch, getopt und Inode-Vergleichen.

sfind find-Befehl C-Programmierung CSCI 493.66 Hausaufgabe fnmatch getopt Inode-Vergleich rekursive Verzeichnissuche Dateisystem Unix-Tools Programmierprojekt Glob-Muster Link-Erkennung sfind Implementierung C-Tutorial

Einführung in das sfind-Projekt

Der find-Befehl ist eines der mächtigsten Werkzeuge in der Unix-Welt. In diesem Tutorial zeigst du, wie du eine vereinfachte Version – sfind – in C implementierst. Dies ist genau die Aufgabe aus CSCI 493.66 Assignment 4. Du wirst lernen, wie du Verzeichnisse rekursiv durchläufst, Dateien anhand von Tests filterst und die Ergebnisse ausgibst. Das Ganze ist wie ein einfacher Datei-Explorer aus der Kommandozeile – nützlich für deine Programmierprojekte und Hausaufgaben.

Was ist sfind?

sfind durchsucht ein oder mehrere Verzeichnisse nach Dateien, die einen bestimmten Test erfüllen. Wenn kein Verzeichnis angegeben ist, wird das aktuelle Arbeitsverzeichnis verwendet. Für jede passende Datei wird der relative Pfad zum Startverzeichnis ausgegeben. Anders als das echte find unterstützt sfind nur einen Test pro Aufruf. Das macht die Implementierung einfacher, aber du lernst trotzdem die Kernkonzepte: Rekursion, Dateisystem-Operationen und String-Matching.

Die Tests im Detail

Es gibt zwei Tests:

  • -s filename: Prüft, ob die aktuelle Datei ein harter Link zur angegebenen Datei ist. Das bedeutet, beide haben denselben Inode-Index und befinden sich im selben Dateisystem.
  • -m fileglob: Prüft, ob der Dateiname (ohne Verzeichnispfad) mit einem Glob-Muster übereinstimmt, z. B. 'b?sh'.

Diese Tests sind wie Filter in einer Social-Media-App: Du suchst nach bestimmten Profilen (Dateien), die bestimmte Kriterien erfüllen. Der -m-Test ist wie ein Suchfilter, der nur Posts mit einem bestimmten Hashtag anzeigt.

Vorbereitung: Wichtige Header und Funktionen

Für sfind benötigst du folgende Header:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fnmatch.h>
#include <getopt.h>

fnmatch() wird für den -m-Test verwendet – es vergleicht einen Dateinamen mit einem Glob-Muster. getopt() hilft dir, die Kommandozeilenargumente zu parsen. Beachte: Die Tests kommen nach den Verzeichnisnamen, also musst du die Argumente entsprechend verarbeiten.

Grundstruktur des Programms

Dein Programm sollte so aufgebaut sein:

  1. main(): Verarbeitet die Argumente mit getopt(), ruft die Suchfunktion für jedes Verzeichnis auf.
  2. Suchfunktion (z. B. sfind_dir): Öffnet ein Verzeichnis, liest Einträge, überspringt . und .., ruft für Unterverzeichnisse rekursiv sich selbst auf, für reguläre Dateien wird der Test angewendet.
  3. Testfunktionen: is_link_to() und matches_glob().
  4. Beachte: Alle Funktionen außer main() müssen vor main() definiert werden.

    Rekursive Verzeichnisdurchsuchung

    Die Rekursion ist das Herzstück. Du öffnest ein Verzeichnis mit opendir(), liest Einträge mit readdir(). Für jeden Eintrag baust du den vollständigen Pfad mit snprintf(). Mit stat() oder lstat() (für symbolische Links) holst du die Dateiinformationen. Wenn es ein Verzeichnis ist, rufst du die Funktion rekursiv auf. Achte darauf, symbolische Links nicht zu folgen – verwende lstat().

    Beispielcode für die Rekursion:

    void search_dir(const char *base_path, const char *rel_path, int test_type, const char *test_arg) {
        DIR *dir = opendir(base_path);
        if (!dir) { perror("opendir"); return; }
        struct dirent *entry;
        while ((entry = readdir(dir)) != NULL) {
            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
            char full_path[1024];
            snprintf(full_path, sizeof(full_path), "%s/%s", base_path, entry->d_name);
            struct stat st;
            if (lstat(full_path, &st) == -1) { perror("lstat"); continue; }
            if (S_ISDIR(st.st_mode)) {
                // Rekursion
                char new_rel[1024];
                if (strlen(rel_path) == 0) snprintf(new_rel, sizeof(new_rel), "%s", entry->d_name);
                else snprintf(new_rel, sizeof(new_rel), "%s/%s", rel_path, entry->d_name);
                search_dir(full_path, new_rel, test_type, test_arg);
            } else {
                // Test anwenden
                if (test_type == 1) { // -s Test
                    if (is_link_to(full_path, test_arg)) {
                        printf("%s/%s\n", rel_path, entry->d_name);
                    }
                } else if (test_type == 2) { // -m Test
                    if (matches_glob(entry->d_name, test_arg)) {
                        printf("%s/%s\n", rel_path, entry->d_name);
                    }
                }
            }
        }
        closedir(dir);
    }

    Dieser Code ist wie ein Algorithmus in einer Navigations-App, der alle Straßen (Verzeichnisse) abfährt und nach bestimmten Geschäften (Dateien) sucht.

    Implementierung des -s Tests

    Für den -s-Test vergleichst du die Inode-Nummern und die Geräte-IDs der aktuellen Datei und der Referenzdatei. Verwende stat() für die Referenzdatei und lstat() für die aktuelle Datei. Wenn beide st_ino und st_dev gleich sind, handelt es sich um denselben Inode (harter Link).

    int is_link_to(const char *path, const char *target) {
        struct stat st_path, st_target;
        if (lstat(path, &st_path) == -1) return 0;
        if (stat(target, &st_target) == -1) return 0;
        return (st_path.st_ino == st_target.st_ino && st_path.st_dev == st_target.st_dev);
    }

    Dies ist nützlich, um Duplikate zu finden – ähnlich wie wenn du in deiner Musikbibliothek nach doppelten Songs suchst.

    Implementierung des -m Tests

    Der -m-Test verwendet fnmatch(). Diese Funktion vergleicht einen String mit einem Glob-Muster. Der Rückgabewert 0 bedeutet Übereinstimmung.

    int matches_glob(const char *filename, const char *pattern) {
        return fnmatch(pattern, filename, 0) == 0;
    }

    Glob-Muster sind wie Suchfilter in deiner E-Mail: * steht für beliebig viele Zeichen, ? für genau eines. Das Muster 'b?sh' passt auf bash, aber nicht auf bsh.

    Fehlerbehandlung und Ausgabe

    Fehlermeldungen müssen auf stderr ausgegeben werden. Bei falscher Nutzung zeigst du eine Usage-Nachricht an. Der Exit-Status ist 0 bei Erfolg, 1 bei Fehler. Beispiel:

    if (argc < 2) {
        fprintf(stderr, "Usage: sfind [dir ...] [-s file | -m pattern]\n");
        return 1;
    }

    Denk daran: Nur passende Dateien werden auf stdout ausgegeben, alles andere auf stderr.

    Komplette main()-Funktion mit getopt

    Da die Tests nach den Verzeichnissen kommen, musst du getopt() so konfigurieren, dass es die Optionen erst nach den Verzeichnissen erwartet. Du kannst die Argumente manuell parsen oder getopt() mit einer Schleife verwenden, die die Optionen erkennt. Ein einfacher Ansatz: Zuerst alle Argumente sammeln, dann die Optionen suchen.

    int main(int argc, char *argv[]) {
        int opt;
        int test_type = 0;
        char *test_arg = NULL;
        // Optionen parsen
        while ((opt = getopt(argc, argv, "s:m:")) != -1) {
            switch (opt) {
                case 's':
                    test_type = 1;
                    test_arg = optarg;
                    break;
                case 'm':
                    test_type = 2;
                    test_arg = optarg;
                    break;
                default:
                    fprintf(stderr, "Usage: ...\n");
                    return 1;
            }
        }
        // Verzeichnisse sammeln (optind zeigt auf erstes Nicht-Option-Argument)
        if (optind >= argc) {
            // Kein Verzeichnis: aktuelles Verzeichnis verwenden
            search_dir(".", "", test_type, test_arg);
        } else {
            for (int i = optind; i < argc; i++) {
                search_dir(argv[i], argv[i], test_type, test_arg);
            }
        }
        return 0;
    }

    Beachte: Der relative Pfad für die Ausgabe beginnt mit dem Verzeichnisnamen. Wenn du search_dir mit rel_path = argv[i] aufrufst, dann wird für eine Datei dir/datei ausgegeben.

    Vollständiges Beispiel

    Angenommen, du hast folgende Verzeichnisstruktur:

    /home/user/test/
    ├── dir1/
    │   ├── file1.txt
    │   └── file2.c
    ├── dir2/
    │   └── script.sh
    └── link1 -> dir1/file1.txt

    Mit dem Befehl sfind /home/user/test -m '*.txt' wird ausgegeben:

    /home/user/test/dir1/file1.txt

    Mit sfind /home/user/test -s /home/user/test/dir1/file1.txt wird link1 gefunden, da es ein harter Link (hier symbolisch, aber im Beispiel ein harter Link) zur selben Datei ist.

    Tipps zur Fehlervermeidung

  • Vergiss nicht, closedir() aufzurufen, um Speicherlecks zu vermeiden.
  • Verwende lstat() statt stat(), um symbolische Links nicht zu folgen.
  • Behandle den Fall, dass ein Verzeichnis nicht geöffnet werden kann (z. B. fehlende Rechte) – gib eine Fehlermeldung auf stderr aus und fahre fort.
  • Teste mit verschiedenen Glob-Mustern, um sicherzustellen, dass fnmatch() korrekt arbeitet.

Zusammenfassung

Mit diesem Tutorial hast du die Grundlagen für eine eigene sfind-Implementierung. Du hast gelernt, wie man Verzeichnisse rekursiv durchläuft, Dateien mit fnmatch und Inode-Vergleichen filtert und Kommandozeilenargumente mit getopt parst. Diese Fähigkeiten sind nicht nur für deine Hausaufgabe nützlich, sondern auch für viele andere C-Programmierprojekte – sei es ein eigener Dateimanager, ein Backup-Tool oder eine Suchmaschine für lokale Dateien. Viel Erfolg bei deiner Abgabe!