Die vorgekaute Kommandozeile

Der Unix-User hat’s leicht: Wenn er z.B. in einem Haufen von C-Quetltexten alle Vorkommen von "struct gaga" sucht, tippt er fgrep ‘structgaga’ *.c>a ein (oder was ähnliches), woraufhin die Resultate in eine frisch erzeugte Datei namens „a“ wandern, die ersieh dann in seinen Editor oder sonstwohin ziehen kann.

Auch das Programmieren solcher Utilities wie ‘fgrep’ ist unter Unix ein Kinderspiel, denn ein beträchtlicher Teil seiner Arbeit wird dem Programmierer von der Kommando-Shell abgenommen. Diese verrichtet nämlich in obigem Beispiel gleich drei Hiwi-Jobs:

Die Vorteile einer solchen automatischen Argument-Expansion für den Programmierer liegen also auf der Hand. Aber auch der Benutzer profitiert davon: Der Expansions-Mechanismus hat immer die gleiche Syntax, und der Mensch ist ja bekanntlich ein Gewohnheitstier. Das mühsame Studium von Gebrauchsanweisungen reduziert sich auf ein Minimum.

Das können wir auch! Oder?

Kein Wunder also, daß diverse Kommando-Shells für unseren ST, wie etwa die beliebte PD-Shell ‘Gulam’, diesen Mechanismus nach bestem Wissen und Gewissen nachzubilden suchen. Aber wir können ja alle ein Lied davon singen: Leider funktioniert der Mechanismus nur bei den eingebauten Shell-Kommandos einwandfrei, z.B. bei ‘grep’ oder ‘Is’ in Gulam. Bei „externen Kommandos“ hingegen, d.h. bei von der Shell gestarteten Programmen, erleben wir allerlei unangenehme Überraschungen:

Die Gründe hierfür sind hinlänglich bekannt:

Die Sache mit den Wildcards

Zur Überwindung dieser Probleme sind diverse Strategien vorgeschlagen worden. Beispielsweise für die Übergabe langer Argumentlisten die jetzt von Atari höchstselbst abgesegnete „ARGV-Methode“, bei der die Argumente über das Environment an den Kind-Prozeß weitergereicht werden. Derlei Mechanismen können natürlich nur dann funktionieren, wenn sich sowohl der Eltern-Prozeß (etwa die Kommando-Shell) als auch der Kind-Prozeß (das aufgerufene Programm) an das gleiche so vereinbarte Protokoll halten.

Bei der ARGV-Methode bedeutet das in der Praxis ganz konkret: Der Elternprozeß expandiert die Wildcards so, wie er es für richtig hält, und legt die resultierende Argumentliste an der vereinbarten Stelle im Environment ab, damit der Kindprozeß sie sich dann ebendort abholen kann. Letzteres geschieht am elegantesten im sogenannten „Startup-Code“, also einem vom Compiler-System automatisch erzeugten Programmteil, der unmittelbar nach dem Start des Prozesses durchlaufen wird, bevor die vom Programmierer geschriebene Funktion mainO angesprungen wird. Aber selbst wenn unser Compiler in seinem Startup-Code den ARGV-Standard unterstützen sollte, was selten genug der Fall ist, bleibt die mühselige Arbeit des Wildcard-Expandierens dem Elternprozeß überlassen. Der Elternprozeß ist aber oft gar keine Kommando-Shell, sondern irgendein pfiffiges Anwendungsprogramm (z.B. ein Text-Editor oder was auch immer) mit einer eingebauten Option „Externes Programm starten“. Klar, daß die Schöpfer solcher Anwendungsprogramme normalerweise genug andere Sorgen haben, als für irgendwelche optional gestarteten Programme auch noch nebenbei die Wildcards zu expandieren. Von eventueller I/O-Umleitung will ich gar nicht erst reden, denn um den standardmäßig von GEMDOS hierfür vorgesehen Mechanismus zum Funktionieren zu bewegen, muß man mindestens eines dieser berüchtigten Systemvektor-verbiegenden Utility-Programme im AUTO-Ordner stehen haben...

Eine portable Lösung

Nach diesen Vorbemerkungen sollte klar sein, daß eine halbwegs befriedigende universelle Lösung eigentlich nur die sein kann, über den Elternprozeß überhaupt keine speziellen Annahmen (ARGV-Fähigkeit usw.) zu machen und die gesamte Drecksarbeit der Kommandozeilen-Expansion in den Startup-Code des Compilers zu verlagern. Der Startup-Code existierender Compiler ist normalerweise in Assembler geschrieben, also nicht gerade ein pflegeleichter Programmteil, und zu allem Überdruß auch noch hochgradig abhängig vom verwendeten Compilersystem, deren es ja bekanntlich mehrere gibt (Lattice, Laser, Pure, Megamax, Mark Williams, Turbo, Sozobon, um nur einige zu nennen). Glücklicherweise ist aber ein derart schmerzlicher Eingriff in den Startup-Code nicht nötig; es geht auch ganz „portabel“ (d.h. systemunabhängig), wie das hier vorgestellte Modul beweist. Sinnigerweise habe ich es ARGX getauft.

Unser Trick besteht einfach darin, daß wir uns hinter dem Startup-Code einklinken, also in den Aufruf der vom Programmierer geschriebenen Funktion main()- Um den Programmierer (im allgemeinen sind wir das ja selbst!) nicht unnötig zu verwirren, verlangen wir von ihm nur, daß er bitteschön die Zeile #include "argx.h" in den Anfang des Programmteils setzen soll, in dem auch die Funktion mainO steht, und außerdem unser Modul argx.c mit an seinen Code dazulinken muß.

Und was haben wir davon?

Als Belohnung für diese kleine Mühe kriegen wir als Parameter unserer Funktion main() eine fertig vorgekaute Argumentliste. In dieser sind die Wildcards schon expandiert, die Gänsefüßchen so wie gewünscht interpretiert, und außerdem eventuelle Direktiven zur I/O-Umleitung befolgt und danach entfernt worden. Als kleiner Bonus steht noch, wieder im Unix-Stil, im „nullten Argument“ der Kommandoname, also der Name, unter dem unser Programm zur Laufzeit gestartet wurde. Man erinnere sich: Der Kopf der Funktion main() im C-Programm hat i.a. folgendes Aussehen:

void main(argc, argv, env)
int argc; /* Anzahl der Argumente */
char **argv; /* Array von Zeigern auf Arg.-Strings */
char **env; /* Array von Zeigern auf Env.-Strings */
{
    ...

wobei die beiden Array-Parameter argv und env wie folgt organisiert sind:

argv[0] = Name des gerade ablaufenden Kommandos
argv[1] = 1. Argument von der Kommandozeile
argv[2] = 2. Argument
argv[argc - 1] = letztes Argument

env[0] = 1. Env.-String (z.B. "PATH=;C:\")
env[1] = 2. Env.-String
env[N - 1] = letzter Env.-String
env[N] = NULL (Ende-Marke)

Dabei garantiert unser ARGX-Code das so definierte, Unix-kompatible Format des Environments env. Leider ist dies keine Selbstverständlichkeit auf dem ST; einige Compiler liefern für env nämlich einfach die Anfangsadresse des Speicherbereichs, in dem die Environment-Strings abgelegt sind.

Die neuen Kommandozeilen-Funktionen

Was macht denn nun ein mit ARGX ausgerüstetes Programm genau? Es schaut sich einfach den gewöhnlichen (max. 126 Zeichen langen) GEMDOS-Standard-Kommandozeilen-Puffer an, und liest die Argumente von dort ein. Als Worttrenner dienen dabei wie üblich Leerzeichen (oder Tabs). Wenn aber irgendwo ein einfaches oder doppeltes Anführungszeichen angetroffen wird, werden alle darauffolgenden Zeichen bis zum nächsten Anführungszeichen der jeweils gleichen Sorte in einem Stück wörtlich übernommen. Die begrenzenden Anführungszeichen selbst werden dabei natürlich entfernt. Auf diese Weise sind fortan auch Argumente möglich, die Leerzeichen enthalten.

Wenn nun ein Argument Wildcard-Zeichen ('*’, “?’) enthält, wird eine Expansion versucht. D.h. es wird eine alphabetisch sortierte Liste aller Vorgefundenen Dateinamen erzeugt, die auf das Muster passen. Wenn so eine nicht-leere Liste entsteht, wird sie anstelle des angegebenen Musters in die Argumentliste eingefügt. War die Liste leer, wird das Muster unverändert übernommen. Um dem Anwender noch ein wenig Extrakomfort zu bieten, habe ich mir bei dieser Gelegenheit erlaubt, die normale (MS-)DOS-Expansionsmethode geringfügig zu erweitern:

Damit ist unser Leben wieder ein wenig Unix-ähnlicher geworden. Den ganz eingefleischten Unix-Puristen unter uns überläßt der Autor gerne die Implementation der vollen Unix-Wildcard-Syntax als Übungsaufgabe.

Nun zur I/O-Umleitung. Viel zu sagen gibt’s hier eigentlich nicht; alles funktioniert so, wie man es von MS-DOS oder Unix her gewohnt ist. Wenn in der Kommandozeile Ausdrücke der Form

<infile (Eingabe von Datei infile holen)
> outfile (Ausgabe in Datei outfile schreiben)
>> appendfile (Ausgabe an Datei appendfile anhängen)

stehen, werden diese nicht in die Argumentliste übernommen, sondern als Aufforderung zur I/O-Umleitung aufgefaßt und entsprechend die Standardein- oder -ausgabe auf die angegebene Datei umgelenkt. Dabei dürfen zwischen I/O-Umleitungs-Symbol und Dateinamen Leerzeichen stehen, müssen aber nicht.

Selbstverständlich kann der ehrenwerte User jederzeit, wenn er es wünscht, die spezielle Interpretation von Zeichen wie ‘<‘ oder durch unseren Mechanismus verhindern, indem er sie in Anführungszeichen einschließt. Sonst noch Fragen?

Zu den Listings

Schließlich noch ein paar Bemerkungen zu den Listings:

ist auf einem ANSI-Standard-Compiler im wesentlichen äquivalent zu

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

typedef char bool;

#define FALSE 0 
#define TRUE 1

Die Datei stdstuff.h erledigt automatisch die entsprechenden Anpassungen für diverse Nicht-ANSI-Compiler: Diese kennen ja meist nicht die berüchtigten Funktions-Prototypen, haben fast nie ein <stdlib.h>, manchmal ist ‘TRUE’ schon in <stdio.h> definiert usw.


/* ARGX.C * (c) 1992 MAXON Computer * * Modul zur automatischen Expansion von Kommandozeilen-Parametern * Expandiert Wildcards, beruecksichtigt Anfuehrungszeichen, und * wertet I/O-Umieitungs-Direktiven aus */ /* * Zunaechst der obligatorische Standardkram */ #include "stdstuff.h" /* * Dann das GEMDOS-Binding, dh die Schnittstelle zu DOS-Funktionen * wie Fsfirst(), Fsnext(), usw Unser Compiler muss ausserdem eine * Variable namens '_base' oder dergl erzeugen, die zur Laufzeit die * Adresse der Basepage des Programms enthaelt * * Systemabhaengig! Als Beispiel fuer ein Nicht-Sozobon-C-System sind * hier die noetigen #ifdef's und #define's fuer Turbo-C zu sehen. */ #ifdef __TURBOC__ #include <tos.h> #define _base _BasPag #else #include <osbind.h> #endif /* * Codes fuer I/O-Umleitungs-Symbole */ #define INPUT 1 /* < infile */ #define OUTPUT 2 /* > outfile */ #define APPEND 3 /* >> appendfile */ #define NONE 0 /* nix von alledem */ /* * Die 3 Argumente fuer das, was der arme Benutzer fuer die Funktion * main{) haelt */ static int Argc; /* Anzahl der Argumente + 1 */ static char **Argv; /* Array der Argument-Strings */ static char **Env; /* Array der Environment-Strings */ /* * Funktions-Vorwaerts-Deklarationen ANSI-Compiler werden mit * Prototypen, der Rest der Welt mit K&R Funktions-Deklarationen * versorgt */ static char *wildmatch FUNC((char *patt)); static void sort FUNC((char **argv)); static void addarg FUNC({char *arg)) static char *savestr FUNC((char *p)); static char *xmalloc FUNC((unsigned n)); static char *xrealloc FUNC((char *p, unsigned n)) /* * Unsichtbar fuer den Benutzer: Die "wahre" Funktion main(). Unser * Header "argx fa" benennt kurzerhand die vom Benutzer geschriebene * Funktion main() in main_() um, so dass sein Programm tatsaechlich * immer HIER startet */ void main() { extern struct basepage /* auch 'Process Descriptor' genannt */ { char *p_lowtpa; char *p_hitpa, char *p_tbase; long p_tlen; char *p_dbase; long p_dlen; char *p_bbase; long p_blen; char *p_dta; struct basepage *p_parent, /* Basepage des Elternprozesses */ long p_reserved1; char *p_env; /* Zeiger auf Environment-Strings */ char p_devx[6]; char p_reserved2; char p_defdrv; long p_reserved3[18]; char p_cmdlin[128]; /* GEMDOS-Kommandozeile */ } *_base; /* zeigt auf unsere eigene Basepage */ char linebuf[128]; /* Puffer fuer Kommandozelle */ char wordbuf[128]; /* Puffer fuer 1 extrahiertes Wort */ int io, /* welches I/O-Symbol wars? */ bool iswild; /* wild expandieren? */ register char c, d, *p, *q, /* fleissige Helferlein */ register int i, n; /* Maedchen fuer alles */ /* * Die Adressen der Environment-Strings packen wir in ein * NULL-terminiertes Array, wie sich das gehoert, und ignorieren * dabei souveraen eventuellen ARGV-Kram. */ n = 0; if ((p = _base->p_env) != NULL) while (*p != '\0' && strncmp(p, "ARGV=",5) != 0) { while (*p++ *= '\0'); n++; } Env = (char **)xmalloc(sizeof(char *) * (n + 1)); p = _base->p_env; for (i = 0; i < n; i++) { Env[i] = p; while (*p++ != '\0'); } Env[n] = NULL; /* * Nun beginnen wir mit dem Bau unseres Argument-Arrays. */ Argc = 0; Argv = NULL, addarg(""); /* provisor Wert fuer Argv[0] */ /* * GEMDOS-Kommandozeilen-Puffer kopieren */ p = _base->p_cmdlin; /* GEMDOS-Puffer-Adresse */ n = *p++; /* Laengen-Byte auslesen */ strncpy(linebuf, p, 127); /* Rest kopieren */ linebuf[127] = '\0'; /* und mit NUL abschliessen */ if (n >= 0 && n < 127) /* wenn Laengen-Wert sinnvoll */ linebuf[nj = '\0'; /* dorthin noch ne NUL */ /* * Zeilenpuffer-Inhalt auswerten */ p = linebuf; /* Auf die Plaetze */ c = *p++; /* fertig .. */ for (;;) /* los! */ { /* * Leerzeichen ueberspringen */ while (c != '\0' && isspace(c)) c = *p++; /* * Gegebenenfalls I/O-Umleitungs-Symbol einlesen */ if (c = '<') io = INPUT, c = *p++; else if (c != '>') io = NONE; else if ((c = *p++) == '>') io = APPEND; c = *p++; else io = OUTPUT; if (io != NONE) /* * Leerzeichen hinter I/O-Symbol ueberspringen */ while (c != '\0‘ && isspace(c)) c = *p++, /* * Nicht vergessen: Abbruch bei NUL-Zeichen */ if (c = '\0') break; /* * Nun 1 Wort vom Zeilen- in den Wortpuffer uebertragen * Dabei auf Anfuehrungs- und Wildcard-Zeichen achten */ iswild = FALSE; /* wird TRUE bei Wildcards */ q = wordbuf; /* da soll unser Wort hin */ while (c != '\0' && !isspace(c)) { if (c == '\'' || c == "") { /* * Anfuehrungszeichen gefunden: Alles * bis z. naechstsn Anfuehrungszeichen der * gleichen Sorte wcertlich uebernehmen */ d = c; while ((c = *p++) != '\0' && c != d) *q++ = c; if (c = '\0’) /* * Fehlendes abschliessendes Gaensefuesschen * hoeflich ignorieren */ break; } else { /* * Sonst Zeichen direkt uebernehm * Ggf Wildcard-Alarm ausloesen */ if (c == || C == '?') iswild = TRUE; *q++ = c; } c = *p++; /* naechstes Zeichen */ } *q = '\0'; /* Wort abschliessen */ /* * Wenn des so erhaltene Wort leer ist (z B. bei "" * in der Kommandozeile) ignorieren */ if (*wordbuf == '\0') continue; /* * I/O-Umleitung oder Wildcard-Expansion ausfuehren * und ggf. resultierende Argumente in argv[] ablegen. */ if (io = INPUT) freopen(wordbuf, "r", stdin); /* < infile */ else if (io == OUTPUT) freopen(wordbuf, "w", stdout); /* > outfile */ else if (io == APPEND) freopen(wordbuf, "a", stdout); /* >> appendfile */ else if (iswild && (q = wildmatch(wordbuf)) != NULL) { /* * Wildcard-Suche lieferte einen passenden Namen: * Hole alle uebrigen passenden Namen und ordne die * resultierende Liste alphabetisch. */ n = Argc; /* ab hier wird sortiert */ do addarg(q); while ((q = wildmatch(NULL)) != NULL); sort(S&rgv[n]); } else /* * Wildcard-Suche war erfolglos, oder mutwillig durch * Anfuehrungszeichen verhindert worden: Wort direkt * an Argumentliste anhaengen. */ addarg(wordbuf); } /* * Wenn moeglich, setzen wir das nullte Argument auf den Namen, * unter dem unser Programm gestartet wurde, den wir uns auf * etwas verschlungenen Pfaden besorgen muessen. */ if ((p = (char *)_base->p_parent) != NULL) { p = *(char **)(p + 0x7C); p = *(char **)(p + 0x36); *Argv = strlwr(savestr(p)); /* kopiert & kleingeschrieben */ } /* * Schliesslich koennen wir nach unserer fleissigen Vorarbeit * den User mit einer voll expandierten Argumentliste in seinem * main() begluecken. */ main_(Argc, Argv, Env); exit(0); } /* * Routine zur Wildcard-Expansion: Liefert entweder naechsten * auf das angegebene Muster patt' passenden Eintrag, oder NULL * wenn nichts mehr gefunden worden konnte. Bei patt != NULL wird * eine neue Suche gestartet, ansonsten eine schon begonnene Suche * fortgesetzt */ static char *wildmatch(patt) char *patt; { static struct dta { char d_reserved[21]; char d_attr; short d_time; short d_date; long d_size; char d_name[14]; } dta[1]; /* unsere DTA */ static char buf[256]; /* Puffer fuer (vollen) Dateinamen */ static char *name; /* zeigt auf Namen hinter Pfad-Praefix */ register char c, *p, *q; /* dies & das */ register int err; /* Fehlercode von Fsfirst()/Fsnext() */ if (patt != NULL) { /* * Suche mit neuem Muster starten: Muster in unseren privaten * Puffer kopieren Guenstige Gelegenheit, um dabei alle * Unix'schen Vorwaertsschraegstriche nach links umzuklappen, * so wie es GEMDOS mag */ p = buf; for (q = patt; (c = *q) != '\0'; q++) { if (p >= buf + sizeof(buf) - 16) return NULL; /* Puffer zu klein */ *p++ = (c == '/') ? '\\ : c; /* Schraegstriche umklappen */ } *p = '\0'; /* * Vom String-Ende beginnend, das Ende des Pfad-Praefixes * suchen (markiert durch Backslash oder Laufwerk-Doppelpunkt) * und merken, damit wir spaeter die gefundenen Dateinamen dort * hinschreiben koennen */ while (p != buf && strchr("\\:",p[-1]) == NULL) p--; name = p; /* * Nun gehts wieder nach rechts. Kucken, ob das Muster einen * Punkt enthaelt Wenn ja, so lassen Wenn nicht, dann * am * Ende durch *.* ersetzen (Billig-Emulation von Unix-Wildcards) */ while ((c = *p) != '\0' && c != ' ') p++; if (c = '\0' && p != name && p[-1] == '*') strcpy(p, ".*"); /* * Suche mit DOS-Funktionen starten */ Fsetdta(dta); err = Fsfirst(buf, 0); } else /* * Schon begonnene Suche fortsetzen */ err = Fsnext(); if (err != 0) /* * Keine weiteren Dateien gefunden, oder Muster war Schrott. */ return NULL; /* * Gefundenen Namen hinter Pfad-Praefix ablegen und vollen * Namen (in Klemschrexbweise) zurueckliefern */ strcpy(name, dta->d_name); return strlwr(buf); } /* * NULL-terminierten argv[] alphabetisch sortieren * Ginge sicherlich auch mit Quicksort, aber wen kuemmert's. */ static void sort(argv) char **argv; { char *arg, **argp; for (; *(argp = argv) != NULL; argv++) while (*++arqp != NULL) if (strcmp(*argp, *argv) < 0) { arg = *argv; *argv = *argp; *argp = arg; } } /* * Ge-malloc()te Kopie eines Strings an Argv[] anhaengen * Sorgt automatisch fuer abschliessende NULL */ static void addarg(arg) char *arg; { Argv = (char **)xrealloc(Argv, sizeof(char *) * (Argc + 2)); Argv[Argc] = (arg == NULL) ? NULL : savestr(arg); Argv[++Argc] = NULL; } /* * Ge-malloc()te Kopie von einem String machen. */ static char *savestr(p) char *p; { return strcpy(xmalloc(strlen(p) + 1), p); } /* * Speicher per malloc() reservieren * Bei Fehlschlag Programm mit Meldung abbrechen */ static char *xmalloc(n) unsigned n; { return xrealloc(NULL, n); } /* * Speicher per malloc() reservieren oder per realloc() vergroessern * Bei Fehlschlag Programm mit Meldung abbrechen */ static char *xrealloc(p, n) char *p; unsigned n; { p = (p == NULL) ? malloc(n) : realloc(p, n); if (p == NULL) { fputs("Memory full\n", stderr); exit(1); } return p; }
/*
 * ARGX.H
 *
 * Header-Datei fuer automatische Argument-Expansion.
 */

#define main main_ /* das wars schon */

/* * STDSTUFF.H * * Nasi's Standard Include-Datei */ #ifndef STDSTUFF_H #define STDSTUFF_H /* * Bei einigen praehistorischen Compilern, die den Datentyp 'void‘ * nicht kennen, muss man die folgende Zeile aktivieren */ /* #define void int */ /* * Die folgende Zeile ist fuer einige aeltere Compiler, bei denen * der Header fuer die Standard-String-Funktionen <strings.h> * und nicht <string.h> heisst. */ /* #define NO_STRING_H */ /* * Zunaechst das Unvermeidliche... Wir setzen voraus, dass <stdio.h> * Deklarationen fuer alle Standard-I/O Funktionen enthaelt */ #include <stdio.h> #ifndef EOF #define EOF (-1) #endif #ifndef NULL #define NULL (char *)0 #endif /* * ANSI-konforme Compiler haben eine Standard-Include-Datei <stdlib.h> * — oder sollten ($%&/#^!) zumindest eine solche haben -- in der * der Datentyp 'size_t' definiert wird und Deklarationen von Standard- * Funktionen wie malloc(), getenv() stehen. */ #ifdef __STDC__ #define FUNC(args) args /* int f FUNC((int a)) —> int f(int a) */ #define VOID void /* VOID *p —> void *p */ #include <stdlib.h> /* Dekl. von malloc() usw. */ #else #define FUNC (args) () /* int f FUNC((int a)) —> int f() */ #undef VOID /* fuer Sozobon... */ #define VOID char /* VOID *p —> char *p */ #define const #define size_t unsigned extern void free(), exit(); extern int atoi(), abs(), system(); extern long atol(), labs(); extern char *malloc(), *calloc(), *realloc(); extern char *itoa(), *ltoa(), *ultoa(), *getenv(); #endif /* * Bei Compilern mit <strings.h> statt <string.h> tragen die Standard- * String-Funktionen noch altmodische Namen */ #ifdef NO_STRING_H #include <strings.h> #define strchr index #define strrchr rindex #define memcmp bcmp #define memcpy (q,p,n) bcopy(p,q,n) #else #include <string.h> #endif #include <ctype.h> typedef char bool; #ifndef FALSE #define FALSE 0 #endif #ifndef TRUE #define TRUE 1 #endif #endif STDSTUFF_H
/*
 * XECHO.C
 *
 * Testprogramm zur automatischen Argument-Expansion .
 */

#include "stdstuff.h"
#include "argx.h”

void main(argc, argv, env) 
int argc; 
char **argv;
char **env;     /* nicht etwa 'char *env;'! */
{
    int i;

    for (i = 0; i < argc; i++)
        printf("%d: %s\n", i, argv[i]); 
    printf("\n”);
    for (i = 0; env[i] != NULL, i++)
        printf("%s\n", env[i]); 
    exit(0);
}
#
# Makefile for xecho.ttp
#

CFLAGS = -O

OFILES = argx.o xecho.o 
HFILES = stdstuff.h argx.h

.c.o:
    cc -c $(CFLAGS) $<

xecho.ttp: $(OFILES) 
    cc $(OFILES) -o $@

$(OFILES): $(HFILES)

Ralf Nasilowski
Links

Copyright-Bestimmungen: siehe Über diese Seite