Programmieren in C


von Prof. Jürgen Plate

5 Dateien

Dateien sind ein allgemeines Konzept für die permanente Speicherung bzw. Ein-/Ausgabe. Dateien bestehen aus beliebig vielen Komponenten eines bestimmten Datentyps. Je nach Dateiart können die einzelnen Komponenten sequentiell oder wahlfrei gelesen werden. Am Ende einer Datei können weitere Komponenten hinzugefügt werden. Die Abbildung der abstrakten Dateistruktur auf reale Speichergeräte (Platte, Band, Drucker, etc.) erfolgt durch das Betriebssystem.

Eine Datei besitzt also lauter Komponenten gleichen Typs. Zusammen mit der Dateivariable wird implizit auch ein Pufferbereich im Arbeitsspeicher definiert, der mindestens eine Dateikomponente (--> aktueller Wert) aufnehmen kann. Auf diesen Datenwert wird dann über die Dateivariable zugegriffen. Zusammen mit dem Typ "Datei" müssen einige Standardoperationen definiert sein, die den Zugriff auf die Datei erlauben:

5.1 Zugriffsart

Nach Art des Zugriffs auf die Komponenten wird unterschieden in sequentielle Dateien und Dateien mit wahlfreiem Zugriff.

5.2 Grundlegende Abläufe auf Dateien

Der Zugriff auf Laufwerke bzw. allg. auf die Peripherie erfolgt in C ebenfalls über Zeiger (sog. Streams oder Filepointer). Diesbezüglich gilt alles als "Datei"; prinzipiell muß eine Datei vor dem Zugriff auf sie geöffnet, und hinterher wieder geschlossen werden. Verwendet werden Funktionen, Datentypen, etc. aus der Standardbibliothek <stdio.h>.
Wenn in einem Programm nichts weiter vereinbart wird, so wirken die Schreib-und Lesebefehle auf sogenannte Standarddateien. Diese sind durch das Betriebssystem vordefiniert.(z. B. Tastatur als Eingabe und Bildschirm als Ausgabe). Will man das ändern, so kann man die Standard-E/A z. B. durch UNIX-Kommandos umlenken.

Programmname < Dateiname
Dieses Kommando bewirkt, daß das Programm von der angegebenen Datei liest.

Programmname > Dateiname
Dieses Kommando bewirkt, daß das Programm auf die angegebenen Datei ausgibt.

Die sogenannten Standard-Filepointer sind immer initialisiert, die zugehörigen Dateien immer geöffnet:

stdin Standardeingabe (Tastatur)
stdout Standardausgabe (Bildschirm)
stderr Fehlerausgabe (Bildschirm!)

Die Unterscheidung zwischen stdout und stderr ist beispielsweise dann relevant, wenn man die Bildschirmausgabe (stdout) in irgendwelche Files umleitet (z. B. mit '>' unter DOS und UNIX), aber verhindert werden soll, daß auch etwaige Fehlermeldungen dorthin "verschwinden".

Ein einfaches Beispiel für Zugriff auf die Standarddateien stdin und stdout:


/* Programm 'cat' */

#include <stdio.h>
 
int main(void)
  { 
  int ch;
  while ((ch = getchar()) != EOF)
    putchar(ch);
   return 0;
  }

Solange Eingabedaten von der Standardeingabe kommen werden sie auf der Standardausgabe wieder ausgegeben. Das Programm "echot" also die Standardeingabe. Mit der Ausgabeumleitung kann man damit Tastatureingaben in eine Datei schreiben, z. B. durch den Programmaufruf: cat > mein.text. Mittels Eingabeumleitung kann man Dateiinhalte auf den Bildschirm bringen, z. B.: cat < mein.text.

Eine Unschönheit von C kann man an diesem Programm gut erkennen. Die Variable ch ist nicht - wie man es erwarten sollte - als char definiert, sondern als int. Das liegt einzig und alleine daran, daß die vordefinierte Konstante EOF den Wert -1 besitzt und daher bei der Verwendung einer char-Variablen das Datei- bzw. Eingabeende nicht erkannt würde.

Das Eingabeende unter UNIX wird durch das Zeichen Ctrl-D (bzw. auf der deutschen Tastatur Strg-D) repräsentiert. Unter DOS/Windows ist es das Zeichen Ctrl-Z (bzw. auf der deutschen Tastatur Strg-Z).

Beispiel: Kopieren der Standardeingabe zur Standardausgabe; Zählen der Byteanzahl.


#include <stdio.h>

int copy(void);

void main()
  {	
  int bytes;
  int blocks;
  bytes = copy();
  fprintf (stderr, "%d Bytes kopiert.\n", bytes);
  }

int copy(void)
  {
  int c;
  int bytes;
  bytes = 0;
  while ((c = getchar()) != EOF)
    {
    putchar(c);
    bytes = bytes + 1;
    }
  return (bytes);
  }

Beispiel: Kopieren der Standardeingabe zur Standardausgabe; Numerieren aller Zeilen.


#include <stdio.h>


#include <stdio.h>
#define YES 1
#define NO 0
main() 
  {
  short nl_anz;
  int c, zeile_nr;
  zeile_nr = 1;
  nl_anz = YES;
  while ((c = getchar()) != EOF)
    {
    if(nl_anz)
      {
      printf("%6d ", zeile_nr);
      zeile_nr = zeile_nr + 1;
      }
	if(c != '\n') 
	  nl_anz = NO;
	else 
	  nl_anz = YES;
	putchar(c);
    }	
  }

Beispiel: Worte, Zeichen und Zeilen zählen.


#include <stdio.h>

#define IMWORT	1
#define AUSSEN	0

main()
  {
  int c,wo;
  int nc, nw, nl;		    /* Zeichen, Worte, Zeilen */
	
  wo = AUSSEN;              /* Anfangswerte */
  nc = nw = nl = 0;
  while((c = getchar())!= EOF)
    {
    ++nc;
    if(c == '\n') ++nl;
    if( (c == '\n') || (c == '\t') || (c == ' ') ) wo = AUSSEN;
    else 
      if(wo == AUSSEN) 
        { 
        wo=IMWORT; 
        ++nw;
        }
    }
	printf("\nDas waren %d Zeichen, %d Worte und %d Zeilen.\n",nc,nw,nl);
  }

Beispiel: Wortlängen im Eingabestrom ermitteln
Es wird die länge eines jeden Wortes bestimmt und die Längen zwischen 1 und 25 gezählt. Alle Wörter, die länger als 25 Zeichen sind, werden in einem 26. Zähler registriert. Als Ausgabe erhält man die Anzahlen der der Worte:


#include <stdio.h>

#define MAXWORDLEN 26               /* maximale Wort-Laenge + 1*/

int main(void)
  {
  int c;                            /* gelesenes Zeichen */
  int wl,zl;                        /* Wortlaenge, Zeilenlaenge */
  int i;                            /* Schleifenzaehler */
  int words,lines;		            /* Wortzaehler, Zeilenzaehler */
  int maxline,maxword;              /* Maximallaengen */
  int wordlen[MAXWORDLEN];          /* speichert die Wortlaengen */

  zl = words = maxline = maxword = lines = 0;
  for ( i=0; i < MAXWORDLEN; i++ )
    wordlen[i] = 0;
  c = getchar();                     /* 1. Zeichen lesen */
  while (c != EOF)
    {
    while (!(('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))
	   && (c != '\n') && (c != EOF))
      {                               /* Wortanfang suchen */
      c = getchar();                  /* Zeichen lesen */
      zl++;                           /* Zeilenlaenge erhoehen */
      }
    wl = 0; 			              /* Wortlaenge auf 0 setzen */
    while ((c != EOF)
	   && ((('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z'))
	   || ('0' <= c && c <= '9') || (c == '_')))
      {                               /* naechstes Wort lesen */
      c = getchar();                  /* Zeichen lesen */
      zl++;                           /* Zeilenlaenge erhoehen */
      wl++;                           /* Wortlaenge erhoehen */
      }
    if (wl > 0) words++;              /* Wortzaehler erhoehen */
    if (wl > maxword ) maxword = wl;  /* max. Wortlaenge */
    if (wl >= MAXWORDLEN)             /* groesser als MAXWORDLEN? */
      wordlen[MAXWORDLEN-1]++;        /* ja - ins letzte Element  */
    else
      wordlen[wl-1]++;                /* passenden Zaehler erhoehen */

    if ((c == '\n') || (c == EOF))    /* neue Zeile? */
      {
      lines++;                        /* Zeilenzaehler erhoehen */
      if (zl > maxline)
	maxline = zl;                     /* max. Zeilenlaenge */
      zl = 0;
      c = getchar();
      }
    }

  for ( i=0; i < MAXWORDLEN; i++ )    /* Maximalzahl der Worte */
    printf("%10d | %10d\n", i, wordlen[i]);
  
  return 0; 
  }

Sequentielle Dateien

Will man zusätzlich zu den Standard-Dateien noch weitere Dateien auf der Platte benutzen, so muß man diese Dateien an das Programm anbinden. Alle Befehle zur Dateibearbeitung sind nicht Teil von C. Die Standardbibliotheken enthalten aber viele Funktionen zum Arbeiten mit Dateien.

Im folgenden Beispiel wird eine Datei sequentiell beschrieben und wieder gelesen. An diesem Beispiel werden einige grundlegende Befehle erklärt, weshalb der Quelltext mit Zeilennummern in eckugen Klammern versehen wurde. Die Zeilennummern finden Sie in den Erklärungen unten wieder:


[ 1] #include <stdio.h>
[ 2] 
[ 3] int main(void)
[ 4]   { 
[ 5]   FILE *fp;
[ 6]   int i,xi;
[ 7]   static char dateiname[]="daten.datei";
[ 8]   char text[80];
[ 9]
[10]   /* Beschreiben der Datei */ 
[11]   fp = fopen(dateiname,"w");
[12]   if ( fp == NULL)
[13]     { 
[14]     fprintf(stderr,"Datei %s kann nicht zum Schreiben\
[15]                     geoeffnet werden\n",dateiname);
[16]     exit(1);
[17]     }
[18]   for ( i=0; i<10; i=i+2)
[19]     fprintf(fp,"%d\n",i); 
[20]   fclose(fp);
[21]
[22]   /* Lesen der Datei */
[23]   fp = fopen("meine.dat","r");
[24]   if (fp == NULL)
[25]      {
[26]      fprintf(stderr,"Datei %s kann nicht zum Lesen\
[27]                      geoeffnet werden\n",dateiname);
[28]      exit(2);
[29]      }
[30]   while(feof(fp) == 0) 
[31]      { 
[32]      fscanf(fp,"%d",&xi);
[33]      printf("%d",xi);
[34]      }
[35]   fclose(fp);
[36]   exit(0);
[37]   }

Im Programm finden wir folgende Dateianweisungen:

  1. [ 5] Definieren einer Datei:
    FILE *<Dateizeiger>
    FILE ist eine spezielle Stream-Struktur, der Datentyp für Dateien. Er ist in der Standardbibliothek <stdio.h> als Struktur festgelegt, die Informationen über die Datei enthält (z. B. Pufferadresse, Zugriffsrechte u.s.w. ).
    Mit obiger Anweisung wird ein Zeiger auf den Datentyp FILE definiert.

  2. [11], [23] Öffnen einer Datei:
    <Dateizeiger> = fopen(<Dateiname>,<Zugriffsmodus>);
    Die Funktion fopen verbindet den externen Namen der Datei mit dem Programm und liefert als Ergebnis den Zeiger auf die Beschreibung der Datei. Im Fehlerfall wird der NULL-Zeiger zurückgeliefert. Die Funktion ist definiert als

    FILE *fopen(const char *filename, const char *modus)

    als Zugriffsmodus steht zur Verfügung eine Kombination von "a", "r", "w" und "+":

    Durch anhängen eines Zusatzes kann festgelegt werden, ob es sich bei der zu bearbeitenden Datei um eine Binär- oder Textdatei handelt:

    Die Funktion fopen reagiert folgendermaßen:

    Maximal FOPEN_MAX Dateien können gleichzeitig geöffnet werden, maximale Dateinamenlänge: FILENAME_MAX.
    Zwischen Lesen und Schreiben ist ein Aufruf von fflush() oder ein Positionierungsvorgang nötig.

  3. [14], [19], [26] formatierte Ausgabe auf Datei:
    fprintf(<Dateizeiger>, "<Format>",<Werte>);
    Entspricht der Funktion printf und schreibt die angegebenen Werte im angegebenen Format auf die Datei. Dateizeiger verweist auf die Datei, auf die geschrieben wird. fprintf ist definiert als

    int fprintf(FILE*, const char *format, ...)

  4. [32] formatierte Eingabe von Datei:
    fscanf(<Dateizeiger>, "<Format>",<Werte>);
    Entspricht der Funktion scanf und liest die angegebenen Werte im vereinbarten Format der Datei. fscanf ist definiert als

    int fscanf(FILE*, const char *format, ...)

  5. [30] Dateiende abfragen:
    feof(<dateizeiger>);
    Die Funktion feof liefert den Integerwert 1, wenn das Dateiende gelesen wurde, sonst 0.(int feof(FILE*))

  6. [20], [35] Schließen einer Datei:
    fclose(<Dateizeiger>); Die Datei wird geschlossen, vom Programm abgehängt und der Platz für den Filebeschreibungsblock wieder freigegeben. Beim Schreiben auf Dateien sollte die Datei geschlossen werden, sobald alle Schreiboperationen abgschlossen sind, da erst beim Schließen die Dateipuffer auf die Platte geschrieben und die Informationen über die Datei in der Dateiverwaltung aktualisert werden. fclose ist definiert als int fclose(FILE*).

Im 2. Programmbeispiel werden von der Datei "daten.dat" Datenzeilen gelesen. Jede Zeile enthält einen Integerwert, einen Double-Wert und einen Textstring. Diese Daten werden dann mit einer vorangestellten Zeilennummer in die Datei "tabelle.txt" geschrieben.


#include <stdio.h>
#include <stdlib.h>

int  main(void)
  {
  char  msg[100];
  int  n, i;
  double  d;
  FILE  *fpin, *fpout;
  /* Dateien oeffnen */
  fpin  = fopen("daten.dat", "rt");
  fpout = fopen("tabelle.txt", "wt");
  if((fpin == NULL) || (fpout == NULL))
    { /* ... hat nicht funktioniert */
    fprintf(stderr, "Fehler: I/O");
    exit(1);
    }
  /* jetzt neue Datei mit anlegen, mit den Wertesaetzen zeilenweise
     und mit Zeilennummern versehen */
  i = 1;
  for(;;) /* Endlosschleife */
    {
    fscanf(fpin, "%d%lf%s", &n, &d, msg);
    if(feof(fpin)) break; /* Dateiende gelesen, sofort aufhoeren! */
    fprintf(fpout, "%d>  %s  %d  %f\n", i++, msg, n, d);
  }
  /* Dateien schliessen */
  fclose(fpout);
  fclose(fpin);   

  return 0;
  }

Ein weiteres Beispiel: Programm zum Kopieren einer Datei. Das Programm hat zwei Kommandozeilen-Parameter, die Namen von Quell- und Zieldatei. In diesem Beispiel wird besonders auf die Fehlerbehandlung eingegangen. Wenn etwas schief geht, interessiert uns die Ursache, die bei vielen Funktionen als Nummer in errno und als englischer Text in strerror(errno) hinterlegt sind. Deshalb wird die Headerdatei errno.h eingebunden.

Sämtliche Funktionen, deren Ergebniswerte nicht mehr interessieren, sind mit einem Cast nach void versehen:

Sie sehen, in C sollte man sehr, sehr sorgfältig alle Ergebniswerte korrigieren, dies ist häufig viel aufwendiger als der Teil, der die gewünschten Aktionen durchführt.

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <errno.h>


int main(int argc, const char *argv[])
  {
  FILE *infile,*outfile;
  int c,i;

  if(argc != 3)
    {
    (void)fprintf(stderr,
       "Aufruf: %s quelldatei zieldatei\n", argv[0]);
    return(EXIT_FAILURE);
    }
  infile = fopen(argv[1],"rb");
  if(infile == NULL)
    {
    (void)fprintf(stderr,
       "Fehler beim öffnen der Datei %s: %s\n",
       argv[1],strerror(errno));
    return(EXIT_FAILURE);
    }
  outfile = fopen(argv[2],"wb");
  if(outfile == NULL)
    {
    (void)fprintf(stderr,
      "Fehler beim Erzeugen der Datei %s: %s\n",
      argv[2], strerror(errno));
    (void)fclose(infile);
    return(EXIT_FAILURE);
    }
  while((c = getc(infile)) != EOF)
    if(putc(c,outfile) == EOF)
      {
      (void)fprintf(stderr,
        "Fehler beim Schreiben der Datei %s: %s\n",
        argv[2], strerror(errno));
      (void)fclose(infile);
      (void)fclose(outfile);
      return(EXIT_FAILURE);
      }
  if(ferror(infile))
    {
    (void)fprintf(stderr,
      "Fehler beim Lesen der Datei %s: %s\n",
      argv[1], strerror(errno));
    (void)fclose(infile);
    (void)fclose(outfile);
    return(EXIT_FAILURE);
    }
  if(fclose(infile) == EOF)
    {
    (void)fprintf(stderr,
      "Fehler beim Schließen der Datei %s\n", argv[1]);
    (void)fclose(outfile);
    return(EXIT_FAILURE);
    }
  if(fclose(outfile) == EOF)
    {
    (void)fprintf(stderr,
      "Fehler beim Schließen der Datei %s\n", argv[2]);
    return(EXIT_FAILURE);
    }
  return(EXIT_SUCCESS);
  }

5.3 Textdateien

Dies sind Dateien, deren Komponenten Schriftzeichen sind (Typ char). Sie nehmen eine Schlüsselrolle ein, da die Eingabe- und Ausgabedaten der meisten Computerprogramme Textfiles sind (darunter fällt beispielsweise auch die Druckausgabe). Ein Programm kann vielfach allgemein als eine Datentransformation von einer Textdatei in eine andere aufgefaßt werden.

Das Zeilenende-Problem

Nun sind Texte i. a. in Zeilen unterteilt, und es stellt sich die Frage, wie diese Zeilenstruktur auszudrücken ist. In der Regel enthält der in einem DVS verwendete Zeichencode spezielle Steuerzeichen, von denen eines als Zeilenende-Zeichen verwendet werden kann. Bedauerlicherweise verhindert die Realisierung von Textdateien auf Betriebssystemebene eine einfache Realisierung der Textdatei. Diese Unterschiede machen es oft notwendig, Textdateien bei der Übertragung zwischen verschiedenen Rechner- bzw. Betriebssystemen zu konvertieren.

Gepufferte E/A vs. ungepufferte E/A

Wie bei den meisten Betriebssystemen wird auch bei DOS oder UNIX die Ausgabe gepuffert. Das heißt, es wird erst etwas ausgegeben, wenn ein Zeilenende an das Ausgabegerät gesendet wird, oder gar erst, wenn das Programm zuende ist. Falls Sie das nicht möchten, müssen also dafür sorgen, daß nach jedem Zeichen wirklich auch eine Bildschirmausgabe erfolgt. Sie erreichen dies durch den Funktionsaufruf fflush(stdout); nach jeder Ausgabe.
Die Funktion fflush (FILE *datei) sorgt für sofortiges Wegschreiben des internen Dateipuffers.

Auch die Eingabe wird wie die Ausgabe gepuffert. Das heißt, es wird beim Aufruf von getchar() gewartet, bis nach dem Zeichen die Enter-Taste gedrückt wird. Um das zu vermeiden (wenn z. B. nur ein einziger Tastendruck erfolgen soll), muß das System in den ungepufferten Betrieb geschaltet werden. Sie erreichen dies unter UNIX durch Umschalten des Terminals in den ungepufferten (raw-)Modus. Das kann durch den Aufruf des Systemkommandos stty erreicht werden:

system ("stty raw");

Will man zusätzlich verhindern, daß das eingegebene Zeichen vom Betriebssystem geechot wird, kann man den Aufruf erweitern:

system ("stty raw -echo");

Danach wird das eingegebene Zeichen auch nicht mehr automatisch auf dem Bildschirm geechot, sondern erst durch die Ausgabe in Ihrem Programm. Auf diese Weise lassen sich beispielsweise auch Passworteingaben realisieren oder es kann verhindert werden, daß die Eingabe eine Bildschirmmaske verunstaltet. Der Funktionsaufruf fflush(stdout); nach jeder Ausgabe kann dann auch entfallen. Bevor das Programm beendet wird, müssen Sie jedoch wieder in den "Normalmodus" zurückschalten:

system ("stty -raw");
bzw.
system ("stty -raw echo");

Weitere Dateifunktionen für Textdateien

Beispiel: Das folgende Programm macht in Textdateien solche Zeichen sichtbar, die normalerweise nicht nicht sichtbar sind, nälich Steuerzeichen mit den ASCII-Codes 0 bis 31. Diese Zeichen werden Hexadezimal ausgegeben und als Markierung ein '\' davor, so würde das Formfeed-Zeichen als '\0C' ausgegeben. Die Zeichen "Tabulator", "Newline" und das Leerzeichen bleiben unverändert, die Zeilenstruktur der Datei bleibt also erhalten.
Auf der Kommandozeile können beliebig viele Dateinamen angegeben werden. Bleibt die Kommandozeile leer, wird automatisch von der Standardeingabe gelesen.


#include <stdio.h>
#include <stdlib.h>

void vis(FILE *fp);

main(int argc, char *argv[])
  {
  int i;     /* Zaehler fuer Parameter */
  FILE *fp;  /* Eingabedatei */

  if (argc == 1) /* keine Datei angegeben */
    vis(stdin);
  else           /* Dateiliste abarbeiten */
    {
    for (i = 1; i < argc; i++)
    if ((fp=fopen(argv[i], "r")) == NULL) 
      {
      fprintf(stderr, "Can't open %s\n", argv[1]);
      exit(1);
      }
    else 
      {
      vis(fp);
      fclose(fp);
      }
    }
    exit(0);
  }

void vis(FILE *fp)
  {
  int c;
  while ((c = getc(fp)) != EOF)
    if (isascii(c) && 
        (isprint(c) || c=='\n' || c=='\t' || c==' '))
      putchar(c);
    else 
      printf("\\%02x", c);
    exit(0);
}

Als nächstes schreiben wir ein Programm, das den Mittelwert aus den eingelesenen Zahlen bildet:


#include <stdio.h>
#include <stdlib.h>

main(int argc, char *argv[])
  {
  double x;  /* Eingabe-Werte */
  int r;     /* Rueckgabewert von fscanf  */
  int n;     /* Anzahl der Werte */
  double sx; /* in sx werden die eingelesenen Werte aufsummiert */
  double mx; /* Mittelwert der x-Werte */
  int i;     /* Zaehler fuer Parameter */
  FILE *fp;  /* Eingabedatei */

  if (argc == 1) /* keine Datei angegeben */
    {
    fprintf(stderr, "Keine Datei angegeben!\n");
    exit(1);
    }
  else           /* Dateiliste abarbeiten */
    {
    for (i = 1; i < argc; i++)
    if ((fp=fopen(argv[i], "r")) == NULL) 
      {
      fprintf(stderr, "Can't open %s\n", argv[i]);
      exit(1);
      }
    else  /* Datei bearbeiten */ 
      {
      n  = 0; /* Zaehler und Summe auf Null setzen */
      sx = 0;
      for(;;)
        {
        r = fscanf (stdin,"%F",&x); /* Wert einlesen */
        if (r == EOF) break; /* Dateiende -- fertig */
        n = n + 1;
        sx = sx + x;
        }
      mx = sx/n;
      fprintf(stdout,"Datei:  %s\n", argv[i]);
      fprintf(stdout,"   Anzahl der verarbeiteten Werte:  %d\n",n);
      fprintf(stdout,"   Mittelwert: %f\n",mx);
      fclose(fp);
      }
    }
    exit(0);
  }

Aufgabe: Erweitern Sie das Mittelwert-Programm so, daß gleichzeitig das Minimum und das Maximum der eingelesenen Zahlen ermittelt und ausgegeben wird.

Oft braucht man ein Programm, daß verschiedene Dateien nach einer Zeichenfolge durchsucht. In UNIX gibt es dazu das recht mächtige Tool grep und seine "Verwandten" egrep und fgrep. Hier soll eine einfache Variante vorgestellt werden, die mgrep heiße. Das Programm kann diverse Optionen verarbeiten:

Die Optionen werden durch ein '-'-Zeichen eingeleitet, um sie im Programm vom Suchbegriff und Dateinamen unterscheiden zu können, also z. B. "-i" oder "-i -c".
Die Kommandozeile hat folgenden Aufbau:

mgrep <Optionen> <Suchbegriff> <Dateiname(n)>

wobei die Optionen auch fehlen dürfen, es werden dann Voreinstellungen verwendet. Der Teil des Programms, der die Kommandozeile auswertet, ist etwa genau so lang, wie der eigentliche Suchteil.


#include <stdio.h>
#include <ctype.h>
#include <string.h>

#define MAXLINE 1024                 /* max. Zeilenlänge */
#define TRUE  1
#define FALSE 0

char upper   = FALSE;                /* Option i */
char count   = FALSE;                /* Option c */
char fnonly  = FALSE;                /* Option l */
char linen   = FALSE;                /* Option n */
char header  = TRUE;                 /* Option h */
char nomatch = FALSE;                /* Option v */

void usage(void)
{
  printf("Aufruf: mgrep [<Optionen>] <Suchmuster> <Datei(en)> \n\n");
  printf("Optionen: i Groß-/Kleinschreibung nicht beachten\n");
  printf("          l nur Dateinamen ausgeben\n");
  printf("          c Zeilen zählen\n");
  printf("          h kein Dateinamen/Zeilennummer ausgeben\n");
  printf("          n Zeilennummer ausgeben\n");
  printf("          v alle Zeilen ausgeben, die nicht übereinstimmen\n\n");
  exit(1);
}

void delete(char *str,int pos, int n)
  /* n Zeichen aus str ab pos löschen */
  {
  int i = pos + n;
  while ((str[pos++] = str[i++]) != '\0');
  }

void del_spaces(char *line)
  /* Führende Leerzeichen (Space und Tab) entfernen */
  {
  while (line[0]==' ' || line[0]==0x09) delete(line, 0, 1);
  }

FILE *open_file(char *name)
  /* Datei öffnen */
  {
  FILE *fp;
  if ((fp=fopen(name,"r")) == 0)
    {
    fprintf(stderr,"Datei %s kann nicht geöffnet werden",name);
    exit(1);
    }
  return fp;
  }

char strstrc(char *str1, char *str2)
  /* Feststellen, ob str2 in str1 enthalten ist           */
  /* In Abhängigkeit von upper Groß-/Kleinschreibung      */
  /* beachten bzw. nicht beachten                         */
  {
  if (upper)
    {
    int i, n, len;
    n = strlen(str2); 
    len = strlen(str1) - n;
    for (i = 0; i <= len; i++)
      if (strnicmp(&str1[i], str2, n) == NULL) return TRUE;
    }
  return strstr(str1, str2) != NULL;
  }

int main(int argc, char *argv[])
  {
  char line[MAXLINE], search[MAXLINE], name[120], match;
  int i, lineno, cnt = 0;
  FILE *fp;

  if (argc < 3) usage();       /* falsche Parameteranzahl */

  /* zuerst die Optionen bearbeiten */
  while (argc > 2 && argv[1][0] == '-') 
    {
    printf("Argument: %s\n",argv[1]);
    switch (argv[1][1]) 
        {
        case 'i':
        case 'I': upper = TRUE; break;
        case 'n':
        case 'N': linen = TRUE; break;
        case 'c':
        case 'C': count = TRUE; break;
        case 'l':
        case 'L': fnonly = TRUE; break;
        case 'h':
        case 'H': header = FALSE; break;
        case 'v':
        case 'V': nomatch = TRUE; break;
        default:  usage(); exit(1);
        }
        argc--;
        argv++;
    }

  /* jetzt sollte noch Suchbegriff und Dateiname da sein */
  if (argc < 2)
    {
    usage();
    exit(1);
    }

  /* suchbegriff uebernehmen */
  strcpy(search, argv[1]);
  argc--;
  argv++;
  
  /* Text in allen Dateien suchen */
  for (i = 1; i < argc; i++)
    {
    lineno=0;
    strcpy(name, argv[i]);
    printf("Durchsuchen: %s\n",name);
    fp = open_file(name);
    while (fgets(line, MAXLINE, fp) != NULL)
      {
      if (((match = strstrc(line, search)) == TRUE
            && !nomatch) || (!match && nomatch))
        {
        cnt++;
        if (!count)
          {
          if (header)
            {
            printf("%s ", name);
            if (fnonly)
              {
              putchar('\n');
              break;
              }
            if (linen) printf("%04d", lineno);
            putchar(':');
            }
          del_spaces(line);
          printf("%s", line);
          }
        }
      lineno++;
      }
    fclose(fp);
    }
  if (count) printf("%d Zeilen gefunden",cnt);
  return 0;
  }

Wenn ASCII-Zeichen auf eine Zeichenkette eingelesen werden, muß gegebenenfalls eine Umwandlung in eine Zahl erfolgen. Die folgende Funktion atof() leistet das Gewünschte:

double atof(char s[]) 
  /* Zeichenkette s nach double wandeln */
  {
  double val, power;
  int i, sign;
  for (i = 0; s[i] == ' ' || s[i] == '\n' || s[i] == '\t'; i++)
    ; /* Zwischenraum uebergehen */
  sign = 1;
  if (s[i] == '+' || s[i] == '-') /* Vorzeichen */
    if (s[i++] == '-') sign = -1;
  for (val = 0; s[i] >= '0' && s[i] <= '9'; i++)
    val = 10 * val + (s[i] - (int)'0');
  if s[i] == '.') i++;
  for (power = 1, s[i] >= '0' && s[i] <= '9'; i++) 
    {
    val = 10*val + (s[i] - (int)'0');
    power = power*10;
    }
  return (sign*val/power);
  }

5.4 Binärdateien

Bei binärer Ein- und Ausgabe auf Dateien werden die Daten nicht in "lesbarer" Form abgelegt, sondern die Interndarstellung der Speicherinhalte wird direkt (byteweise) in die Datei übertragen. Binäres Schreiben einer long-Variablen benötigt also stets vier Byte Speicherplatz, wogegen der erforderliche Speicherplatz bei formatierter Ausgabe von der Größe der Zahl bzw. vom Format abhängt.

Die Funktion fwrite schreibt eine angegebene Anzahl von Datenelementen gleicher Größe in eine Datei. übergeben werden muss:

fwrite wird also definiert als:

size_t fwrite(const void *pt, size_t size, size_t n, FILE *f)

Die auszugebenden Daten müssen zusammenhängend im Speicher stehen. Dies ist bei Vektoren stets der Fall, ebenso wie bei Speicherplatz, der durch einen einzelnen malloc()-Aufruf (siehe Zeiger) zur Verfügung gestellt wurde.

Beispielprogramm für binäres Schreiben:

#include <stdio.h>
#include <stdlib.h>

int main(void)
  {
  int Feld[] = {3, 91, 2134, 6, 33, 267, 9123, -5, 22, 0}; 
  FILE *pf; 
  char dateiname[] = "daten.bin"; /* Dateiname */
  pf = fopen(dateiname, "wb");
  if (pf == NULL)
    {
    printf("Fehler beim Oeffnen der Datei\n");
    exit(1);
    }
  fwrite( (void *)Feld, sizeof(int), 10, pf);
  /* Das Feld wird komplett auf einmal geschrieben */
  fclose(pf);
  }

Die Größe der Datei daten.bin ist also 10 * sizeof(int).

Die Funktion fread ist die zu fwrite gehörige analoge Einlesefunktion. Ihr Rückgabewert ist die Anzahl der tatsächlich gelesenen Bytes. Diese Zahl kann kleiner sein, als die Zahl der zu lesenden Bytes, wenn das Dateiende vorzeitig erreicht wurde. SIe ist definiert als:

int fread(void *ptr, size_t size, size_t n, FILE *f)

Das folgende Programm kopiert eine Datei unter Verwendung der beiden Funktionen fread() und fwrite(). Dabei wird ein char-Array als Puffer verwendet. Diese Kopiermethode ist wesentlich schneller als zeichenweises Kopieren. Das Dateiende wird dadurch erkannt, daß fread() den Wert 0 zurückliefert. fwrite() schreibt genau soviele Bytes, wie von fread() gelesen wurden.

#include <stdio.h>
#include <stdlib.h>

#define MAXBUF 32768 /* Puffergroesse */

int main(void)
  {
  char buffer[MAXBUF];            /* Kopierpuffer */
  FILE *rf, *wf;                  /* Lesedatei, Schreibdatei */
  int numread;                    /* Anzahl gelesener Zeichen */
  char readfrom[] = "daten.bin";  /* Dateinamen */
  char writeto[] = "daten.bak";

  rf = fopen(readfrom, "rb");
  if (rf == NULL)
    {
    printf("Fehler beim Oeffnen der Datei\n");
    exit(1);
    }
  wf = fopen(writeto, "wb");
  if (wf == NULL)
    {
    printf("Fehler beim Oeffnen der Datei\n");
    exit(1);
    }
  do
    {
    numread = fread(buffer, sizeof(char), MAXBUF, rf);
    fwrite( buffer, sizeof(char), numread, wf);
    } while (numread > 0);
  fclose(wf);
  fclose(rf);
  return 0;
  }
Beispiel: Zum Speichern von Daten für das Telefonverzeichnis des Fachbereichs wird die folgende Struktur definiert:
struct Adresse
  {
  char Name[30];
  char Vorname[30];
  int Raumnummer;
  int Telefon;
  };
Gegeben ist eine Binärdatei namens "teldat", die Datensätze vom Struktur-Typ "Adresse" enthält. Gesucht wird eine Funktion tabelle, welche die Binärdatei zum Lesen öffnet, und alle Datensätze nacheinander als formatierte Tabelle ausdruckt:
int tabelle(char dateiname[])
  {
  FILE * fp;
  struct Adresse Data;

  fp = fopen(dateiname,"rb");
  if (fp == NULL) return(1);  /* Fehler */
  printf("+------------------------------------------+------+------+\n");
  printf("| Name                                     | Raum | Tel. |\n");
  printf("+------------------------------------------+------+------+\n");
  while (!feof(fp))
    {
    fread(&Data,sizeof(Data),1,fp);
    printf("| %-10s %-30s | %4d | %4d |\n",
           Data.Vorname, Data.Name, Data.Raumnummer, Data.Telefon);
    }
  printf("+------------------------------------------+------+------+\n");
  fclose(fp);
  return(0);
  }
Beachten Sie, daß im Gegensatz zum Feld oben, nun der Adressoperator '&' bei fread verwendet werden muß.

Die Daten der Datei "teldat" sollen in eine zweite Binärdatei umkopiert werden. Dafür wird die Funktion kopiere verwendet, welche die Binärdatei mit den Adressen (also die Quelldatei) zum Lesen und gleichzeitig eine weitere Binärdatei, die Zieldatei, zum Schreiben öffnet. Die zweite Binärdatei soll Datensätze enthalten, die folgender Struktur genügen:

struct NeueAdresse
  {
  char Name[30];
  int Telefon;
  };
Aufgabe der Funktion ist es, jeweils einen Datensatz aus der Quelldatei zu lesen, die Inhalte in einen Datensatz der Zieldatei zu übertragen und diesen dann in die Zieldatei zu schreiben:
int kopiere(char quelldateiname[], char zieldateiname[])
  {
  FILE * qfp; 
  FILE * zfp;
  struct Adresse Data;
  struct NeueAdresse NData;

  qfp = fopen(quelldateiname,"rb");
  if (qfp == NULL) return(1);
  zfp = fopen(zieldateiname,"wb");
  if (zfp == NULL) return(1);
  while (!feof(qfp))
    {
    fread(&Data,sizeof(Data),1,qfp);
    strcpy(NData.Name,Data.Name);
    NData.Telefon = Data.Telefon;
    fwrite(&NData,sizeof(NData),1,zfp);
    }
  fclose(qfp);
  fclose(zfp);
  return(0);
  }

5.5 Dateien mit wahlfreiem Zugriff

Auf die einzelnen Datensätze einer Datei kann direkt zugegriffen werden. Dazu stehen folgende Befehle zur Verfügung:

int fseek(FILE *f, long offset, int origin)

Mit dieser Funktion kann man einen Zeiger auf eine bestimmte Position innerhalb einer Datei setzen
Die Funktion positioniert um einen offset, der in Bytes gezählt wird. Der Wert origin legt fest, worauf sich offset bezieht:

Der Rückgabewert ist 0, wenn die Positionierung erfolgreich war, sonst -1.

Die Funktion long ftell(FILE *f) gibt die aktuelle Position in der Datei an, auf die der Dateizeiger weist. Im Fehlerfall liefert ftell den Wert -1.

Die Funktion void rewind(FILE *f) positioniert auf den Dateianfang und löscht den Fehlerstatus.

Beispiel: Programm mit wahlfreiem Zugriff auf eine Datei


#include <stdio.h>

int main(void)
  { 
  long pos;
  int count;
  FILE *fp;
  int mode = 0;
  char c;

  fp = fopen("daten.bin","w+");
  if (fp == NULL)
    {
    printf("Fehler beim Oeffnen der Datei\n");
    exit(1);
    }

  /* Datei beschreiben */
  fputs("abcdefghijklmnopqrstuvwxyz",fp);
  puts("abcdefghijklmnopqrstuvwxyz");
  printf("\n");

  /* Wahlfreier Zugriff auf Datei */
  printf("Eingabe der Position im File (0 bis 25):\n");
  scanf("%ld",&pos);
  fseek(fp,pos,mode);
  pos = ftell(fp);
  printf("Dateiposition ist %ld\n",pos);
  fread(&c,1,1,fp);
  printf("\nBuchstabe an dieser Position: %c\n\n",c);
  fclose(fp);
  return 0;
  }

Beispiel: Hexdump
Der Inhalt einer Datei soll Hexadezimal und in ASCII ausgegeben werden. Für die Bildschirmausgabe soll das Programm die Datei blockweise anzeigen, jeweils 16 Zeilen. Der folgende Bildschirmabzug zeigt die Ausgabe:


Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F    0123456789ABCDEF

00000000 :  2F 2A 20 48 65 78 64 75 6D 70 20 65 69 6E 65 72    /* Hexdump einer
00000010 :  20 44 61 74 65 69 20 2A 2F 0D 0A 0D 0A 23 69 6E     Datei */....#in
00000020 :  63 6C 75 64 65 20 3C 73 74 64 69 6F 2E 68 3E 0D    clude <stdio.h>.
00000030 :  0A 23 69 6E 63 6C 75 64 65 20 3C 73 74 64 6C 69    .#include <stdli
00000040 :  62 2E 68 3E 0D 0A 0D 0A 23 64 65 66 69 6E 65 20    b.h>....#define
00000050 :  52 45 54 20 31 33 20 20 20 20 20 20 20 20 20 20    RET 13
00000060 :  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 2F                   /
00000070 :  2A 20 52 65 74 75 72 6E 2D 54 61 73 74 65 20 2A    * Return-Taste *
00000080 :  2F 0D 0A 23 64 65 66 69 6E 65 20 42 45 45 50 20    /..#define BEEP
00000090 :  70 75 74 63 68 61 72 28 27 5C 30 37 27 29 20 20    putchar('\07')
000000A0 :  20 20 20 20 20 20 20 20 20 20 2F 2A 20 53 69 67              /* Sig
000000B0 :  6E 61 6C 74 6F 6E 20 61 75 73 67 65 62 65 6E 20    nalton ausgeben
000000C0 :  2A 2F 0D 0A 0D 0A 0D 0A 2F 2A 20 67 6C 6F 62 61    */....../* globa
000000D0 :  6C 65 20 56 61 72 69 61 62 6C 65 20 2A 2F 0D 0A    le Variable */..
000000E0 :  46 49 4C 45 20 2A 65 69 6E 3B 20 20 20 20 20 20    FILE *ein;
000000F0 :  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20

Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F    0123456789ABCDEF

Anmerkungen zum Programm:

Das Programm sieht dann so aus (aus Gründen der übersichtlichkeit wurde auf ungepuffertes Einlesen verzichtet).

#include <stdio.h>
#include <stdlib.h>

#define RET 13                         /* Return-Taste */
#define BEEP putchar('\07')            /* Signalton ausgeben */

/* globale Variable */
FILE *ein;                             /* Eingabedatei */
char name[64];			               /* Dateiname */
int  ende = 0;                         /* Dateiende erreicht? */
long position;                         /* aktuelle Position in 'ein' */
int puffer[16][16];                    /* Puffer fuer eine Seite */


/* Funktions-Deklarationen */
void seitelesen(long position);        /* Puffer aus Datei lesen */
void ausgabe(long position);           /* Puffer darstellen */


/* Hauptprogramm */
int main(int argc, char *argv[])
  {
  int command;                         /* Bediener-Kommando */

  if (argc < 2)                        /* Datei ueber Kommandozeile? */
    {
    printf("Dateiname: ");             /* Dann ueber Tastatur */
    scanf("%s",&name);
    }
  else
    strcpy(name,argv[1]);
  ein = fopen(name,"rb");              /* Eingabe eroeffnen */
  if (ein == NULL)
    {
    printf("Eingabedatei kann nicht eroeffnet werden\n");
    exit(1);
    }

  position = 0;                        /* Dateianfang */
  for(;;)                              /* endlose       */
    {                                  /* Hauptschleife */
    seitelesen(position);
    ausgabe(position);
    command = getchar();               /* Befehlstaste lesen */
    switch (command)
      {
      case '+' : if (feof(ein) == 0)   /* naechster Block */
		   position = position + 256;
		 else BEEP;
		 break;
      case 'b' :                       /* vorhergehender Block */
      case 'B' :
      case '-' : if (position >= 256)
		   position = position - 256;
		 else BEEP;
		 break;
      case 'Q' :                       /* Beenden */
      case 'q' : fclose(ein); exit(0);
		 break;
      default  : BEEP;                 /* alle anderen Tasten */
      } /* end switch */
    } /* end for */
  return 0;
  }


/* Funktionsdefinitionen */
void seitelesen(long position)         /* Puffer aus Datei lesen */
  {
  int i, j;                            /* Feldindexe */
  int zeichen;	                       /* gelesendes Zeichen */

  fseek(ein,position,SEEK_SET);        /* Pos. ab Dateianfang */
  i = j = 0;
  do
    {                                  /* zeichenweise einlesen */
    zeichen = getc(ein);
    puffer[i][j] = zeichen;
    j++;
    if (j == 16) (i++, j = 0);
    }                                  /* bis Puffer voll oder EOF */
  while (i*16+j < 256 && zeichen != EOF);
  while (i*16+j < 256)                 /* Rest-Puffer auffuellen */
    {
    puffer[i][j] = -1;
    j++;
    if (j == 16) (i++,j = 0);
    }
  }

void ausgabe(long position)            /* Puffer darstellen */
  {
  int i, j;                            /* Feldindexe */

  printf("Dump von  *** %s ***  \n",name);
  printf("\n");
  printf("Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F");
  printf("    0123456789ABCDEF\n");
  printf("\n");
  for (i = 0; i < 16; i++)             /* 16 Zeilen */
    {
    printf("%08lX : ",position);
    for (j = 0; j < 16; j++)           /* Inhalt hexadezimal */
      if (puffer[i][j] < 0)
	printf("   ");
      else
	printf(" %02X",puffer[i][j]);
    printf("    ");
    for (j = 0; j < 16; j++)           /* Inhalt als ASCII-Zeichen */
      if (puffer[i][j] > 31 && puffer[i][j] != 127)
	printf("%c",puffer[i][j]);
      else
	if (puffer[i][j] < 0)          /* Datei zuende - Blanks */
	  printf(" ");
	else
	  printf(".");                 /* nicht druckbares Zeichen */
    printf("\n");
    position = position + 16;
    }
  printf("\n");
  printf("Adresse      0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F");
  printf("    0123456789ABCDEF\n");
  printf("\n");
  printf("Weiter mit <+>         Zurueck mit <B>/<->");
  printf("          Abbruch mit <Q>\n");
  fflush(stdout);
  }

5.6 Weitere Dateifunktionen (tabellarisch)

5.7 Verarbeitung von Verzeichnissen

Um ein Verzeichnis auszulesen, wird es zunächst geöffnet (opendir), danach werden die Einträge gelesen (readdir) und schließlich wird es wieder geschlossen (closedir). Analog zum Dateihandle gibt es ein Verzeichnishandle, das ein Zeiger auf den Datentyp DIR ist. Informationen über den Eintrag liefert die von readdir gelieferte Struktur dirent.
#include <sys/types.h>   /* manchmal benoetigt */
#include <dirent.h>      /* Headerfile fuer die Verzeichnis-Funktionen */

DIR *opendir(const char *name); 
struct dirent *readdir(DIR *dir);
int closedir(DIR *dir);

opendir()

Die Funktion opendir() erhält als Parameter den Namen des Verzeichnisses als Zeichenkette. Der Rückgabewert ist ein Zeiger auf das Verzeichnishandle. Ein Fehler wird dadurch angezeigt, daß dieser Zeiger den Wert NULL hat.

readdir()

Die Funktion readdir() liest den nächsten Eintrag im Verzeichnis und erhält als Rückgabewert einen Zeiger auf eine Struktur dirent. Dieser Zeiger verweist auf eine statische Variable, die nur bis zum nächsten readdir() gültig ist (wird jedesmal überschrieben). Unterschiedliche DIR-Handles habe aber unterschiedliche Variablen. Die Variable hat unter UNIX folgende Struktur (bei anderen Betriebssystemen kann auch die Struktur anders sein):
struct dirent 
  {
  long            d_ino;              /* Inode Nummer */
  off_t           d_off;              /* Offset zum naechsten dirent */
  unsigned short  d_reclen;           /* Laenge dieses Eintrags */
  char            d_name[NAME_MAX+1]; /* Dateiname */
  };
Für das Anwenderprogramm ist meist nur der Name des Eintrags interessant. Will man mehr über diesen Eintrag erfahren, beispielsweise, ob es wieder ein Verzeichnis ist, verwendet man andere Systemaufrufe, z. B. stat().

closedir()

Zuletzt wird das Verzeichnis mit closedir() wieder geschlossen. Ein Beispielprogramm für das Auslesen eines Verzeichnisses sieht so aus:
#include <sys/types.h>
#include <dirent.h>

int main(void)
  {
  DIR *dirHandle;
  struct dirent * dirEntry;

  dirHandle = opendir("."); /* oeffne aktuelles Verzeichnis */
  if (dirHandle != NULL) 
    {
    while ((dirEntry = readdir(dirHandle)) != NULL) 
      {
      puts(dirEntry->d_name);
      }
    closedir(dirHandle);
    }
  return(0);
  }
Wenn man mit den Dateinamen noch weiter arbeiten will, kann der jeweilige Name auch in eine eigene Variable übertragen werden. Die Länge des Arrays passen Sie am besten an die Größe an, die das Betriebssystem vorgibt. Sie ist als Konstante in types.h vorgegeben. Eine Funktion zum Lesen eines Verzeichnisses könnte dann folgendermaßen aussehen:
void read_dir(char *dirname)
  {
  DIR *dir;
  struct dirent *dirzeiger;
  char dateiname[MAXNAMELEN+1];      /* MAXNAMELEN definiert in types.h */
  printf("%s\n",dirname);

  dir = opendir(dirname);
  if(dir != NULL)
    {
    while((dirzeiger = readdir(dir)) != NULL)
      {
      strcpy(dateiname,(*dirzeiger).d_name);
      if (dateiname[0] != '.') printf("%s\n",dateiname);
      }
    closedir(dir);
    }

rewinddir()

Diese Funktion setzt den Lesezeiger wieder auf den Anfang des Verzeichnisses. Die Syntax des Befehls lautet:
void rewinddir(DIR *dir);

getcwd()

Die Funktion getcwd() ermittelt das aktuelle Arbeitsverzeichnis. Dazu hat das aufrufende Programm einen Puffer für den Namen zur Verfügung zu stellen, der groß genug ist. Die Größe wird als weiterer Parameter übergeben. Reicht dieser Speicher nicht aus, gibt getcwd() NULL zurück.
#include <unistd.h>

char * getcwd(char *namebuffer, size_t size);
In einigen Systemen ist es zulässig, NULL als Parameter für namebuffer zu übergeben. Dann alloziiert getcwd() selbst den benötigten Speicher und gibt den Zeiger darauf zurück. Die Anwendung muß dann durch einen Aufruf von free() den Speicher wieder freigeben.

chdir()

Mit der Funktion chdir() wird das aktuelle Arbeitsverzeichnis gewechselt.
#include <unistd.h>

int chdir(char *Pfad);
Bei Erfolg gibt die Funktion 0, sonst -1 zurück. Die Fehlernummer findet sich in der Variablen errno.

Anlegen und Löschen von Verzeichnissen: mkdir(), rmdir()

Die Funktionen zum Anlegen und Löschen der Verzeichnisse heißen wie die entsprechenden UNIX-Befehle. Beim Anlegen werden Zugriffsrechte übergeben. Wie das UNIX-Kommando rmdir kann auch die Funktion nur leere Verzeichnisse löschen.
#include <fcntl.h>
#include <unistd.h>

int mkdir(char *Pfadname, mode_t mode);
int rmdir(char *Pfadname);
Bei Erfolg geben die Funktionen 0, ansonsten -1 zurück. Die Fehlernummer findet sich in der Variablen errno.

5.8 Pipes und Named Pipes

Für ein Betriebssystem ist es wichtig, daß mehrere Prozesse miteinander kommunizieren können. Im Laufe der Zeit haben sich zahlreiche Verfahren dafür entwickelt, wobei die Pipe sich sehr einfach anwenden läßt. Gerade bei Unix oder Linux spielen Pipes eine wichtige Rolle. Nehmen wir an, Sie wollen ein Programm zur Anzeige der gerade laufenden Prozesse schreiben oder die Ausgabe eines beliebigen anderen Kommandos in Ihrem C-Programm weiter verarbeiten. Dazu wird einfach eine Pipe zwischen dem externen und dem eigenen Programm eingerichtet, wobei das externe Programm in die Pipe schreiben und das eigene Programm die Daten lesen soll.

Dazu wird zuerst eine Pipe und ein Kindprozess erzeugt. Der Kindprozess schließt die nicht verwendete Seite der Pipe (in diesem Fall die Leseseite). Nun überlagert sich der Kindprozess mittels exec() mit einer Shell, die ihrerseits das externe Programm startet. Das alles muss man aber nicht selbst erledigen, sondern die Arbeit wird von der Bibliotheksfunktion popen() übernommen.

popen()

Öffnen einer Pipe
#include <stdio.h>

FILE* popen(const char* commandline, const char* mode);
Der erste Parameter commandline ist eine Zeichenkette mit dem gewünschten Kommandoaufruf. Der zweite Parameter mode ist entweder "r" für das Lesen aus der Pipe oder "w" für das Schreiben in die Pipe. Bei erfolreicher Ausführung erhält man einen Dateizeiger zurück, bei einem Fehler wird NULL geliefert. Nach Beendigung kann man die Pipe wieder schließen:

pclose()

Schliessen einer Pipe.
#include <stdio.h>

int pclose(FILE* filep);
Das folgende Beispiel zeigt das Abrufen der Uhrzeit:
#include <stdio.h>
#include <stdlib.h>

FILE* pipe;         /* Filehandle fuer die Pipe */
char Datum[100];    /* Eingabepuffer */

int main(void)
  {
  /* Pipe zum date-Programm lesend oeffnen */
  pipe = popen("date +%H-%M-%S", "r");

  /* genau 10 Zeichen (hh-mm-ss\n\0) lesen, 
     Stringterminator '\0' wird von fgets angehaengt  */
  fgets(Datum,10,pipe);

  /* Pipe wieder schliessen */
  pclose(pipe);

  /* irgendwas machen ... */
  printf ("%s\n",Datum);
  return 0;
  }
Solche Pipes erlauben das "Anzapfen" beliebiger Programme um entweder deren Ausgabe im eigenen Programm weiterzuverarbeiten oder um den Input eines externen Programms zu erzeugen. So kann man beispielsweise mit relativ wenig Aufwand aus den eigenen Daten eine Steuerdatei für das Tool GnuPlot erzeugen und so aus dem eigenen Programm heraus beliebige Zwei- oder Dreidimensionale Grafiken erzeugen.

Die bisher besprochenen Pipes fordern eine "Verwandtschaft" zwischen den jeweiligen Prozessen. Will man eine Verbindung zwischen beliebigen Prozessen herstellen, kann man sich auf "Named Pipes" (FIFOs) stützen. Mit dieser Mehode läßt sich auch einfach eine Client/Server-Anwendung realisieren. Named Pipes kann man wie Dateien mit dem Kommando mknod in einem beliebigen Verzeichnis anlegen. Über die FIFO läuft dann die Kommunikation zwischen den Prozessen (FIFO bezeichent übrigens das Arbeitsprinzip: First In, First Out).

Natürlich kann eine FIFO auch gleich vom C-Programm erzeugt werden. Dazu existiert eine Bibliotheksfunktion:

mkfifo()

Einrichten einer Named Pipe
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char* pathname, mode_t mode)

Der erste Parameter gibt Pfad und Namen an, also die Position im Dateisystem, wo die Named Pipe angelegt werden soll. Der zweite Parameter gibt den Erzeugungsmodus an. Hier gibt es sehr viele Möglichkeiten, die im der Manualpage erklärt werden. Das folgende Beispiel legt eine FIFO an, um danach in diese Pipe etwas zu schreiben. Dach werden die Daten wieder aus der Pipe gelesen.

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) 
  {
  int pipe;                             /* File-Handle der Pipe */
  char puffer[500];                     /* Lese-Puffer */
  char test[] = "Dies ist ein Test\n";  /* Ausgabestring */

 /* Pipe erzeugen Mode = Schreiben/Lesen */
 mkfifo("fifi", O_RDWR);

 /*  Pipe als Datei oeffnen */
 pipe = open("fifo01", O_RDWR);

 /* irgend etwas in die Pipe schreiben */
 write (pipe, test, strlen(test));

 /* nun versuchen wir, das wieder zu lesen */
 if (read(pipe, &puffer, sizeof(puffer)) < 0) 
   { printf("Pipe leer !\n"); }
 else 
   { printf("%s\n",puffer); }

 /* Pipe schliessen */
 close(pipe);
 /* Die Pipe ist weiter vorhanden! */
 }
Schreibt ein Prozess in eine Pipe, die noch nicht von einem anderen Prozess zum Lesen geöffnet wurde, wird das Signal SIGPIPE generiert.

Das folgende Beispiel zeigt, wie eine Client-Server-Anwendung aussehen könnte. Client und Server werden einfach aus dem obigen Beispiel "herausgezogen". Der Server legt eine Pipe mit Lese/Schreibzugriff an. Danach wartet er auf Daten, die in diese Pipe geschrieben werden, um sie auf der Standardausgabe auszugeben. Der Client darf erst nach dem Server gestartet werden, da die Pipe dann schon existieren muss. Er schreibt in die Pipe und beendet sich dann.

/*** CLIENT ***/
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) 
  {
  int pipe;                             /* File-Handle der Pipe */
  char test[] = "Dies ist ein Test\n";  /* Ausgabestring */

  /*  Pipe als Datei oeffnen */
 pipe = open("fifo01", O_RDWR);

 /* irgend etwas in die Pipe schreiben */
 write (pipe, test, strlen(test));

 /* Pipe schliessen */
 close(pipe);
 }


/*** SERVER ***/
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void) 
  {
  int pipe;                             /* File-Handle der Pipe */
  char puffer[500];                     /* Lese-Puffer */

 /* Pipe erzeugen Mode = Schreiben/Lesen */
 mkfifo("fifi", O_RDWR);

 /*  Pipe als Datei oeffnen */
 pipe = open("fifo01", O_RDWR);

 /* aus der Pipe lesen */
 read(pipe, &puffer, sizeof(puffer));

 /* Pipe schliessen */
 close(pipe);
 }

5.9 Low-Level-I/O

Die folgenden Funktionen sind nicht Teil des Posix-Standards. Sie sind trotzdem fast überall implementiert, da sie für den Low-Level-Zugriff auf Dateien und Geräte (z. B. die serielle Schnittstelle) notwendig sind. Hier begeben Sie sich ins Reich der Systemprogrammierung. Die fünf elementaren Low-Level-Funktionen sind: Das Dateihandle (Dateideskriptor) ist hier nicht wie weiter oben der Pointer auf eine Struktur, sondern eine schlichte ganze Zahl. Die Standardhandles sind 0 (Standardeingabe), 1 (Standardausgabe) und 2 (Standardfehlerausgabe). Als Headerdateien werden benötigt:
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

Open und Close

Öffnen einer Datei/eines Gerätes
#include <fcntl.h>

int open(const char *pfad, int modus);
int open(const char *pfadname, int flags, mode_t zugriffsrechte);

int  close(int handle);
Der Funktion wird der Name und der gewünschte Zugriffsmodus übergeben, der optionale dritte Parameter erlaubt das Setzen von Zugriffsrechten. Die Funktion liefert einen Dateideskriptor zurück, der in den weiteren Funktionsaufrufen als Parameter die Datei identifiziert. Die möglichen Zugriffsmodi sind:
  O_RDONLY   - nur lesen
  O_WRONLY   - nur schreiben
  O_RDWR     - lesen und schreiben
  O_APPEND   - anhaengen
  O_CREAT    - anlegen, falls noch nicht vorhanden
  O_EXCL     - in Kombination mit O_CREAT verhindern des Überschreibens
  O_NOCTTY   - kein Kontrollterminal des Prozesses
  O_NONBLOCK - bei Pipe/Gerät sind die I/O-Operationen nicht blockierend (Socket)
  O_SYNC     - Schreibvorgang wird direkt ausgeführt
  O_BINARY   - Binaerdatei (DOS/Windows)
  O_TEXT     - Textdatei (DOS/Windows)
Gegebenenfalls kann man die Werte "verodern" (z. B. O_WRONLY|O_CREAT).

Die Zugriffsrechte lauten:

  S_IWRITE         - Schreibrecht
  S_IREAD          - Leserecht
  S_IWRITE|S_IREAD - Schreib- und Leserecht
Die Funktion liefert die Handlenummer zurück oder bei Fehler -1.

Die Datei wird mit close(handle) wieder geschlossen. Die Funktion liefert 0 zurück oder bei Fehler -1. Beispiel:

#include <unistd.h>
#include <fcntl.h>

int main(void) 
  {
  int fd;

  fd = open("Testdatei",O_RDONLY);  /* Datei muß existieren */
  if (fd == -1) 
    perror("Datei existiert nicht");
  else 
    close(fd); 
  return 0;
  }
Der Aufruf von open erzeugt keine Datei, falls sie nicht existiert. Das muss der Zugriffsparameter regeln, z. B.:
#include <unistd.h>
#include <fcntl.h>

int main(void) 
  { 
  int fd;

  fd = open("Testdatei",O_WRONLY|O_EXCL|O_CREAT,0644);
  close(fd);
  return 0;
  }
Sofern Sie mit der oktalen Schreibweise der Zugriffsrechte unter UNIX/Linux vertraut sind, können Sie diese wie oben verwenden. Die führende Null nicht vergessen, sonst ist es keine Oktalzahl.

Read und Write

Lesen und Schreiben, sowohl auf geblockte als auch ungeblockte Dateien/Geräte.
#include <unistd.h>
#include <fcntl.h>

int write(int fh, const void *puffer, size_t bytezahl);
int read(int fh, const void *puffer, size_t bytezahl);
Transfermenge sind immer Bytes. Wichtig ist, dass die Transfermenge (bytezahl) nie mehr als die Puffergröße betragen darf. Das folgende Beispielprogramm kopiert eine Datei:
#include <unistd.h>
#include <fcntl.h>

int main(void)
  {
  int in, out;           /* Filehandles  */
  char buffer[1024];     /* Datei-Puffer */
  int count;

  in = open("Quelle",O_RDONLY);   /* muss vorhanden und lesbar sein */
  out = open("Ziel",O_WRONLY|O_TRUNC|O_CREAT,0644); 

  while((count = read(in,buffer,1024)) > 0)
    write(out,buffer,count);

  close(in);
  close(out);
  return 0;
  }
read() versucht, die angegebene Anzahl Bytes in den Puffer einzulesen, wobei die tatsächliche Anzahl vom Aufruf zurückgeliefert wird. Deshalb sollten stets nur count Bytes weggeschrieben werden. Der letzte Block kann auch weniger als 1024 Bytes enthalten, deshalb kann man nicht einfach immer 1024 wegschreiben.

Lseek

lseek() leistet das Gleiche wie fseek() und dient zum Verschieben des File-Deskriptors innerhalb der geöffneten Datei.
#include <unistd.h>
#include <fcntl.h>

long lseek(int fh, long offset, int mode);
Die Datei wird wieder durch den File-Deskriptor fh identifiziert, der natürlich zuvor geöffnet bzw. erzeugt wurde. Um wieviele Bytes der File-Deskriptor von der Position wie verschoben werden soll, wird mit offset angegeben. Die Angaben von mode sind dieselben wie bei fseek():
SEEK_SET oder 0 - vom Dateianfang aus um offset Bytes verschieben
SEEK_CUR oder 1 - von der aktuellen Position um offset Bytes verschieben
SEEK_END oder 2 - vom Dateiende um offset Bytes verschieben
Die Funktion gibt den Wert der aktuellen Position des File-Deskriptors zurück. Bei einem Fehler gibt diese Funktion –1 zurück. Es sollte keinesfalls auf " < 0" geprüft werden, sondern auf –1, da es möglicherweise Gerätedateien gibt, die einen negativen Wert zurückliefern. Beispiele:
long akt_pos;
akt_pos = lseek(fh, 0L, SEEK_CUR);
Deskriptor auf den Dateianfang setzen:
lseek(fh, 0L, SEEK_SET);
Deskriptor um 222 Bytes nach vorn versetzen:
lseek(fh, 222L, SEEK_CUR);
Deskriptor um 100 Bytes zurücksetzen:
lseek(fh, -100L, SEEK_CUR);
Oft wird von einem Gerät mittels read() gelesen (serielle Schnittstelle, USB-Schnittstelle, I/O-Port bei Embedded Systemen etc.), aber man will die Daten zeilenweise weiterverarbeiten. Der Puffer enthält natürlich irgendwo Newline-Zeichen, man muss sie nur Suchen. Aus diesem Grund soll hier das Splitten des Puffers in einzelne Zeilen etwas näher betrachtet werden. Normalerweise wäre es ein Zufall, wenn ein Zeilenende genau mit dem Pufferende zusammenfällt, und man den String per Standardfunktion splitten kann. Die Regel ist, dass ein Puffer den Anfang und der nächgste Puffer das Ende einer Zeile enthält. Darum soll jetzt die Arbeit mit mehreren aufeinanderfolgenden Datenblöcken näher betrachtet werden:

Die erste Möglichkeit, die sich anbietet, ist das zeichenweise Lesen von der Netzwerk-Schnittstelle (das Betriebssystem puffert ja die Pakete für uns). Sobald dann ein Newline ('\n') auftritt, wird die Funktion verlassen und gibt den String zurück. Es versteht sich von selbst, dass vom aufrufenden Programm ein Zeichen-Array zur Verfügung gestell werden muss und dass man dessen Länge nicht überschreitet. Die folgende Funktion get_line() liest von einer vorhergeöffneten Datei genau eine Zeile ein (der Unterstrich in get_line() ist notwendig, weil getline() eine Bibliotheksfunktion ist). Als Parameter werden neben dem Datei-Descriptor das Array und dessen maximale Länge übergeben:

int get_line(int fd, char *buffer, unsigned int len)
  {
  /* read a '\n' terminated line from fd into buffer
   * of size len. The line in the buffer is terminated
   * with '\0'. It returns -1 in case of error and -2 if the
   * capacity of the buffer is exceeded.
   * It returns 0 if EOF is encountered before reading '\n'.
  */
  int numbytes = 0;
  int ret;
  char buf;

  buf = '\0';
  while ((numbytes <= len) && (buf != '\n'))
    {
    ret = read(fd, &buf, 1);   /* read a single byte */
    if (ret == 0) break;       /* nothing more to read */
    if (ret < 0) return -1; /* error or disconnect */
    buffer[numbytes] = buf;    /* store byte */
    numbytes++;
    }
 if (buf != '\n') return -2;   /* numbytes > len */
 buffer[numbytes-1] = '\0';    /* overwrite '\n' */
 return numbytes;
 }
Nachteil dieser Lösung ist die Geschwindigkeit bzw. deren Fehlen. Durch die vielen read()-Aufrufe ist die Funktion ziemlich langsam. Besser wäre eine Lösung, bei der ein Datenpaket komplett eingelesen und dann Zeile für Zeile ans aufrufende Programm weitergereicht wird. Genau das macht die folgende Funktion, bei der die Parameter die gleiche Aufgabe haben wie oben. Diese Funktion hat einen internen Puffer, der mittels read() gefüllt wird und dessen Inhalt Stück für Stück bei jedem Aufruf weitergegeben wird. Dazu verwendet die Funktion die statischen Variablen bufptr, count und mybuf, deren Werte erhalten bleiben und bei jedem Aufruf wieder zur Verfügung stehen. Werden mit read() mehrere Zeilen gelesen, bleibt der jeweilige Rest in mybuf erhalten und wird beim nächsten Aufruf der Funktion verarbeitet:
int readline(int fd, char *buffer, unsigned int len)
  {
  /* read a '\n' terminated line from fd into buffer
   * bufptr of size len. The line in the buffer is terminated
   * with '\0'. It returns -1 in case of error or -2 if the
   * capacity of the buffer is exceeded.
   * It returns 0 if EOF is encountered before reading '\n'.
   * Notice also that this routine reads up to '\n' and overwrites
   * it with '\0'. Thus if the line is really terminated with
   * "\r\n", the '\r' will remain unchanged.
  */
  static char *bufptr;
  static int count = 0;
  static char mybuf[1500];
  char *bufx = buffer;
  char c;

  while (--len > 0)            /* repeat until end of line  */
    {                             /* or end of external buffer */
    count--;
    if (count <= 0)            /* internal buffer empty --> read data */
      {
      count = read(fd, mybuf, sizeof(mybuf));
      if (count < 0) return -1;/* error or disconnect */
      if (count == 0) return 0;   /* nothing to read - so reset */
      bufptr = mybuf;             /* internal buffer pointer    */
      }
    c = *bufptr++;                /* get c from internal buffer  */
    if (c == '\n')
      {
      *buffer = '\0';             /* terminate string and exit  */
      return buffer - bufx;
      }
    else
      {
      *buffer++ = c;              /* put c into  external buffer */
      }
    }
  return -2;                      /* external buffer to short */
  }
Beim Senden von Daten sollte man eigentlich nicht zwischenpuffern, sondern jede Zeile sofort auf die Reise schicken - schließlich wartet der Empfänger darauf.

Zum Inhaltsverzeichnis Zum nächsten Abschnitt


Copyright © FH München, FB 04, Prof. Jürgen Plate