Thiemo Mättig

Programmierung einer einfachen Shell

Die Aufgabe zum 10.05.2000 lautete, eine kleine Shell für Unix zu programmieren, die höchstens eine eingegebene Pipe verstehen und korrekt verarbeiten kann. Als Grundlage dafür stand der in der Vorlesung gegebene Beispielcode zur Verfügung. Dieser kann bereits jedes beliebige Kommando in seine Bestandteile zerlegen (Funktion "parse") und ausführen (Funktion "execute"). Damit die Shell jedoch auch eine Pipe ('|') erkennt, sind viele Anpassungen nötig. Diese im Vergleich zum Ausgangscode veränderten Programmteile sind im Folgenden fett gedruckt. Erklärende Kommentare sind kursiv gehalten.

#include <stdio.h>
main()
{ char buf[1024];
  /* Zwei Zeigerfelder zur Aufnahme von zwei Kommandos */
  char *args[64], *args2[64];
  for (;;)
  { printf("Command: ");
    if(gets(buf) == NULL) {printf("\n"); exit(0);}
    /* Die Befehlszeile wird in ihre Bestandteile zerlegt */
    parse(buf, args, args2);
    /* ... und schliesslich ausgeführt */
    execute(args, args2);
  }
}

parse(char *buf, char **args, char **args2)
{ /* Hilfsvariable zur Erkennung eines zweiten Kommandos */
  int pipefound=0;
  while(*buf != NULL)
  { /* Auch der Strich '|' wird mit NULL überschrieben */
    while((*buf == ' ') || (*buf == '\t') || (*buf == '|'))
      *buf++ = NULL;
    /* Nach einer Pipe wird statt "args" "args2" benutzt */
    if(pipefound==0) *args++ = buf; else *args2++ = buf;
    /* Auch der Strich '|' zählt hier als Trennzeichen */
    while((*buf != NULL) && (*buf != ' ') &&
      (*buf != '\t') && (*buf != '|')) buf++;
    /* Erkennung des Striches und Setzen der Hilfvariablen */
    if(*buf == '|') pipefound=1;
  }
  /* Beide Argument-Listen werden mit NULL terminiert */
  *args = NULL; *args2 = NULL;
}

execute(char **args, char **args2)
{ int pid, pid2, status;
  /* Zuerst die Pipe mit ihren Filedescriptoren erzeugen */
  int filedescr[2]; pipe(filedescr);
  /* Der erste Kindprozess wird gestartet */
  if((pid = fork()) < 0) {perror("fork"); exit(1);}
  /* Der erste Kindprozess verarbeitet nun diesen Teil */
  if(pid == 0)
  { /* Folgender Teil nur, wenn eine Pipe eingegeben wurde */
    if(*args2!=NULL)
    { /* Standardausgabe wird geschlossen und ersetzt */
      close(1); dup(filedescr[1]);
      /* Die Descriptoren der Pipe sind nicht mehr nötig */
      close(filedescr[0]); close(filedescr[1]);
    }
    /* Das eigentliche Starten des ersten Kommandos */
    execvp(*args, args); perror(*args); exit(1);
  }
  /* Eventuell einen zweiten Kindprozess starten */
  if(*args2!=NULL)
  { if((pid2 = fork()) < 0) {perror("fork"); exit(1);}
    /* Dieser Kindprozess verarbeitet nun diesen Teil */
    if(pid2 == 0)
    { /* Standardeingabe wird geschlossen und ersetzt  */
      close(0); dup(filedescr[0]);
      /* Die Descriptoren der Pipe sind nicht mehr nötig */
      close(filedescr[0]); close(filedescr[1]);
      /* Das eigentliche Starten des zweiten Kommandos */
      execvp(*args2, args2); perror(*args2); exit(1);
    }
  }
  /* Der Elternprozess wartet auf die Beendigung */
  while(wait(&status)!=pid) /* empty */;
}

Nachdem der Befehl "gets(buf)" eine Eingabe entgegen genommen und im Feld "buf" gespeichert hat, wird die Funktion "parse" aufgerufen. Diese zerlegt das Kommando in seine Bestandteile, wobei es Leerzeichen und Tabulatoren durch NULL ersetzt. Jeder Beginn eines Kommandoteiles wird als Zeiger im Feld "args" gesichert. Erst nach dem Erkennen eines senkrechten Striches wird an Stelle von "args" das Feld "args2" verwendet.

Die Funktion "execute" führt diese Kommandos schliesslich aus. Dabei wird mit Hilfe des Befehles "if(*args2!=NULL)" streng geprüft, ob ein einfaches oder zwei durch eine Pipe verbundene Kommandos eingegeben wurden. Im ersten Fall wird lediglich ein einzelner Kindprozess erzeugt, der das Kommando ausführt. Wurde jedoch eine Pipe verwendet, so erzeugt "execute" zwei Kindprozesse. Beide werden mittels einer Pipe auf Systemebene verbunden, so dass die Ausgabedaten des ersten zur Standardeingabe des zweiten Prozesses geleitet werden.

Der Elternprozess überspringt während dessen die mit "if(pid==0)" und "if(pid2==0)" geklammerten Programmteile. Er erreicht die letzte Zeile "while(wait..." und wartet dort auf die Beendigung des ersten (!) Kindprozesses. Leider wartet er nicht auf den zweiten Kindprozess, so dass sich die Bildschirmausgaben unter Umständen überlagern. Dies ist ein sehr problematisches Verhalten, das ich trotz intensiver Analyse und duzender Lösungsversuche noch nicht verbessern konnte.

Es ist auch zu beachten, dass diese Shell auf jeglichen Komfort verzichtet. Sie erkennt beispielsweise keine Ein- und Ausgabeumleitungen ('<' und '>'). Auch Editierfunktionen und eine History - in der Praxis nahezu unverzichtbare Hilfen - fehlen hier.