Artikel

Entwicklung eines Kommandointerpreters

command.com
im Eigenbau

Oliver Müller • Befehlseingaben in der Kommandozeile gelten im Zeitalter "bunter Bildchen" als verpönt und veraltet. Aber sie haben auch unbestrittene Vorteile. Grund genug zu zeigen, was hinter einem Kommandozeilen-Interpreter steckt und wie man ihn programmiert.

Ein Kommandozeilen-Interpreter besteht im wesentlichen aus vier Teilen:

  • einem Initialisierungsteil
  • dem Editor
  • dem Parser
  • dem Ausführungsteil

Neben gewöhnlichen Initialisierungsarbeiten, die jedes Programm erledigen muß, erzeugt der Initialisierungsteil einige essentielle Datenstrukturen und setzt Variablen auf Ausgangswerte. Außerdem analysiert er die übergebenen Parameter und baut das Environment-Segment auf. Wird nämlich ein gewöhnliches Programm gestartet, bekommt es entweder vom Betriebssystem eine Kopie des Eltern-Environments oder einen explizit vom Elternprozeß angegebenen Umgebungsspeicher zugewiesen. Bei einem Kommandoprozessor verhält es sich etwas anders: Wird dieser nämlich als "Basis-Shell" aus der CONFIG.SYS heraus gestartet, übergibt ihm der DOS-Kernel keinen gültigen Umgebungsspeicher. Diesen muß der Befehlsinterpreter erst selber generieren.

Aufbau des Environments

In der hier entwickelten Beispiel-Shell CMD baut die Funktion init() das Environment auf. CMD erkennt In der Kommandozeilenanalyse anhand des Parameters /P, ob es sich um den Basisprozeß handelt. Wenn dem so ist, generiert es eine leere Umgebung von der Größe envSize und weist diese environ zu. Der erste Zeiger in das Environment (eptr[0]) wird dabei zum Null-Pointer und markiert so das Ende der Variablenliste. Läuft CMD aber als Kindprozeß (durch Aufruf in der Kommandozeile), so passiert zweierlei: Erstens wird der allokierte Speicher mit den Strings der Eltern-Task gefüllt und zweitens jeweils ein Zeiger des Arrays eptr auf die Adresse des kopierten Strings innerhalb von environ gesetzt. Der letzte Pointer (eptr[i]) wird hier auf Null gesetzt und fungiert ebenfalls als Endmarke.
Nachdem die Initialisierung mit dem Durchlaufen von init() abgeschlossen ist, tritt das System in die Hauptschleife ein. Dort ruft es die jeweiligen Routinen der Module Editor und Parser ständig nacheinander auf.

Verwaltung des Editors

Der Editor von CMD ist als Einzeileneditor konzipiert. Er ist in der Lage Escape, Backspace und Cursorpositionierung mittels der Pfeiltasten rechts und links korrekt zu interpretieren (allerdings gibt es lediglich einen Overwrite-Modus). In edit() existieren vier Variablen, die für die Verwaltung der eingegebenen Zeichen notwendig sind. Dies sind

  • len (die Länge der Eingabe)
  • cur (die aktuelle Cursorposition relativ zum Eingabe-String)
  • sowie x0 und y0, die die Startkoordinaten bezogen auf einen 80x25-Zeichen-Display bezeichnen.

Bei Eingabe eines normalen Zeichens legt die Routine dieses Zeichen Im Pufferspeicher an der Position cur ab und erhöht diese Positionsvariable anschließend um eins. Entspricht dagegen die betätigte Taste der linken oder rechten Cursortaste, so wird cur einfach dekrementiert beziehungsweise inkrementiert. Die anschließende Neupositionierung der Schreibmarke erfolgt mit Hilfe des Makros curpos(): Es errechnet aufgrund von x0, y0 und cur die Bildschirmkoordinaten des Cursors und setzt ihn mittels gotoxy(). Die nächste Eingabe überschreibt anschließend das Zeichen unter dem Cursor (cmd[cur]).
Um einen Einfüge-Modus zu implementieren, müßten bei jeder Eingabe die Zeichen ab cur um eine Position verschoben und das Zeichen anschließend in cmd[cur] gespeichert werden. Anschließend wären noch cur und len zu erhöhen und die Anzeige zu aktualisieren.

Das Herzstück – der Parser

Die Leistungsfähigkeit einer Kommandosprache hängt nicht zuletzt vom Parser ab. Er ist unter anderem für die formale Funktionalität zuständig und entscheidet beispielsweise, ob es sich bei der Eingabe um ein Schlüsselwort (interner Befehl) oder um ein Executable (externer Befehl) handelt. Ebenfalls zu seinen Aufgaben gehört es, die Kommandozeile in Argumente aufzuspalten und den ausführenden Teil zu starten. Außerdem muß der Parser eventuell vor der Ausführung die Standardkanäle umleiten.
Die Routine parse() besteht aus zwei Abschnitten: Teil 1 ist dafür zuständig, die Eingabezeile in die einzelnen Argumente aufzuspalten, während Teil 2 untersucht, ob es sich um einen internen oder externen Befehl handelt und diesen gegebenenfalls ausführt.
Der erste Abschnitt besteht aus einer Schleife, die bis zur Null-Marke des übergebenen Kommando-Strings durchlaufen wird. Parse überspringt zunächst alle Whitespaces (wie Leerzeichen, Tabulatoren) und liest danach solange, bis das folgende Argument beendet ist. Dies ist dann der Fall, wenn entweder ein Whitespace, ein Null-Zeichen oder ein Anführungszeichen vorliegt. Ist das Argument auf diese Weise extrahiert, fordert die Routine Speicher an und weist Ihn einem Element von p zu. Anschließend wird das Argument dorthin kopiert und die Prozedur wiederholt. Dem auf den letzten Argumentenzeiger folgenden Pointer wird der Wert Null als Endmarke zugewiesen.

Ein/Ausgabeumlenkung

Stößt der Parser innerhalb des Aufspaltens der Argumente auf die Zeichen ">" oder "<", ruft er die Subroutine redirect() auf. Dieses Unterprogramm extrahiert die Dateinamen aus der Kommandozeile und öffnet anschließend die zugehörigen Files. Gibt redirect() Null zurück, so ist ein Fehler aufgetreten, andernfalls ist die Argumentenaufspaltung abgeschlossen.
Bevor der Parser in den zweiten Abschnitt eintritt, untersucht er die Dateihandles out und in. Weisen diese nicht den Wert -1 auf wird das Handle des entsprechenden Standardkanals (zum Beispiel Handle von CON: für stdin) mittels der Funktion dup() dupliziert, das heißt, eine Sicherheitskopie erstellt (entspricht dem Aufruf der DOS-Funktion 45hex). Anschließend wird der Standardkanal durch den Aufruf von force() umgeleitet. In diesem Unterprogramm ist die DOS-Routine 46hex gekapselt. Diese Systemfunktion beschreibt der Kasten "Schlüssel zur Ein/Ausgabeumlenkung". Nach einer solchen Umleitung erfolgen alle Ein- respektive Ausgaben in eine Datei – egal ob aus der Shell selbst oder aus einem externen Prozeß.
Nun beginnt der zweite Abschnitt des Parsings. Durch Analyse des Elements p[0] des Argumentenfeldes läßt sich feststellen, ob es sich um ein internes Kommando handelt. In der Beispielimplementierung sind lediglich die Befehle EXIT, CD, SET und TYPE realisiert. Diese stehen jedoch exemplarisch für jeweils eine Gruppe von ähnlichen Befehlen – eine individuelle Erweiterung sollte also kein Problem sein.

Einbau der Befehle

EXIT ist der einfachste Befehl, da er, vergleichbar mit CLS, keine Parameter erwartet. Hier ist eine Analyse von p[0] ausreichend. CD ist dagegen ein Kommando, welches sich nicht immer durch die Argumentenaufspaltung von seinem Parameter trennen läßt. Man denke an Angaben wie CD\DEVELOP\CMD. Hier müssen die ersten drei Zeichen von p[0] ausgewertet werden, wobei die ersten beiden CD ergeben müssen. Ist das dritte Zeichen dann ein Null-Byte, ein Punkt oder ein Backslash bei angehängtem Parameter, kann man je nach Art der Verzeichnisangabe durch chdir(&p[0][2]) oder chdir(p[1]) wechseln.
RD, MD und die "langen" Varianten wie CHDIR, RMDIR und MKDIR lassen sich auf die gleiche Weise implementieren. Hier muß im übrigen nur chdir() durch rmdir() beziehungsweise mkdir() ersetzt werden.
Die Programmierung von SET stellt dagegen höhere Anforderungen. Die Analyse innerhalb des Parser beschränkt sich auf das Argument p[0]. Danach erfolgt der Aufruf von set(), einer spezifizierten Funktion des ausführenden Moduls. Übergeben wird der Kommando-String CMD und die Argumentenanzahl, die mittels der Argumentenaufspaltung ermittelt wurde. Hier ist die Übergabe der gesamten Kommandozeichenkette nötig, da im Feld p die Whitespaces de facto eliminiert wurden. Leerzeichen und Tabulatoren sind jedoch für den SET-Parameter keine Trennzeichen, sondern Bestandteil der Wertzuweisung an die Umgebungsvariable.

Verwaltung der Umgebungsvariablen

Für gewöhnlich verwendet man die Funktion getenv() und setenv() der Standard-C-Bibliothek zur Verwaltung von Environment-Variablen. Dieses Verfahren greift hier aber nicht, da der Umgebungsspeicher explizit während der Initialisierung erzeugt wurde. Infolgedessen ist eine Speziallösung erforderlich:
Schritt 1 ist die Extrahierung des Parameters aus der Kommandozeile. Ob ein Parameter existiert, zeigt sich daran, welchen Wert die übergebene Argumentenanzahl argc besitzt. Ist argc gleich eins, wurde SET ohne Parameter aufgerufen. In diesem Fall sind nur die Strings der Umgebungsspeicher auszugeben.
Da Environment-Strings immer den Aufbau Variablenname=Wert besitzen, wird nach der Ermittlung des Parameter-Substrings die Position des Gleichheitszeichens ermittelt. Um die Variablenbezeichnung vom Wert zu trennen, schreibt man aber anstelle des Gleichheitszeichens ein Null-Byte und setzt auf das folgende Zeichen den Zeiger ptr. Auf diese Weise existieren nun zwei Zeichenketten- eine für den Namen und eine für den Wert der Umgebungsvariablen.

Environment-Variablen

In dem Ausführungsmodul (EXEC.C) finden sich die Routinen del() und add() zum Löschen respektive zum Hinzufügen von Variablen. Dabei erfolgt, unabhängig davon, ob eine Variable gelöscht oder dieser nur ein neuer Wert zugewiesen werden soll, ein Aufruf von del(), denn auch im letzteren Fall löscht exec zuerst den alten Environment-String und fügt anschließend einen Neuen hinzu.
Die Funktion del() erwartet als Parameter den Namen der Umgebungsvariablen, die gelöscht werden soll. Diese Routine durchsucht zuerst das Environment nach dem übergebenen Namen. Findet es ihn nicht, liefert del den Index des das Ende markierenden Nullzeigers zurück. Im anderen Fall werden alle folgenden Environment-Strings um die Länge der zu löschenden Zeichenkette nach oben verschoben und die Zeiger des Vektors eptr entsprechend korrigiert. Als Rückgabe erfolgt ebenfalls der Index des Null-Pointers.
Soll die Variable aber nicht gelöscht werden, erfolgt der Aufruf von add() mit der Übergabe des von del() zurückgelieferten Wertes und des Namens der Variablen. Durch Ersetzen des abschließenden Null-Bytes des übergebenen String-Zeigers durch ein "=", wird die Trennung zwischen Namen und Wert wieder aufgehoben. Reicht der verbliebene Speicher aus, schreibt die Routine nun die Zeichenkette in das Environment und aktualisiert die Zeiger.

Befehle entwickeln

Der Parser verwendet ein Format, das es auch ermöglicht, interne Kommandos wie gesonderte Programme zu entwerfen. Das Array p und die Variable i, welche die Anzahl der Argumente enthält, können durchaus mit den formalen Parametern argv und argc der C-typischen Hauptroutine main() verglichen werden. Die Umsetzung des Befehls TYPE veranschaulicht dies.
Der Parser startet die Routine type() und übergibt i und p. Innerhalb von type() wird nun so vorgegangen, als ob ein neues Programm geschrieben und der Befehlsinterpreter nicht existieren würde. In einer for-Schleife werden alle in der Kommandozeile übergebenen Dateien nacheinander geöffnet und deren Inhalt ausgegeben.
Selbst der Parameter env von main kann "simuliert" werden. Entweder der Zugriff erfolgt direkt auf eptr oder das Environment-Parameterfeld wird ebenfalls an das ausführende Unterprogramm übergeben.
Aufgrund von diesem Mechanismus ist eine einfache Erweiterbarkeit der Shell gegeben. Befehle wie COPY oder DIR sollten so kein größeres Problem mehr darstellen.
Wenn das erste Element des Argumenten-Arrays keinem Schlüsselwort entspricht, tritt die Subroutine call() in Kraft. Hierbei wird ein externer Prozeß gestartet. Als Parameter erwartet call lediglich das Argumentenfeld.

Starten externer Prozesse

In dieser Prozedur wird zunächst der Programmname mittels fnsplit() In seine Bestandteile zerlegt. Anschließend wird getestet, ob die Eingabe mit Erweiterung erfolgte. Ist dies nicht der Fall, werden an den Dateinamen nacheinander die Extensionen .exe, .com und .bat angehängt und im über PATH definierten Pfad gesucht. Wird kein passendes File gefunden erfolgt eine Fehlermeldung.
Die Beispiel-Shell CMD besitzt noch keinen Batch-Modus. Als Konsequenz hieraus folgt, daß call() keine Stapeldateien ausführen kann. Eine Anregung, wie dieses Feature zu realisieren ist, findet sich im Kasten "Auf den Stapel gelegt".
Der Aufruf des Programmes erfolgt mittels der Bibliotheksfunktion spawnvpe(), welche als Übergabe ein Argumentenfeld und ein Environment-Array erwartet. Beides wurde vom Kommandoprozessor im geeigneten Format erzeugt. Nachdem der Befehl durch das ausführende Modul abgearbeitet ist, beginnt der Parser mit den Abschlußarbeiten. Die Ein/Ausgabeumlenkungen werden durch Aufruf von force() mit den duplizierten Handles wieder zurückgesetzt und der Speicher des Argumenten-Array wieder freigegeben.

Vorschläge zur Erweiterung

Eine sinnvolle Erweiterung für CMD wäre beispielsweise ein Control-C-Handler, damit unter anderem bei der Ausgabe von langen Dateien, diese abgebrochen werden könnte. Ein raffiniertes Verfahren könnte der unter Linux üblichen bash zum Ausbau des Editors von CMD abgeschaut werden. Hier ist es möglich, über die Tabulatortaste teilweise eingegebene Dateinamen zu erweitern. Unter Linux muß nicht immer der gesamte Pfad eingegeben werden, exemplarisch sei hier folgendes betrachtet:
Die Eingabe von /usr/bin/find kann in /u<TAB>b<TAB>find verkürzt werden. Sind mehrere passende Möglichkeiten verfügbar, ertönt ein akustisches Signal und alle möglichen Dateinamen werden aufgelistet. Die Kommandozeile kann nun entsprechend erweitert werden. Jeder, der schon einmal mit der bash gearbeitet hat und dann wieder mit COMMAND.COM hantieren mußte, wird den Komfort der Tab-Erweiterung zu schätzen wissen.
Außerdem gehört in diesen "modernen Zeiten" zu jedem Kommandointerpreter eine History-Funktion. In diesem Zusammenhang bieten sich die Cursor-Tasten hoch und runter an, deren Auswertung im Editor analog zu der links- und rechts-Taste erfolgt. Die Codes für hoch und runter sind 72 respektive 80.
Eine Alias-Verwaltung, ähnlich dem DOSKEY-Makros, würde dem Interpreter auch gut zu Gesichte stehen. Hier wäre innerhalb der Kommandozeile vor der Aufspaltung der Argumente durch den Parser die entsprechende Sequenz in der Befehlszeichenkette zu ersetzen. Komfortabel wäre auch die Einführung des Kommandos "noalias", welches die Substitution des folgenden Befehls unterdrückt.

Literatur
  1. [1] Günther Born: MS-DOS 6.2 Programmierhandbuch.
    Unterschleißheim: Microsoft Press Deutschland 1993

Schlüssel zur Ein-/Ausgabeumlenkung

Die DOS-Funktion 46hex bietet die Möglichkeit Ein/Ausgabekanäle umzulenken.


Die Schnittstelle ist wie folgt definiert:

AH = 46hex mov ah,46h ; die Funktionsnummer
BX = neues Handle mov bx,[hand]   ; Handle übergeben
CX = Kanal mov cx,1 ; Umlenkung von stdout
CALL Int21hex int 21h

Falls nach dem Aufruf das Carry-Flag gesetzt sein sollte, ist ein Fehler aufgetreten. Der Fehlercode befindet sich dann im AX-Register.


Die definierten Fehlerwerte sind:

4 : Zu viele offene Dateien, kein Handle mehr frei
6 : Handle ist nicht eröffnet oder ungültig

(Hinweis: BX = CX führt in DOS 3.3 zu einem Systemabsturz.)


Die Standardkanäle von DOS:

Kanal-Nr. Name Standardverknüpfung
0 Input device (stdin) CON
1 Output device (stdout) CON
2 Error device (stderr) CON
3 Auxiliary device AUX
4 Printer device PRN


Checkliste für den Befehlseinbau

Die Argumente einer Befehlszeile liegen im Parser im Array p vor, welches als letztes Element einer Null-Zeiger als Endemarke enthält. Die Anzahl der Argumente ist in der Variablen i gespeichert. Die eingegebene Kommandozeile befindet sich in CMD.
Der Name des eingegebenen Befehls oder Programmnamens befindet sich in p[0]. Es gibt im Prinzip nur zwei Kommandokategorien:

  • Befehle, die komplexe Parameter erwarten, die nicht durch Whitespaces getrennt werden können. In diesem Fall muß CMD übergeben werden und ein spezifischer Auswertungsalgorithmus entwickelt werden (siehe Implementierung von SET).
  • Kommandos, die wie externe Programme implementiert werden können. Hier entsprechen p und i den formalen Parametern argv und argc von main(). Falls das Feld env von main() bei einer externen Implementierung berücksichtigt werden würde, kann diesen durch eptr "simuliert" werden. (Beispiel: TYPE)


Datenaustausch zwischen Prozessoren – Piping

Die hohe Kunst des Pipings beruht auf der Ein/Ausgabeumlenkung. Die Ausgabe eines Prozesses A wird auf die Eingabe einer Task B transferiert. Durch den Umweg über eine temporäre Datei kann dies einfach realisiert werden.
Ein simpler Weg die Beispiel-Shell CMD um das Piping zu erweitern, wäre den Parser in eine Schleife innerhalb einer neuen Subroutine zu legen. Diese Routine könnte preparse() genannt werden. Dem Unterprogramm preparse() wird eine Kommandozeile ähnlich der folgenden übergeben: "type c:\autoexec.bat | more".
Eine solche Kommandozeile kann einfach in einzelne Parts aufgespalten werden. Diese Teile wäre im obigen Beispiel "type c:\autoexec.bat" und "more". Hier kann ein ähnlicher Algorithmus wie bei der Argumentenaufspaltung verwendet werden.
Die einzelnen Kommandos sind immer durch ein "|" getrennt. Sehr einfach kommt man zu den einzelnen Teilen, wenn jeweils das "|" durch ein Null-Byte substituiert wird. Ein Zeiger aus einem Array muß nun lediglich auf das auf "|" folgende Zeichen zeigen.
Nun müssen nur noch die Pointer aus dem Feld nacheinander an die Routine parse() übergeben werden und Ein- und Ausgabe umgelenkt werden.
Das Unterprogramm parse() muß des weiteren einen Fehlerstatus zurückliefern, da im Falle eines Fehlers die Abarbeitung des Piping abgebrochen werden muß.



Auf den Stapel gelegt

Eine Batch-Verarbeitung besteht auf den ersten Blick nur aus der Änderung der Eingabequelle. Im normalen Dialogbetrieb ist für den Eingang der Befehlszeilen der Editor verantwortlich. Während der Stapelverarbeitung muß die Eingabe jedoch von einer Datei aus erfolgen. Hier ist also lediglich ein neues Modul zum Einlesen aus einer Datei erforderlich. Außerdem muß noch ein Flag existieren, das beim Ausführen eines Batch-Files gesetzt wird und die Eingabe auf das Batch-Modul setzt. Beim Erreichen von EOF wird dieses Flag wieder zurückgesetzt und damit die Eingabe wieder auf den Editor transferiert.
Hierauf folgt nun das eigentliche Problem: Die Verwaltung von Labels. Die grundlegende Administration wird mittels einer Tabelle realisiert. Die Datensätze bestehen aus zwei Zellen: Name des Labels und Adresse relativ zum Dateianfang der folgenden Anweisung. Hierzu ein Beispiel:
. . .
:tp
turbo.exe
. . .
Der Datensatz für die Marke "tp" würde als Adresse die Position des "t" von TURBO.EXE enthalten.
Für den Aufbau einer derartigen Tabelle gibt es zwei Strategien. Entweder man verwendet ein 2-Pass-Konzept oder ein 1-Pass-Konzept
Bei der 2-Pass-Version sind, wie der Name schon andeutet, zwei Durchläufe notwendig. Im ersten Pass werden nur die Labels betrachtet und sämtliche anderen Anweisungen ignoriert. Hier werden lediglich die Marken In die Tabelle gefüllt. Im zweiten Durchlauf werden umgekehrt die Labels Ignoriert und Befehle ausgeführt. Dieses Verfahren hat den Vorteil, daß bei jedem GOTO-Statement das übergebene Label mit der Tabelle verglichen werden und so auf Vorhandensein getestet werden kann.
Das 1-Pass-Konzept benötigt nur einen Durchgang. Während der Ausführung werden die angetroffenen Labels in die Tabelle eingetragen. Soll nun ein Sprungkommando ausgeführt werden, das der Tabelle noch nicht existiert, so wird der Rest der Datei nach dem Label durchsucht. Hierbei werden wiederum alle angetroffenen Marken ebenfalls in der Tabelle gespeichert, jedoch nun die Anweisungen bis zur gesuchten Marke ignoriert. Existiert das Label nicht, so kann dies erst bei Erreichen des Dateiendes festgestellt werden. Das wirkt sich bei großen Stapeldateien nicht gerade vorteilhaft auf die Laufzeit aus. Jedoch benötigt dieses Verfahren bei (korrekt programmierten) Batch-Files nur einen Durchlauf.
Beide Varianten haben ihre Vor- und Nachteile, demzufolge muß die Antwort auf die Frage nach dem besseren Verfahren wohl als individuell verschieden betrachtet werden.


Listing 1: cmd.c
/* Projekt: Kommandointerpreter
   Datei  : cmd.c
   Zweck  : Hauptmodul
   Sprache: C
   Autor  : Oliver Müller */

#include <string.h>
#include "error.h"
#include "cmd.h"

short    pFlag=0;
unsigned envSize=1024;
char     *environ, **eptr;

void init(char *a[], char *e[])
{
  unsigned n=0, i, len;
  char *curr;
  for (i=1; a[i]; i++)
    if (a[i][0] == '/')
      switch (a[i][1])
      {
        case 'P' :
        case 'p' : pFlag=1; break;
        default  : error (eUNKNOWN, a[i]);
      }
  environ = emalloc (envSize);
  eptr = (char**) emalloc (envSize * sizeof(char*));
  if (pFlag) eptr[0]=0;
  else
  {
    for (i=0; e[i]; i++)
    {
      len=strlen(e[i]);
      curr=&environ[n];
      if ((n+=len+1) > envSize)
        error(eENV);
      strcpy(curr,e[i]);
      eptr[i]=curr;
    }
    eptr[i]=0;
  }
}

#pragma argsused

int main(int argc, char *argv[], char *env[])
{
  char cmd[MAXCMD];
  init (argv, env);
  for (;;)
  {
    prompt();
    edit(cmd);
    parse(cmd);
  }
}


Listing 2: cmd.h
/* Projekt : Kommandointerpreter
   Datei   : cmd.h
   Sprache : C
   Autor   : Oliver Müller */

#ifndef _CMD_H_OGM
#define _CMD_H_OGM
#define MAXCMD 128

void prompt(void);
void edit(char *cmd);
void parse(char *cmd);
void call(char *args[]);
void set(char *cmd, unsigned argc);
void type(unsigned argc, char *args[]);
char *emalloc(unsigned size);

#endif


Listing 3: edit.c
/* Projekt : Kommandointerpreter
   Datei   : edit.c
   Sprache : C
   Autor   : Oliver Müller */

#include <stdio.h>
#include <dir.h>
#include <conio.h>
#include "cmd.h"

void prompt()
{
  char p[MAXPATH];
  getcwd (p, MAXPATH);
  printf ("%s> ", p);
}

#define curpos() \
        { \
          if (cur > 80-x0) gotoxy (cur-80+x0, y0+1); \
          else gotoxy (x0+cur, y0); \
        }

void edit(char *cmd)
{
  char c;
  unsigned n, cur=0, len=0, x0=wherex(), y0=wherey();
  for (;;)
    switch (c=getch())
    {
      case '\r' : cmd[len]='\0';
                  putchar('\n'); return;
      case '\b' : if (cur == 0) putchar('\a');
                  else
                  {
                    cur--; curpos();
                    for (n=cur; n < len-1; n++)
                    {
                      cmd[n]=cmd[n+1];
                      putchar(cmd[n]);
                    }
                    putchar(' '); len--;
                    curpos();
                  }
                  break;
     case 27    : gotoxy (x0,y0); cur=0;
                  for (n=len+1; n > 0; n--)
                    putchar(' ');
                  len=0; gotoxy(x0,y0);
                  break;
     case '\f'  :
     case '\t'  : putchar('\a');
                  break;
     case 0     : switch (getch())
                  {
                    case 75 : /* links */
                              if (cur==0) putchar('\a');
                              else cur--;
                              curpos(); break;
                    case 77 : /* rechts */
                              if (cur==len) putchar('\a');
                              else cur++;
                              curpos(); break;
                    default : putchar('\a');
                  }
                  break;
     default    : if (len >= MAXCMD-1)
                    putchar('\u');
                  else
                  {
                    cmd[cur++]=c; putchar(c);
                    if (len < cur) len=cur;
                    if (len > 80-x0 && y0==25)
                    y0=24;
                  }
    }
}


Listing 4: exec.c
/* Datei   : exec.c
   Zweck   : Ausführendes Modul
   Sprache : C
   Autor   : Oliver Müller */

#include <stdio.h>
#include <process.h>
#include <dir.h>
#include <string.h>
#include <ctype.h>
#include <mem.h>
#include "error.h"
#include "cmd.h"

extern unsigned envSize;
extern char **eptr, *environ;

#define nobat() \
        { printf ("No batch mode!\n"); \
          return; }

void call (char *args[])
{
  char dr[MAXDRIVE], di[MAXDIR], fi[MAXFILE], ex[MAXEXT], pa[MAXPATH];
  fnsplit (args[0], dr, di, fi, ex);
  if (ex[0] == '\0')
  {
    strcpy (ex, ".COM");
    fnmerge (pa, dr, di, fi, ex);
    if (!searchpath(pa))
    {
      strcpy (ex, ".EXE");
      fnmerge (pa, dr, di, fi, ex);
      if (!searchpath(pa))
      {
        strcpy (ex, ".BAT");
        fnmerge (pa, dr, di, fi, ex);
        if (!searchpath(pa))
        {
          error (eNOTFOUND);
          return;
        }
        else nobat();
      }
    }
  }
  else if (!strcmpi(ex,".BAT")) { nobat(); }
       else strcpy(pa, args[0]);
  if (spawnvpe(P_WAIT, args[0], args, eptr) == -1) perror("");
}

unsigned del(char *name)
{
  unsigned n, l=strlen(name);
  for (n=0; eptr[n]; n++)
    if (!strncmp(name, eptr[n], l) && eptr[n][l] == '=') break;
  if (eptr[n] == 0) return n;
  for (n++; eptr[n]; n++)
  {
    memmove (eptr[n-1], eptr[n], strlen(eptr[n])+1);
    eptr[n]=eptr[n-1] + strlen(eptr[n-1])+1;
  }
  eptr[--n]=0;
  return n;
}
void add(char *s, unsigned last)
{
  unsigned size=0, n;
  for (n=0; eptr[n]; n++)
    size+=strlen(eptr[n])+1;
  s[strlen(s)] = '=';
  if (size+strlen(s)+1 > envSize)
  {
    error(eENV); return;
  }
  eptr[last] = &environ[size];
  strcpy(eptr[last], s);
  eptr[last+1] = 0;
}

void printenv(void)
{
  unsigned n;
  for (n=0; eptr[n]; n++)
    printf("%s\n",eptr[n]);
}

#pragma warn -eff

void set (char *cmd, unsigned argc)
{
  char *ptr, c[MAXCMD];
  unsigned last;
  if (argc == 1) printenv();
  else
  {
    while (isspace(*cmd))  *cmd++;
    while (!isspace(*cmd)) *cmd++;
    while (isspace(*cmd))  *cmd++;
    strcpy (c, cmd);
    if ((ptr=strstr(c, "=")) == NULL)
    {
      error(eSYN); return;
    }
    *ptr='\0'; *ptr++;
    strupr(c); last=del(c);
    if (ptr[0] != '\0') add(c, last);
  }
}

#pragma warn -eff

void type (unsigned argc, char *args[])
{
  unsigned n; int c; FILE *f;
  if (argc == 1)
  {
    error(eMISSARG); return;
  }
  for (n=1; args[n]; n++)
  {
    f=fopen(args[n], "rt");
    if (!f)
    {
      error(eOPEN, args[n]); return;
    }
    while ((c=getc(f)) != EOF) putchar(c);
    fclose(f); putchar('\n');
  }
}


Listing 5: parse.c
/* Projekt : Kommandointerpreter
   Datei   : parse.c
   Zweck   : Parser-Modul
   Sprache : C
   Autor   : Oliver Müller */

#ifdef __TURBOC__
  #include <alloc.h>
#else
  #include <malloc.h>
#endif
#include <string.h>
#include <ctype.h>
#include <dir.h>
#include <stdlib.h>
#include <fcntl.h>
#include <io.h>
#include <stdio.h>
#include <sys\stat.h>
#include "error.h"
#include "cmd.h"

#define pfree() \
        { \
          for (n=0; p[n]; n++) \
            free(p[n]); \
          return; \
        }

int out, in;
extern short pFlag;

void force (int a, int b)
{
  asm {
    mov ah,0x46
    mov bx,b
    mov cx,a
    int 0x21
  }
}

#pragma warn -eff

int redirect(char *rest)
{
  char ofile[MAXPATH], ifile[MAXPATH];
  unsigned n;
  ofile[0]='\0'; ifile[0]='\0';
  for (; *rest!='\0'; *rest++)
    if (*rest == '<')
    {
      if (ifile[0] != '\0')
      {
        error(eINRED); return 0;
      }
      for (*rest++; isspace(*rest); *rest++);
      for (n=0; !isspace(*rest) && *rest != '\0'; *rest++,n++)
        ofile[n]=*rest;
      ofile[n]='\0';
    }
    else if (*rest == '>')
    {
      if (ofile[0] != '\0')
      {
        error(eOUTRED); return 0;
      }
      for (*rest++; isspace(*rest); *rest++);
      for(n=0; !isspace(*rest) && *rest != '\0'; *rest++,n++)
        ofile[n]=*rest;
      ofile[n]='\0';
    }
  if (ofile[0])
    if ((out=open(ofile, O_CREAT|O_TRUNC|O_TEXT, S_IWRITE|S_IREAD)) == -1)
    {
      perror(""); return 0;
    }
 if (ifile[0])
   if ((in=open(ifile, O_RDONLY|O_TEXT)) == -1)
   {
     if (out != -1) close(out);
     perror(""); return 0;
   }
 return 1;
}

#pragma warn -eff

void parse (char *cmd)
{
  char *p[MAXCMD], *ptr;
  unsigned n=0, m, i=0;
  int odup, idup;
  in=-1; out=-1;
  for(;;)
  {
    while (isspace(cmd[n]))
    {
      if (cmd[n] == '\0') break;
      n++;
    }
    if (cmd[n] == '\0') break;
    ptr=&cmd[n]; m=n; n++;
    if (ptr[0] == '>' || ptr[0]=='<')
    {
      if (!redirect(ptr)) pfree();
      break;
    }
    else
    {
      if (ptr[0] == '"')
      {
        while (cmd[n] != '"')
        {
	  if (cmd[n] == '\0')
          {
	    error(eQUOTE); pfree();
          }
	  n++;
        }
        n++;
      }
      else
        while (!isspace(cmd[n]))
        {
          if (cmd[n] == '\0') break;
          n++;
        }
      p[i]=(char*)malloc(n-m+1);
      if (!p[i])
      {
        error(eOUTOFMEM);
        pfree();
      }
      strncpy(p[i], ptr, n-m);
      p[i][n-m]='\0'; i++;
    }
  }
  p[i]=0;
  if (out != -1)
  {
    odup=dup(1); force(1, out);
  }
  if (in != -1)
  {
    idup=dup(0); force(0, in);
  }
  if (p[0] == 0); /* Leerzeile */
  else if (!strcmpi(p[0], "EXIT"))
       {
         if (!pFlag) exit(0);
       }
       else if (!strncmpi(p[0], "CD", 2) &&
               (p[0][2] == '\0' || p[0][2] == '.' || p[0][2] == '\\'))
            {
              if (p[0][2] == '\0')
              {
                if (i != 2) error(eARGS, "CD", 1);
                if (chdir(p[1]) == -1) perror("");
              }
              else
              {
                if (i != 1) error(eARGS, "CD", 1);
                if (chdir(&p[0][2]) == -1) perror("");
              }
            }
            else if (!strcmpi(p[0], "TYPE"))
                 {
                   type(i,p);
                 }
                 else if (!strcmpi(p[0], "SET"))
                      {
                        set(cmd,i);
                      }
                      else call(p);
  if (out != -1)
  {
    force(1, odup); close(out);
  }
  if (in != -1)
  {
    force(0, idup); close(in);
  }
  putchar('\n'); pfree();
}


Listing 6: error.c
/* Projekt : Kommandointerpreter
   Datei   : error.c
   Zweck   : Fehlerbehandlungsmodul
   Sprache : C
   Autor   : Oliver Müller */

#ifdef __TURBOC__
  #include <alloc.h>
#else
  #include <malloc.h>
#endif
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include "error.h"

static char *msg[] =
{
  "Environment full !\n",
  "Missing quote!\n",
  "%s expects %d parameter(s)!\n",
  "Input already redirected!\n",
  "Output already redirected!\n",
  "File or path not found!\n",
  "Unknown option %s!\n",
  "Syntax error!\n",
  "Out of memory!\n",
  "Missing arguments!\n",
  "Can't open %s!\n"
};

void error(int no,...)
{
  va_list ap;
  va_start (ap, no);
  vfprintf (stderr, msg[no], ap);
  va_end (ap);
}

char *emalloc(unsigned size)
{
  char *ptr=(char*)malloc(size);
  if (!ptr)
  {
    fprintf (stderr, "Out of memory!\n");
    exit(1);
  }
  return ptr;
}


Listing 7: error.h
/* Projekt : Kommandointerpreter
   Datei   : error.h
   Zweck   : Spezifikation Fehlerbehandlung
   Sprache : C
   Autor   : Oliver Müller */

#ifndef __ERROR_H_OGM
#define __ERROR_H_OGM

#define eENV 0
#define eQUOTE 1
#define eARGS 2
#define eINRED 3
#define eOUTRED 4
#define eNOTFOUND 5
#define eUNKNOWN 6
#define eSYN 7
#define eOUTOFMEM 8
#define eMISSARG 9
#define eOPEN 10

void error(int no,...);
#endif


Listing 8: makefile
# Kommandointerpreter für mc extra
# Autor: Oliver Mueller

CC=bcc
LINK=tlink
CFLAGS=-c -v -ms -O -O2 -Ie:\bcc31\include
LFLAGS=/x /v /Le:\bcc31\lib c0s
LIBS=cs.lib
OBJS=cmd.obj error.obj edit.obj parse.obj exec.obj

.c.obj:
	$(CC) $(CFLAGS) $*.c

cmd.exe: $(OBJS)
	$(LINK) $(LFLAGS) $(OBJS), $<,,$(LIBS)

cmd.obj : cmd.c cmd.h error.h

error.obj: error.c error.h

parse.obj: parse.c error.h cmd.h

exec.obj: exec.c error.h cmd.h

edit.obj: edit.c cmd.h

clean:
	del *.obj


aus mc extra 04/96 – Seite 31-35