Kapitel 15: Stringmanipulation

Inhaltsverzeichnis

Kapitel 17: Betriebssystem-Anweisungen

16. Datei-Zugriff

Ebenfalls ein wichtiger Punkt bei der Verarbeitung von Daten ist der Zugriff auf externe Ressourcen, insbesondere auf Dateien. Zunächst beschäftigen wir uns mit den klassischen Dateien, die auf einem Datenträger gespeichert werden; aber auch Hardwarezugriffe über COM-Port oder Drucker werden wie Dateizugriffe geregelt.

16.1 Datei öffnen und schließen

Beim Öffnen einer Datei gibt es bereits eine Menge Dinge, die Sie Ihrem Programm mitteilen müssen. Bevor wir die Informationen im Einzelnen besprechen, werfen wir erst einmal einen Blick auf die Syntax:

OPEN dateiname FOR dateimodus [ACCESS zugriffsart] [LOCK sperrart] _
     [ENCODING format] AS [#]dateinummer [LEN = recordlaenge]

Der Dateiname muss dabei einschließlich der Dateiendung angegeben werden1 und darf auch eine Pfadangabe enthalten. Auf die Pfadangaben geht Kapitel 17.1.1 genauer ein.

Warning Achtung:
Beachten Sie bei allen Datei- und Pfadnamen die korrekte Groß- und Kleinschreibung! Windows arbeitet case insensitive, d. h. dem System ist Groß- und Kleinschreibung egal. Anders ist das jedoch unter Linux, welches case sensitive arbeitet. Schon aus Gründen der Portabilität ist die korrekte Schreibweise dringend anzuraten — sonst stoßen Anwender auf einer anderen Plattform möglicherweise auf Probleme, die vom Entwickler nicht nachvollzogen werden können.

In QuickBASIC kam es zu einem Laufzeitfehler, wenn die Datei aus irgendwelchen Gründen nicht geöffnet werden konnte. Bei einem compilierten Programm wäre es aber sehr unvorteilhaft, wenn es wegen einer fehlenden Datei gleich zum Absturz käme. Daher wird ein möglicherweise auftretender Fehler ignoriert und die Datei einfach nicht geöffnet. Anschließende Lese- und Schreibzugriffe laufen dann ins Leere. Um dennoch einen missglückten Öffnen-Versuch abzufangen, kann OPEN auch als Funktion eingesetzt werden. Wenn die Datei ohne Fehler geöffnet werden konnte, liefert die Funktion den Wert 0. Ansonsten entspricht der Rückgabewert der Fehlernummer.

In der Funktions-Variante des Befehls müssen die Parameter eingeklammert werden:

wert1 = OPEN(dateiname FOR dateimodus [ACCESS zugriffsart] [LOCK sperrart] _
            [ENCODING format] AS [#]dateinummer [LEN = recordlaenge])

Mögliche Fehlernummern sind:

  • 1: 'Illegal function call' (ungültiger Funktionsaufruf)
    Dies ist der Fall, wenn Sie eine Dateinummer auswählen, unter der bereits eine Datei geöffnet ist.

  • 2: File not found (Datei nicht gefunden)
    Die gewünschte Datei existiert nicht, oder Sie besitzen nicht die erforderlichen Rechte, um die Datei zu öffnen. Der Fehler kann auch auftreten, wenn z. B. mit ENCODING "UTF-8" eine Datei geöffnet werden soll, die nicht UTF-8-codiert ist oder bei der das Byte Order Mark nicht gesetzt ist.
    'File not found' ist in der Regel der am häufigsten auftretende Fehler.

  • 3: 'File I/O error' (Datei-Ein-/Ausgabe-Fehler)
    Es ist ein allgemeiner Lese- oder Schreibfehler aufgetreten, etwa weil kein Speicherplatz mehr zur Verfügung steht.

Note Hinweis:
Gerade im Umgang mit externen Ressourcen ist es sinnvoll, Fehler (wie in diesem Fall Lese- und Schreibfehler bei einer Datei) abzufangen, da Sie ja keine darüber hinausgehende Kontrolle haben, ob die Ressourcen wirklich existieren und zugreifbar sind. Wir werden in diesem Kapitel ein paar solcher Abfangroutinen einsetzen.

16.1.1 Dateinummer festlegen

Der Zugriff auf Dateien erfolgt über eine Dateinummer. Wenn Sie eine Datei öffnen, legen Sie eine Nummer im Bereich von 1 bis 255 fest, unter der die Datei in Zukunft angesprochen werden soll. Das bedeutet zum einen, dass Sie später für die Dateizugriffe den Dateinamen nicht mehr zu kennen brauchen (es spielt auch keine Rolle mehr, ob es sich überhaupt um eine Datei handelt oder aber um einen Drucker oder eine Konsole), zum anderen können Sie aber auch nicht zwei Dateien zur gleichen Zeit unter derselben Nummer öffnen — das Programm wüsste dann ja nicht mehr, welche Datei denn nun gemeint ist.

Es ist also nötig, eine Dateinummer zu wählen, die im Augenblick noch nicht in Verwendung ist. Für kurze, überschaubare Programme ist das noch kein Problem. Spätestens aber, wenn Sie OPEN in einem Unterprogramm verwenden wollen, können Sie nicht sicher sein, ob die von Ihnen frei gewählte Dateinummer nicht bereits vom Hauptprogramm oder einem anderen Unterprogramm belegt wurde.

Glücklicherweise steht Ihnen mit FREEFILE() eine Funktion zur Verfügung, die Ihnen die nächste verfügbare Dateinummer mitteilt. Am besten gewöhnen Sie sich — auch bei kurzen Programmen — gleich an, die Dateinummer über FREEFILE() zu ermitteln.

Quelltext 16.1: Freie Dateinummer ermitteln
DIM AS INTEGER dateinummer = FREEFILE
IF OPEN("test.txt" FOR INPUT AS #dateinummer) THEN
  ' Datei konnte erfolgreich geoeffnet werden
END IF

Zum oben verwendeten Dateimodus INPUT kommen wir gleich. Zunächst noch kurz zum verwendeten Rautezeichen (Hash) #: Bei den Lese- und Schreibzugriffen ist es notwendig, dem Compiler mitzuteilen, dass der Zugriff nicht über Tastatur bzw. Ausgabefenster, sondern über die gewünschte Datei erfolgen soll. Um die Dateinummer von einer ganz gewöhnlichen auszugebenden Zahl zu unterscheiden, muss das Rautezeichen vorangestellt werden. Im OPEN-Befehl ist das Rautezeichen optional, wird aber empfohlen, um deutlich zu machen, dass es sich um eine Dateinummer handelt.

Warning Achtung:
Wenn Sie mehrere Dateien öffnen wollen, müssen Sie die erste Datei öffnen, bevor Sie eine FREEFILE-Abfrage für die zweite Datei machen. FREEFILE() fragt immer nach der aktuell nächsten freien Dateinummer. Solange zwischenzeitlich keine Datei geöffnet wird, wird sich der Rückgabewert von FREEFILE() nicht ändern!

16.1.2 Der Dateimodus

FreeBASIC unterstützt fünf Dateimodi, die wir uns nun genauer ansehen werden. Bei den ersten drei handelt es sich um sequentielle Modi, die letzten beiden arbeiten mit einem wahlfreien Zugriff.

INPUT

sequentieller Zugriff: Daten lesen

OUTPUT

sequentieller Zugriff: Daten schreiben

APPEND

sequentieller Zugriff: Daten anhängen

RANDOM

Random-Access (lesen/schreiben)

BINARY

wahlfreier Binärzugriff (lesen/schreiben)

16.1.2.1 Sequentieller Zugriff

Die „klassischen“ Dateien unter BASIC arbeiten mit einem sequentiellen Zugriff. Das bedeutet: Wenn eine Datei z. B. zum Lesen geöffnet wird, befindet sich ein Datenzeiger (Datei-Cursor) am Anfang dieser Datei. Beim Einlesen der Daten wandert dieser Zeiger weiter und markiert immer die Stelle, an welcher der nächste Lesezugriff erfolgen wird. Hat der Zeiger das Dateiende erreicht (EOF: End Of File), kann nicht weiter gelesen werden. Ein Lesezugriff erfolgt über den Dateimodus INPUT. In Quelltext 16.1 wurde eine Datei in diesem Lesemodus geöffnet.

Wenn Sie schreibend auf die Datei zugreifen wollen, verwenden Sie den Dateimodus OUTPUT. Auch hier wird der Datenzeiger an den Anfang der Datei gesetzt. Beim Schreiben rückt er weiter, sodass die geschriebenen Daten hintereinander in der Datei zu liegen kommen. Im Gegensatz zu INPUT, das sinnvollerweise nur aus bereits existierenden Dateien lesen kann, wird bei OUTPUT die Datei ggf. neu angelegt. Öffnen Sie allerdings eine bestehende Datei im Dateimodus OUTPUT, wird ihr bisheriger Inhalt gelöscht.

Warning Achtung:
Wenn Sie eine Datei im Dateimodus OUTPUT öffnen, spielt es keine Rolle, ob Sie tatsächlich Daten schreiben oder nicht — bestehende Daten werden auf jeden Fall gelöscht. Wenn Sie eine Datei zum Schreiben öffnen, ohne Daten zu schreiben, haben Sie nach Programmende eine leere Datei vorliegen.

Als dritten sequentiellen Modus gibt es dann noch APPEND. Hierbei öffnen Sie die Datei ebenfalls zum Schreiben, der Dateizeiger wird jedoch ans Ende der Datei gesetzt. Wenn die Datei zuvor nicht existiert hat, arbeitet APPEND genauso wie OUTPUT. Wenn Sie jedoch eine bereits bestehende Datei öffnen, wird ihr Inhalt nicht gelöscht, sondern die Ausgabe an die bisherigen Inhalte angehängt. APPEND eignet sich damit hervorragend dazu, eine fortlaufende Log-Datei zu schreiben.

Und wenn Sie aus einer Datei sowohl lesen als auch in sie hineinschreiben wollen?
Für diese Vorgehensweise sind sequentielle Zugriffe nicht gedacht. Stattdessen können Sie die Datei zuerst lesend öffnen, alle Inhalte in den Speicher laden und die Datei wieder schließen. Anschließend öffnen Sie die Datei schreibend. Sie können stattdessen natürlich auch mit einer temporären Ausgabedatei arbeiten. Wenn Sie gleichzeitig an verschiedenen Stellen lesen und schreiben wollen, empfiehlt sich der Einsatz des wahlfreien Zugriffs.

16.1.2.2 Random-Access und BINARY

Ein sequentieller Zugriff ist sehr praktisch, wenn eine komplette Datei in den Speicher gelesen werden soll, nicht jedoch, wenn Sie in einer sehr großen Datei (etwa einer Datenbank) gezielt auf bestimmte Einträge zugreifen wollen (z. B. auf den Datensatz Nr. 2448). Für solche Anwendungsfälle wurde der Random-Access-Zugriff eingeführt, der aber in FreeBASIC vollständig durch den wahlfreien Zugriff über BINARY ersetzt werden kann.

Der Gedanke hinter der Zugriffsart RANDOM ist die Einteilung der Daten in gleich große Blöcke. Nehmen wir an, Sie wollen eine Datenbank anlegen, in der Vor- und Nachname, Wohnort und Telefonnummer Ihrer Kunden festgehalten werden. In der Regel werden Sie dazu ein UDT mit den entsprechenden Attributen anlegen. Dieses UDT besitzt eine bestimmte Größe, sagen wir 120 Byte. Diese Größe wird beim OPEN-Befehl als Record-Länge hinter dem Schlüsselwort LEN angegeben — RANDOM ist der einzige Dateimodus, in dem LEN sinnvoll eingesetzt werden kann.

Über Random-Access können Sie nun direkt den 2448. Eintrag auslesen oder aber den 731. Eintrag überschreiben. Das Programm springt dazu selbständig an die richtige Stelle ((2448-1)·120 bzw. (731-1)·120) und nimmt den Lese- bzw. Schreibzugriff vor. Gerade um Datensätze mitten in der Datei zu schreiben, ist Random-Access deutlich besser geeignet als der sequentielle Zugriff — bei letzterem müsste man sämtliche Daten einlesen und anschließend (mit Veränderung) neu schreiben.

Trotzdem gilt Random-Access inzwischen als veraltet und wird kaum noch eingesetzt. Grund dafür ist die mangelnde Flexibilität: Jeder Datensatz benötigt genau die gleiche Größe. Es ist damit auch nicht möglich, mehrere verschiedenartige Datensätze gemeinsam in einer Datei zu speichern. Als deutlich bessere Alternative dient der Dateimodus BINARY. Mit ihm lassen sich zum einen sämtliche RANDOM-Zugriffe komplett ersetzen, zum anderen ermöglicht er aber auch einen tatsächlich wahlfreien Zugriff auf jede beliebige Stelle innerhalb der Datei.

16.1.2.3 Vergleich der Dateimodi

Bei der Entscheidung, welchen Dateimodus man benutzen will, gibt es an sich nur eine Frage: Wollen Sie einen sequentiellen Dateizugriff nutzen oder einen wahlfreien Zugriff? Wenn Sie sich für einen sequentiellen Zugriff entscheiden, ergibt sich der benötigte Dateimodus von selbst, je nachdem, ob Sie lesen, (neu) schreiben oder an bestehende Daten anhängen wollen. Bei einem wahlfreien Zugriff sollten Sie auf jeden Fall BINARY verwenden, denn RANDOM bietet keine Vorteile, die Sie mit BINARY nicht genauso nutzen können (außer vielleicht, dass man es „von früher so gewohnt“ ist).

Sequentielle Dateimodi bieten sich vor allem dann an, wenn Sie eine Datei zeilenweise einlesen oder schreiben wollen — also wenn die Daten durch Zeilenumbrüche getrennt sind und Sie im Vorfeld nicht wissen, wie lang die einzelnen Zeilen sein werden. Auch das Komma dient in der Regel als Trennzeichen zwischen den einzelnen Daten. Aber auch hier können die Daten unterschiedlich lang sein. Da Sie mit BINARY die exakte Stelle ansteuern können, von der aus Sie lesen bzw. schreiben wollen, müssen Sie dazu natürlich diese exakten Stellen kennen, was bei verschieden langen Zeilen nicht der Fall ist. Im Grunde genommen ist jede Art von textbasierter Datei (CSV-Dateien, FreeBASIC-Quellcodes …) ein gutes Anwendungsziel für einen sequentiellen Dateimodus.

Für Binärdateien sind sequentielle Modi dagegen in der Regel ungeeignet. Ein Binärformat können Sie z. B. wählen, damit die Daten von außen nicht so ohne Weiteres sichtbar sind, oder aber, um die Informationen deutlich kompakter zu speichern. Und selbstverständlich liegen auch Bilder, Audiodateien usw. üblicherweise nicht als Textdatei vor.2 Wenn Sie also z. B. die Abmessungen eines BMP-Bildes auslesen wollen, ist das Öffnen mittels BINARY die einzig sinnvolle Möglichkeit. Quelltext 16.7 wird ein solches Auslesen demonstrieren.

16.1.3 Zugriffsart beschränken

Während in den Dateimodi INPUT, OUTPUT und APPEND jeweils nur der Lese- bzw. nur der Schreibzugriff erlaubt ist, können mit RANDOM oder BINARY geöffnete Dateien sowohl ausgelesen als auch beschrieben werden. Manchmal ist es praktisch, die Zugriffsmöglichkeiten einzuschränken und beispielsweise nur das Lesen zu erlauben. Dazu dient das Schlüsselwort ACCESS.

Quelltext 16.2: Dateizugriff auf das Lesen beschränken
DIM AS INTEGER dateinummer = FREEFILE
IF OPEN("test.txt" FOR BINARY ACCESS READ AS #dateinummer) THEN
  ' aus der Datei kann jetzt nur gelesen werden
END IF

Anstelle von READ ist auch WRITE (nur Schreibzugriff) und READ WRITE (Lese- und Schreibzugriff; Standard) möglich. Auswirkung hat diese Angabe nur in den Dateimodi RANDOM und BINARY.

16.1.4 Datei für andere Programme sperren

Die Syntax von OPEN sieht mit dem Schlüsselwort LOCK die Möglichkeit vor, eine Datei während des Öffnens für andere Programme zum Lesen und/oder Schreiben zu sperren. Diese Sperre funktioniert jedoch nicht wie vorgesehen, und es scheint auch auf Dauer so zu bleiben. Daher wird LOCK hier nur der Vollständigkeit halber erwähnt.

Tip Hinweis für Experten:
Wenn Sie mehrere Programme haben, die auf dieselbe Datei zugreifen, können Sie eine zusätzliche Sperrdatei verwenden, um gleichzeitigen Zugriff zu vermeiden: Das Programm, welches auf die Datei zugreifen will, muss zuvor die Sperrdatei umbenennen; schlägt das Umbenennen fehl, dann hat bereits ein anderes Programm den Dateizugriff gestartet. Nach Beendigung des Zugriffs wird die Datei dann wieder zurückbenannt, und ein anderes Programm kann sich den Zugriff sichern. Ein Problem beim Einsatz von Sperrdateien ist, dass bei einem Absturz des sperrenden Programmes die angelegte Sperre dauerhaft erhalten bleibt und händisch behoben werden muss. Wenn der Benutzer (der ja nicht der Programmierer sein muss) mit der Sperrpraxis nicht vertraut ist, wird dies voraussichtlich für viel Frust sorgen.

16.1.5 Text-Encoding

Zur Unterstützung von Unicode kann beim Öffnen sequentieller Dateien angegeben werden, in welcher Codierung die Datei vorliegt. FreeBASIC speichert einen WSTRING je nach Plattform in UCS-2 bzw. UCS-4 (siehe Kapitel 6.3.1), und eine Übertragung zwischen Dateiformat und FreeBASIC-internem Format benötigt eine korrekte Umwandlung. Wenn Sie beim Öffnen die richtige Zeichenkodierung angeben, nimmt FreeBASIC die Umwandlung automatisch vor. Da dieses Verfahren nur bei sequentiellem Zugriff sinnvoll ist, kann ENCODING im Dateimodus RANDOM und BINARY nicht eingesetzt werden.

Folgende UTF-Codierungen werden unterstützt:

  • "ASCII"

  • "UTF-8"

  • "UTF-16"

  • "UTF-32"

Damit die Datei erfolgreich geöffnet werden kann, muss das Byte Order Mark (BOM) gesetzt sein. Das BOM wird in UTF-16- und UTF-32-codierten Dateien benötigt, um die Byte-Reihenfolge (Big-Endian oder Little-Endian) zu kennzeichnen. In UTF-8 steht die Byte-Reihenfolge zwar fest, FreeBASIC braucht das BOM aber dennoch. Ist das BOM nicht enthalten, wird beim Versuch, die Datei im UTF-Format zu öffnen, der Laufzeitfehler 2 (File not found) zurückgegeben.

Tip Unterschiede zu QuickBASIC:
Unicode wird in QuickBASIC nicht unterstützt. In der Dialektform -lang qb steht ENCODING nicht zur Verfügung.

16.1.6 Datei(en) schließen

Wenn alle Lese- oder Schreibvorgänge beendet sind, muss die Datei wieder geschlossen werden. Tatsächlich werden die Daten in der Regel erst dann physikalisch auf den Datenträger geschrieben, wenn die Datei geschlossen wird. Der dazugehörige Befehl lautet CLOSE, gefolgt von der Dateinummer oder den Dateinummern der gewünschten Dateien.

Quelltext 16.3: Dateien schließen
' Oeffne zwei Dateien: eine mit Lese-, eine mit Schreibzugriff
DIM AS INTEGER dateinummerEin, dateinummerAus
' erste freie Dateinummer ermitteln
dateinummerEin = FREEFILE
IF OPEN("eingabe.txt" FOR INPUT AS #dateinummerEin) <> 0 THEN
  ' Oeffnen fehlgeschlagen - Programm beenden
  PRINT "eingabe.txt konnte nicht geoeffnet werden."
  END
END IF
' naechste freie Dateinummer ermitteln (NACHDEM die erste verwendet wurde)
dateinummer2 = FREEFILE
IF OPEN("ausgabe.txt" FOR OUTPUT AS #dateinummer) <> 0 THEN
  ' Oeffnen fehlgeschlagen - Programm beenden
  PRINT "ausgabe.txt konnte nicht geoeffnet werden."
  ' Die Eingabedatei ist noch geoeffnet und wird nun geschlossen.
  ' Die Ausgabedatei wurde ja nicht erfolgreich geoeffnet.
  CLOSE #dateinummerEin
  END
END IF

' Dateien konnte erfolgreich geoeffnet werden. Es folgt das Hauptprogramm.
' Am Ende werden beide Dateien geschlossen:
CLOSE #dateiEin, #dateiAus

Achtung: Eine ggf. bereits existierende Datei ausgabe.txt wird durch dieses Programm überschrieben!

In der letzten Zeile sehen Sie, wie mehrere Dateien gleichzeitig geschlossen werden können. Das Rautezeichen vor den Dateinummern ist auch hier optional. Alternativ kann CLOSE auch ohne Parameter verwendet werden, um sämtliche noch offenen Dateien zu schließen. Dazu müssen Sie aber sicher sein, dass keine von anderen Programmteilen geöffnete Dateien mehr existieren, die noch benötigt werden.

Nach dem Schließen einer Datei ist die von ihr verwendete Dateinummer wieder frei und kann erneut eingesetzt werden. Schon aus diesem Grund empfiehlt es sich, Dateien nicht länger als notwendig geöffnet zu halten.

Beim Programmende werden alle noch geöffneten Dateien automatisch geschlossen. Trotzdem sollten Sie Dateien immer explizit schließen. Das gehört zum guten Programmierstil und verhindert, dass Sie im entscheidenen Moment ein notwendiges CLOSE vergessen. Wenn Sie innerhalb einer häufig aufgerufenen Prozedur Dateien öffnen und nicht wieder schließen, werden Sie bald an die Grenze der maximal 255 gleichzeitig geöffneten Dateien stoßen, und ein weiteres Öffnen wird scheitern. Wenn Sie sich von vorn herein ein explizites CLOSE angewöhnen, können Sie solche Fehler vermeiden.

16.2 Lesen und Schreiben sequentieller Daten

Lesen und Schreiben sequentieller Daten funktioniert nahezu genauso wie beim Lesen von der Tastatur bzw. das Schreiben in das Ausgabefenster. Der einzige Unterschied ist, dass die Dateinummer mit angegeben werden muss.

16.2.1 PRINT# und WRITE#

Mit PRINT# können Dateien sequentiell beschrieben werden. Der Befehl folgt dabei denselben Regeln wie PRINT (siehe Kapitel 4.1), einschließlich der Komma- und Strichpunkt-Regeln. Auch TAB wird unterstützt. Die Ausgabe in der Datei entspricht genau dem, was Sie sonst im Programmfenster erwarten würden.

PRINT# eignet sich vor allem dann, wenn das Lesen der gespeicherten Daten über den Texteditor angenehm gestaltet werden soll, z. B. wenn Sie eine schön formatierte Ausgabe erreichen wollen (etwa eine tabellarische Darstellung) oder wenn ein spezielles textbasiertes Format, etwa HTML, genutzt werden soll. Wenn Ihr vorrangiges Ziel dagegen ist, die geschriebenen Daten mit geringem Aufwand wieder einzulesen, ist WRITE# möglicherweise die bessere Wahl. Hier werden die Parameter durch Kommata getrennt (ein Semikolon ist nicht zulässig), und diese Kommata werden auch in die Datei geschrieben. Außerdem werden Strings mit Anführungszeichen umgeben.

Quelltext 16.4: Vergleich zwischen PRINT# und WRITE#
DIM AS INTEGER dateinummer = FREEFILE
DIM AS STRING txt = "b"
OPEN "ausgabe.txt" FOR OUTPUT AS #dateinummer
PRINT #dateinummer, 1; 2, "a"; txt
WRITE #dateinummer, 1, 2, "a", txt
CLOSE #dateinummer
END

Für beide Befehle ist das Rautezeichen notwendig, da sonst nicht zwischen der Datei-Eingabe und der Tastatur-Eingabe unterschieden werden könnte. Aus diesem Grund werden wir bei der Datei-Ausgabe auch von PRINT# und WRITE# sprechen, auch wenn der Befehl tatsächlich nur PRINT bzw. WRITE lautet. In der Datei ausgabe.txt steht nun folgendes:

Ausgabe:
 1 2          ab
1,2,"a","b"

Beachten Sie bei der Dateiausgabe in der ersten Zeile die Auswirkung des Strichpunktes (direktes Aneinanderhängen) und des Kommas (Einrückung). Bei WRITE# werden die Daten, durch Komma getrennt, ohne Einrückungen direkt aneinandergehängt. WRITE# eignet sich daher nicht dazu, eine schön formatierte Ausgabe zu erhalten, sondern hat einen rein praktischen Nutzen. Die Daten können nämlich mit INPUT# problemlos wieder einzeln eingelesen werden.

Note Hinweis:
WRITE existiert auch als normaler Schreibbefehl im Programmfenster, hat dort aber keinen großen Nutzen.

16.2.2 INPUT#, LINE INPUT#

Auch INPUT# und LINE INPUT# funktionieren grundsätzlich genauso wie die in Kapitel 5.1 behandelten Befehle zur Tastatureingabe. Wenn Sie die in Quelltext 16.4 gespeicherten Daten wieder einlesen wollen, können Sie folgendermaßen vorgehen:

Quelltext 16.5: INPUT-Methode bei Dateien
DIM AS INTEGER dateinummer = FREEFILE
DIM AS INTEGER wert1, wert2
DIM AS STRING zeile1, wert3, wert4
OPEN "ausgabe.txt" FOR INPUT AS #dateinummer
LINE INPUT #dateinummer, zeile1                 ' gesamte Zeile einlesen
INPUT #dateinummer, wert1, wert2, wert3, wert4  ' Einzelwerte einlesen
CLOSE #dateinummer
PRINT "erste Zeile: """; zeile1; """"
PRINT "wert1 = "; wert1
PRINT "wert2 = "; wert2
PRINT "wert3 = """; wert3; """"
PRINT "wert4 = """; wert4; """"
SLEEP
Ausgabe:
erste Zeile: " 1 2          ab"
wert1 =  1
wert2 =  2
wert3 = "a"
wert4 = "b"

INPUT# folgt denselben Begrenzungsregeln durch Kommata. Wenn Sie also eine Zeile einlesen wollen, die ein Komma beinhaltet, wird der Lesevorgang vor dem Komma beendet.3 Das kann so gewollt sein; wenn Sie aber Textdateien zeilenweise einlesen wollen, weichen Sie besser auf LINE INPUT# aus.

16.2.3 INPUT() und WINPUT() für Dateien

Auch die Funktionsversion INPUT() zum Auslesen einer festgelegten Zeichenzahl kann auf eine zum Lesen geöffnete Datei angewendet werden. Dazu muss allerdings die Dateinummer als zweiter Parameter angegeben werden (anstatt als erster), und es darf ihr auch kein Hash vorangestellt werden. Wenn Sie dagegen aus einer Unicode-codierten Datei lesen wollen, verwenden Sie stattdessen WINPUT().

Quelltext 16.6: INPUT() bei Dateien
DIM AS INTEGER dateinummer = FREEFILE
DIM AS STRING einlesen
OPEN "ausgabe.txt" FOR INPUT ENCODING "UTF-8" AS #dateinummer
PRINT "Die ersten fuenf Zeichen der Datei lauten:"
einlesen = WINPUT(5, dateinummer)
PRINT """" & einlesen & """"
CLOSE #dateinummer
SLEEP

Denken Sie daran, dass die Datei nur dann korrekt mit ENCODING "UTF-8" (oder anderen Unicode-Arten) geöffnet werden kann, wenn in der Datei das BOM gesetzt ist.

Falls Sie sich übrigens fragen, was in der sechsten Zeile die vier aufeinanderfolgenden Anführungszeichen bedeuten sollen: Auf diese Art und Weise können Sie ein Anführungszeichen auf der Konsole ausgeben. Das vorderste und hinterste Anführungszeichen der Vierergruppe begrenzt den String, und die beiden mittleren werden zusammen als ein Anführungszeichen interpretiert.

16.3 Zugriff auf binäre Daten

Binäre Dateien sind auf der einen Seite deutlich flexibler und kompakter, auf der anderen Seite erfordern sie aber eine deutlich bessere Planung. Im Gegensatz zum sequentiellen Zugriff lesen und speichern Sie hier keine Datenzeilen, sondern direkt einzelne Datentypen. Daher ist es notwendig, zu wissen, welche Daten sich wo befinden.

16.3.1 Binäre Daten speichern

Zum Speichern binärer Daten verwenden wir den Befehl PUT#:

PUT #dateinummer, [position], daten [, menge]
  • dateinummer ist wieder die Nummer, die der Datei mit OPEN zugewiesen wurde.

  • position gibt die Stelle an, an die geschrieben werden soll. Der Parameter kann ausgelassen werden. Dann schreibt der Befehl an der Stelle, an der sich der Dateizeiger gerade befindet. Direkt nach dem Öffnen einer Datei ist das die Position +1+.

  • daten enthält den zu schreibenden Wert. In der Regel wird es sich dabei um eine Variable oder einen Pointer handeln, denn hier ist es wichtig, dass der Datentyp klar erkennbar ist. PUT# benötigt eine exakte Angabe über die Länge der geschriebenen Daten, also die Größe des Datentyps.
    Auch Strings sind möglich. Die Datenlänge berechnet sich dann über die Länge des Strings. Zahlenwerte oder mathematische Berechnungen funktionieren dagegen nicht.

  • Handelt es sich bei daten um einen Pointer, dann kann mit menge angegeben werden, wie viele Elemente des entsprechenden Datentyps geschrieben werden sollen. Sie können damit in einem ALLOCATE-Puffer z. B. zehn INTEGER hintereinander ablegen und diese zehn Werte dann in einem Rutsch in die Datei schreiben. Ohne Angabe von menge wird immer ein einzelner Wert geschrieben. Wenn Sie ein Array schreiben, gibt menge an, wie viele Einträge geschrieben werden sollen.

PUT# existiert auch als Funktion. Dann muss die Parameterliste mit Klammern umgeben werden; der Rückgabewert ist eine FreeBASIC-Fehlernummer (oder 0, wenn kein Fehler aufgetreten ist).

Die folgenden Beispiele dienen nur zur Demonstration der Syntax; der Code ist in dieser Form nicht lauffähig. Insbesondere kann der Pointer-Zugriff nicht ohne Reservierung des Speicherplatzes stattfinden.

DIM AS STRING   varlength = "123"
DIM AS STRING*5 fixlength = "456"
DIM AS SHORT    shortVal, array(10)
DIM AS LONG PTR longPtr
PUT #dateinummer,, varlength            ' schreibt 3 Byte (aktuelle Stringlaenge)
PUT #dateinummer,, fixlength            ' schreibt 5 Byte (feste Stringlaenge)
PUT #dateinummer, 8, shortVal           ' schreibt 2 Byte ab Position 8
PUT #dateinummer,, CAST(LONG, shortVal) ' schreibt 4 Byte (Laenge eines LONG)
PUT #dateinummer,, shortVal+1           ' schreibt ein INTEGER!!!
PUT #dateinummer,, 123                  ' funktioniert nicht (Datentyp unklar)
PUT #dateinummer,, CAST(LONG, 123)      ' funktioniert auch nicht (trotz CAST)
PUT #dateinummer,, *longPtr             ' schreibt 4 Byte (Laenge von LONG)
PUT #dateinummer,, *longPtr, 5          ' schreibt 20 Byte (5 LONG-Werte)
PUT #dateinummer,, array(3), 4          ' schreibt die Werte array(3) bis array(6)

Nicht alle Speichergrößen sind intuitiv. Verwenden Sie auf jeden Fall keine Rechenausdrücke, oder setzen Sie sie in ein CAST, um den Datentyp sicherzustellen. Auch bei Strings variabler Länge müssen Sie aufpassen, damit Sie beim Einlesen wieder die richtige Länge verwenden.

Besonders praktisch ist, dass Sie mit PUT# auch komplette UDTs schreiben können. Es gibt dabei jedoch eine Einschränkung: Strings variabler Länge funktionieren genauso wenig wie Pointer-Inhalte. Der Grund dafür ist ganz einfach: Ein Pointer selbst ist lediglich ein INTEGER-Wert mit der Adresse auf den Inhalt. Diesen zu speichern bringt nichts, da der tatsächliche Inhalt verloren geht. Bei Strings variabler Länge ist es dasselbe, denn auch dieser verweist nur auf den tatsächlichen String-Inhalt. Wenn Sie im UDT aber nur Zahlenwerte und Strings fester Länge verwenden, ist ein Speichern und Laden des gesamten UDTs problemlos möglich. Ein Beispiel dafür finden Sie in Quelltext 16.8.

Note Hinweis:
Im Grafikmodus gibt es ebenfalls einen Befehl PUT (und GET), der in diesem Fall Bilddaten auf den Bildschirm schreibt (bzw. davon liest). Siehe dazu [KapGrafikPUT]

16.3.2 Binäre Daten einlesen

Der Lesebefehl GET# ist bis auf einen weiteren optionalen Parameter genauso aufgebaut wie PUT# und kann ebenfalls als Funktion eingesetzt werden.

GET #dateinummer, [position], daten [, menge[, gelesen]]

gelesen ist eine Variable, in der gespeichert wird, wie viele Byte tatsächlich eingelesen wurden. Damit können Sie überprüfen, ob alle Daten korrekt gelesen wurde oder ob versucht wurde, hinter dem Dateiende zu lesen.

Als Beispiel wollen wir die Abmessungen eines BMP-Bildes auslesen. Wie viele andere Dateitypen besitzt BMP einen Header, in dem u. a. Informationen zu Bildabmessung, Auflösung, Farbtiefe, Komprimierung usw. gespeichert sind. Diese Daten besitzen eine feste Position innerhalb der Datei und können daher gezielt angesteuert werden.

Quelltext 16.7: Bildabmessungen eines BMP auslesen
DIM AS LONG breit, hoch, dateinummer = FREEFILE
OPEN "testbild.bmp" FOR BINARY ACCESS READ AS #dateinummer
GET #dateinummer, 19, breit                  ' Breite aus der BMP-Datei auslesen
GET #dateinummer, 23, hoch                   ' Hoehe aus der BMP-Datei auslesen
CLOSE #dateinummer                           ' schliesse die Datei
PRINT "Das Bild ist"; breit; "px breit und"; ABS(hoch); "px hoch."
SLEEP

Hinweis: Die Höhe des Bildes kann (selten) negativ angegeben sein, um damit eine Datenspeicherung von oben nach unten (statt von unten nach oben) zu kennzeichnen.

Die Verwendung des richtigen Datentyps ist von entscheidender Bedeutung. In Quelltext 16.7 beispielsweise muss ein LONG (bzw. ein INTEGER<32>) verwendet werden; ein INTEGER würde in einem 64-Bit-System zu falschen Werten führen, da es sich dort um ein INTEGER<64> und damit um einen zu großen Datentyp handelt.

Warning Achtung:
Wenn Ihr Programm sowohl unter 32-Bit- als auch 64-Bit-Rechnern verwendet werden kann, sollten Sie beim Dateizugriff den Datentyp INTEGER unbedingt vermeiden und stattdessen LONG, INTEGER<32>, INTEGER<64> o. ä. verwenden.

Sehen wir uns zum Thema Datentyp-Größen noch folgendes Beispiel an:

DIM AS USHORT shortVarEin = 6440, shortVarAus
DIM AS UBYTE  byteVarEin  =   86, byteVarAus 
DIM AS INTEGER dateinummer = FREEFILE
OPEN "test.dat" FOR BINARY AS #dateinummer
' in der Reihenfolge USHORT - UBYTE schreiben
PUT #dateinummer, 1, shortVarEin
PUT #dateinummer,,   byteVarEin
' in der Reihenfolge UBYTE - USHORT lesen
GET #dateinummer, 1, byteVarAus
GET #dateinummer,,   shortVarAus
' und auf dem Bildschirm ausgeben
PRINT shortVarAus, byteVarAus
SLEEP
Ausgabe:
22034         52

Was genau schiefgelaufen ist, sieht man, wenn man sich die Hexadezimalwerte ansieht:

PRINT HEX(4660),  HEX(86)     ' Werte der Eingabe
PRINT HEX(22034), HEX(52)     ' Werte der Ausgabe
SLEEP
Ausgabe:
1234          56
5612          34

Gespeichert wurde erst das USHORT &h1234 (zwei Byte) — und zwar, da es sich um ein Little-Endian-System4 handelt, in der Reihenfolge &h34 &h12 — und dann das UBYTE &h56. In der Datei steht nun die Bytefolge &h341256. Beim Auslesen wird erst ein UBYTE extrahiert, nämlich &h34, und als zweites ein USHORT mit der Byte-Reihenfolge &h12 &h56. Das USHORT wird, wieder wegen des Little-Endian-Systems, als &h5612 zusammengesetzt.
Hätten Sie den Inhalt der Datei in einen ausreichend langen STRING gelesen, wären die Byte-Werte als ASCII-Codes interpretiert worden (in diesem Fall als CHR(&h34, &h12, &h56)).

Auch die Verwendung eigener UDTs soll kurz demonstriert werden. Wir verwenden ein UDT mit Unter-UDT, denn auch das stellt keine Schwierigkeit dar, solange in keiner der Ebenen Strings variaber Länge oder Pointer vorkommen.

Quelltext 16.8: UDT speichern und auslesen
TYPE TypKoordinaten
  AS INTEGER x, y
END TYPE
TYPE TypTest
  AS UBYTE          id
  AS TypKoordinaten position
END TYPE

DIM AS TypTest testfeld(1 TO 4), testwert
DIM AS INTEGER dateinummer = FREEFILE

' testfeld() mit Daten befuellen
FOR i AS INTEGER = 1 TO 4
  testfeld(i).id = i
  testfeld(i).position.x = 10
  testfeld(i).position.y = 5 + i*2
NEXT

' testfeld() speichern
OPEN "test.dat" FOR BINARY AS #dateinummer
PUT #dateinummer,, testfeld(1), 4

' dritten Datensatz auslesen und ausgeben
GET #dateinummer, 1 + 2*SIZEOF(TypTest), testwert
PRINT testwert.id, testwert.position.x, testwert.position.y
CLOSE #dateinummer
SLEEP
Ausgabe:
3              10            11

Sie sehen, dass Sie in eine mit BINARY geöffnete Datei sowohl schreiben als auch aus ihr lesen können, was insbesondere die Verwendung mit Datensätzen sehr bequem macht. Noch eine kurze Erläuterung zur GET#-Zeile: Der erste Datensatz startet an Position 1 und benötigt SIZEOF(TypTest) Byte an Speicherplatz. Dementsprechend startet der zweite Datensatz an Position 1+SIZEOF(TypTest) und der dritte bei 1+2*SIZEOF(TypTest). Zur Erinnerung: SIZEOF() gibt die Größe einer Variablen oder eines Datentyps an.

16.3.3 Binäre Speicherung von Strings variabler Länge

Um eine Zeichenkette korrekt aus der Datei auszulesen, müssen Sie ihre Länge kennen. Und ebenso wichtig: Das Programm muss wissen, wie lang der auszulesende String sein soll. Das können Sie auf zwei Wegen erreichen:

  • durch die Verwendung von Strings fester Länge: Die Lesemenge ist durch die Größe des Datentyps festgelegt.

  • bei Strings variabler Länge: Auch hier wird die Lesemenge durch die Länge des Strings bestimmt. Vor dem Lesevorgang müssen Sie daher den String auf die richtige Länge bringen.

Die Länge des Strings wird nicht automatisch mitgespeichert. Bei variabler Länge sind Sie daher selbst dafür verantwortlich, die Länge zu speichern. Ich empfehle dazu, erst die Länge als Zahlenwert zu speichern und dann den String dahinter abzulegen. Beim Lesevorgang lesen Sie dann wieder erst die Länge aus, präparieren dann die Stringvariable so, dass sie die richtige Länge besitzt, und lesen den String ein.

Quelltext 16.9: Strings variabler Länge speichern und laden
DIM AS STRING speicherstring = "Hallo Welt!", ladestring
DIM AS LONG speicherlaenge, ladelaenge
DIM AS INTEGER dateinummer = FREEFILE
OPEN "test.dat" FOR BINARY AS #dateinummer
' Laenge und Stringinhalt speichern
speicherlaenge = LEN(speicherstring)
PUT #dateinummer,, speicherlaenge
PUT #dateinummer,, speicherstring
' Laenge und Stringinhalt lesen
GET #dateinummer, 1, ladelaenge       ' Laenge lesen
ladestring = SPACE(ladelaenge)        ' ladestring auf die richtige Laenge bringen
GET #dateinummer,, ladestring         ' String lesen
PRINT ladestring                      ' Testausgabe
SLEEP

In Zeile 11 wird in ladestring eine Zeichenkette aus der benötigten Anzahl an Leerzeichen erzeugt. Das ist sehr effektiv, aber lediglich eine von mehreren Möglichkeiten. Wichtig ist nur, dass ladestring vor dem Lesevorgang die richtige Länge hat.

16.3.4 Position des Dateizeigers

Die Position des Dateizeigers können Sie jederzeit mit SEEK setzen oder lesen. Im ersten Fall wird SEEK als Prozedur eingesetzt, im zweiten Fall SEEK() als Funktion.

Quelltext 16.10: Dateizeiger setzen und lesen
DIM AS INTEGER dateinummer = FREEFILE
OPEN "test.dat" FOR BINARY AS #dateinummer
' Zeiger setzen
SEEK #dateinummer, 100
' Beim Schreiben wird die Position veraendert.
PUT #dateinummer,, "Testeingabe"
' neue Position lesen und ausgeben
PRINT "Zeigerposition:"; SEEK(dateinummer)
CLOSE #dateinummer
SLEEP
Ausgabe:
Zeigerposition: 111

Das erste Setzen des Zeigers hätte natürlich auch über den zweiten Parameter von PUT erfolgen können. Oft ist es programmiertechnisch jedoch sinnvoller, zunächst die gewünschte Ausgangsposition herzustellen und anschließend die Schreibvorgänge durchzuführen.

Eine weitere Funktion zum Ermitteln (jedoch nicht zum Setzen) des Dateizeigers ist LOC(). Diese gibt nicht die aktuelle Position an, sondern das zuletzt gelesene Element. Bei Dateien, die mit BINARY geöffnet wurden, ist daher der Rückgabewert von LOC() immer um genau 1 kleiner als SEEK(). Bei mit RANDOM geöffneten Dateien wird der zuletzt gelesene Datensatz zurückgegeben, und bei sequentiellen Dateien nimmt der Compiler eine Datensatzlänge von 128 Byte an (LOC(dateinummer) = (SEEK(dateinummer)-1)\128.) Allein schon wegen dieser eher unhandlichen Berechnungsformel sollten Sie auf LOC() verzichten und stattdessen SEEK() verwenden. LOC() dient lediglich der Rückwärtskompatibilität zu QuickBASIC.

Dem aufmerksamen Leser sollte nicht entgangen sein, dass vor dem Parameter dateinummer in der Funktion SEEK() kein Rautenzeichen steht. Aufgrund der Compiler-Syntax können innerhalb von Funktionen keine Parameter mit Rautezeichen eingesetzt werden. Dies betrifft auch LOC() und die im Folgenden aufgeführten Funktionen EOF() und LOF(), aber auch selbst definierten Unterprogramme. OPEN, CLOSE, GET#, INPUT# usw. werden beim Compiliervorgang speziell behandelt.

16.4 Dateiende ermitteln

Insbesondere dann, wenn Sie eine komplette Datei auslesen wollen, brauchen Sie eine Möglichkeit, das Ende der Datei zu ermitteln. Dazu stehen Ihnen zwei Alternativen zur Verfügung.

16.4.1 End Of File (EOF)

Die klassische Variante für sequentielle Daten ist EOF(). Diese Funktion gibt zurück, ob das Dateiende erreicht wurde (-1) oder nicht (0). Auch wenn der Dateizeiger mit SEEK hinter das Dateiende gesetzt wurde, liefert EOF() -1 zurück.

Besonders interessant ist EOF() im Zusammenhang mit dem Dateimodus INPUT. Bei den Dateimodi OUTPUT und APPEND gibt EOF() immer -1 zurück, unabhängig von der tatsächlichen Position des Zeigers.

Quelltext 16.11 demonstriert das Einlesen einer kompletten sequentiellen Datei. Wenn Sie den Quellcode unter dem Namen test.bas speichern, können Sie damit genau diesen Quellcode auf dem Bildschirm ausgeben.

Quelltext 16.11: Sequentielle Datei vollständig einlesen
DIM AS STRING zeile
DIM AS INTEGER dateinummer = FREEFILE
OPEN "test.bas" FOR INPUT AS #dateinummer

DO UNTIL EOF(dateinummer)           ' solange das Dateiende nicht erreicht wurde
  LINE INPUT #dateinummer, zeile    ' eine Zeile einlesen ...
  PRINT zeile                       ' ... und auf dem Bildschirm ausgeben
LOOP
CLOSE #dateinummer
SLEEP

16.4.2 Length Of File (LOF)

EOF() kann zwar auch bei mit BINARY geöffneten Dateien eingesetzt werden, allerdings bietet sich dort LOF() in der Regel eher an. LOF() gibt die Länge der geöffneten Datei in Byte an, und vor dem Zugriff auf die gewünschten Daten kann dann überprüft werden, ob der benötigte Datenbereich überhaupt innerhalb der Datei liegt.

Quelltext 16.12: Datensatz aus einer binären Datei einlesen
TYPE TypDatensatz
  AS LONG      id
  AS STRING*10 inhalt
END TYPE
DIM AS TypDatensatz datensatz
DIM AS INTEGER p = 5

DIM AS INTEGER dateinummer = FREEFILE
OPEN "test.dat" FOR BINARY AS #dateinummer
' versuche, Datensatz an Position p zu lesen
IF LOF(dateinummer) >= p*SIZEOF(TypDatensatz) THEN
  ' Die Datei ist lang genug, um den Datensatz 'p' vollstaendig zu enthalten
  GET #dateinummer, 1+(p-1)*SIZEOF(TypDatensatz), datensatz
ELSE
  ' Die Datei ist zu kurz - Datensatz 'p' ist nicht enthalten
  PRINT "Fehler: Datensatz konnte nicht gefunden werden."
END IF
CLOSE #dateinummer
SLEEP

Zur Erläuterung: Wie schon in Quelltext 16.8 beginnt Datensatz 1 an der Stelle 1 und ist SIZEOF(TypDatensatz) lang (hier 16 Byte; siehe auch Kapitel 7.3). Der fünfte Datensatz beginnt dementsprechend an der Stelle 1+(5-1)*SIZEOF(TypDatensatz). Ist die Datei kürzer als 5*SIZEOF(TypDatensatz), dann ist der fünfte Datensatz nicht vorhanden oder unvollständig. Anstatt zu versuchen, den Datensatz einzulesen — der Lesevorgang schlägt dann fehl und datensatz wird auf den Initialwert gesetzt — fängt das Programm den Fehler mit einer Meldung ab.

16.5 Datei löschen

Zum Entfernen einer Datei aus dem Dateisystem dient der Befehl KILL. Dem Befehl wird eine Zeichenkette übergeben, die den Namen der zu löschenden Datei enthält — einschließlich Dateiendung. So wie OPEN kann auch KILL als Funktion eingesetzt werden und gibt dann im Erfolgsfall 0 und ansonsten eine Fehlernummer zurück.

Achtung: Mit KILL gelöschte Dateien werden nicht in den Papierkorb geschoben, sondern wirklich gelöscht!

Quelltext 16.13: Datei löschen
SELECT CASE KILL("LoeschMich.txt")
  CASE 0 : PRINT "Die Datei wurde geloescht."
  CASE 2 : PRINT "Fehler: Die Datei existiert nicht!"
  CASE 3 : PRINT "Fehler: Unzureichende Zugriffsrechte oder Ordnerzugriff!"
END SELECT
SLEEP

16.6 Standard-Datenströme

Auch die Standard-Datenströme werden wie Dateien behandelt. Sie sind ein unter Unix und Linux allgemein bekanntes Konzept, welches den meisten Windows-Nutzern jedoch eher unbekannt sein dürfte — daher im Vorfeld ein paar kurze Erläuterungen.

16.6.1 Eine kurze Einführung

Insgesamt gibt es drei Standard-Datenströme:

  • die Standardeingabe (stdin): Über sie werden Daten in das Programm eingelesen. Sie ist normalerweise mit der Tastatur verbunden, die Benutzereingaben finden also über die Tastatur statt. stdin hat den Datei-Deskriptor 0. (Zur Frage, wozu der Datei-Deskriptor benötigt wird, kommen wir gleich.)

  • die Standardausgabe (stdout): Über sie gibt das Programm Daten aus. Die Ausgabe erfolgt normalerweise über die Programmkonsole auf dem Bildschirm. (Wenn Sie ein Grafikfenster nutzen, wird dieses als stdout festgelegt.) stdout hat den Datei-Deskriptor 1.

  • die Standardfehlerausgabe (stderr): Über sie werden Status- und Fehlermeldungen ausgegeben. Normalerweise erfolgt auch sie über die Programmkonsole (bzw. über das Grafikfenter). stderr hat den Datei-Deskriptor 2.

Interessant sind die Standard-Datenströme, weil sie auf ein anderes Ein- bzw. Ausgabemedium umgeleitet werden können. Sie können Daten z. B. statt von der Tastatur auch aus einer Datei einspeisen oder umgekehrt die Ausgabe in eine Datei umleiten. Nehmen wir an, Sie haben ein Programm namens test.exe, das von stdin liest, nach stdout ausgibt und Fehlermeldungen nach stderr schreibt. Sie können dieses Programm nun über die Konsole starten:

test.exe

Die Eingabe erfolgt, wie gewohnt, über die Tastatur, und sowohl die Ausgabe als auch die Fehlermeldungen erscheinen im Konsolenfester. Um die Ausgabe in eine Datei umzuleiten, können Sie stattdessen folgendermaßen starten:

test.exe > ausgabe.txt
test.exe >> ausgabe.txt

Die Spitzklammer legt die Ausgabedatei fest. Fehlermeldungen (stderr) werden weiterhin auf der Konsole ausgegeben, die regulären Meldungen (stdout) dagegen in die Datei ausgabe.txt geschrieben. Mit der einfachen Spitzklammer (erste Befehlszeile) legen Sie die Datei neu an und überschreiben die alten Inhalte. Die doppelte Spitzklammer (zweite Befehlszeile) bewirkt, dass die Ausgabe an die bereits bestehende Datei angehängt wird. Wenn Sie die Ausgabe und die Fehlermeldungen getrennt in zwei verschiedene Dateien schreiben wollen, können Sie folgendermaßen vorgehen:

test.exe > ausgabe.txt 2> fehler.txt

Die 2 steht für den Datei-Deskriptor von stderr, gibt also an, dass nicht stdout, sondern stderr in die Datei fehler.txt umgeleitet werden soll. (Sie hätten bei der ersten Spitzklammer auch den Datei-Deskriptor +1+ hinzuschreiben können, aber da das der Standard ist, kann es weggelassen werden. Der Datei-Deskriptor wird also beim Aufruf benötigt, um stdout von stderr zu unterscheiden.) Um die Eingabedatei umzuleiten, d. h. um die Eingabe aus einer Datei zu lesen, drehen Sie die Spitzklammer um (auch hier kann der Datei-Deskriptor 0 dazugeschrieben werden):

test.exe < eingabe.txt

Eine Umleitung von stdin findet oft auch über eine Pipe statt — das ist die Verkettung zweier Programme hintereinander. Die Ausgabe des ersten Programmes wird direkt als Eingabe für das zweite Programm weitergereicht:

programm1.exe | programm2.exe

All diese Umleitungen der Standard-Datenströme funktionieren in Ihrem Programm allerdings nur, wenn Sie dort einen oder mehrere dieser Datenströme öffnen.

16.6.2 Öffnen und Schließen eines Standard-Datenstroms

stdin und stdout werden über OPEN CONS geöffnet, im ersten Fall im Dateimodus INPUT, im zweiten im Dateimodus OUTPUT. Wenn Sie sowohl stdin als auch stdout öffnen wollen, müssen Sie zwei verschiedene Dateinummern verwenden.

Zum Öffnen von stderr greifen Sie auf OPEN ERR zurück. Auf den Datenstrom kann nur schreibend zugegriffen werden, weshalb kein Dateimodus angegeben werden muss — die Angabe wird ignoriert.

Quelltext 16.14 verwendet zur Demonstration alle drei Standard-Datenströme:

Quelltext 16.14: Standard-Datenströme nutzen
' Standard-Datenstroeme oeffnen
DIM AS INTEGER stdin  = FREEFILE
OPEN CONS FOR INPUT AS #stdin
DIM AS INTEGER stdout = FREEFILE
OPEN CONS FOR OUTPUT AS #stdout
DIM AS INTEGER stderr = FREEFILE
OPEN ERR FOR OUTPUT AS #stderr

' Zeile aus stdin lesen und nach stdout ausgeben - Statusmeldung nach stderr
DIM AS STRING zeile
PRINT #stderr, "Zeile wird gelesen und geschrieben"
LINE INPUT #stdin, zeile
PRINT #stdout, "Gelesene Zeile: "; zeile
CLOSE #stdin, #stdout, #stderr
SLEEP

Wenn Sie das Programm „normal“ starten, d. h. ohne Umleitungsangaben, gibt es zunächst auf der Konsole die Statusinformation aus (Zeile 11), wartet dann auf eine Benutzereingabe (diese wird während der Eingabe nicht angezeigt!) und gibt diese anschließend auf der Konsole aus. Um die Umleitung zu testen, können Sie z. B. folgenden Aufruf verwenden:

test.exe < eingabe.txt > ausgabe.txt 2> status.txt

(wobei davon ausgegangen wird, dass das Programm zu test.exe compiliert wurde und eine Eingabedatei namens eingabe.txt existiert — eine eventuell existierende ausgabe.txt und status.txt wird beim Aufruf überschrieben).

Standard-Datenströme können zwar mit CLOSE geschlossen, dann aber nicht mehr geöffnet werden. Um sie komplett zurückzusetzen (damit können Sie sie theoretisch wieder neu öffnen), verwenden Sie den Befehl RESET 0 für stdin bzw. RESET 1 für stdout. Allerdings gibt es in der Regel keinen Grund dafür, mehrmals denselben Standard-Datenstrom zu öffnen. Sie können ihn problemlos bis zum Ende des Programmes geöffnet halten.

Der Vollständigkeit halber soll an dieser Stelle auch OPEN SCRN erwähnt werden, das ebenfalls die Standardausgabe öffnet, jedoch nicht die Standardeingabe. Wenn Sie versuchen, von einer mit OPEN SCRN geöffneten Datei zu lesen, werden Sie dafür mit einer Endlosschleife belohnt. Der Befehl existiert hauptsächlich zur Rückwärtskompatibilität und bietet keine Vorteile im Vergleich zu OPEN CONS.

16.6.3 OPEN PIPE

Für die direkte Kommunikation Ihres Programma mit einem anderen Programm unterstützt FreeBASIC unidirektionale Pipes. Das sind Datenströme, die jedoch nur in eine Richtung laufen, nämlich vom aufgerufenen Programm aus in Richtung des aufrufenden Programmes. OPEN PIPE startet ein Programm (bzw. einen Shell-Befehl) und leitet dessen Ausgabe in einen Datenpuffer um, der über INPUT# gelesen werden kann. Dies kann folgendermaßen aussehen:

Quelltext 16.15: Unidirektionale Pipe
' Pipe oeffnen
DIM AS INTEGER dateinummer = FREEFILE
DIM AS STRING zeile
OPEN PIPE "test.exe" FOR INPUT AS #dateinummer

' Ausgabe zeilenweise einlesen
DO UNTIL EOF(dateinummer)
  LINE INPUT #dateinummer, zeile
  PRINT zeile
LOOP
CLOSE #dateinummer
SLEEP

Hilfreich kann dieser Befehl sein, wenn Sie mit Ihrem Programm einen Konsolenbefehl aufrufen und dessen Rückgabe verarbeiten wollen. Über EXEC (siehe Kapitel 17.3) ist es ebenfalls möglich, einen Rückgabewert eines Programmes abzurufen, dieser ist jedoch sehr eingeschränkt und eigentlich nur als Fehler-Rückgabe gedacht. OPEN PIPE erlaubt Ihnen dagegen jede beliebige Rückgabe, die dann nur noch entsprechend ausgewertet werden muss.

16.7 Hardware-Zugriffe: OPEN COM und OPEN LPT

Nach wie vor existiert der Befehl OPEN COM, um einen Zugriff auf einen COM-Port zu öffnen. Moderne Computer besitzen allerdings meist keinen COM-Port mehr, weshalb dieser wiederum vom Betriebssystem über einen speziellen Treiber simuliert werden muss. COM-Port-Zugriffe sind an sich nur für Spezialisten interessant, die bestimmte Geräte ansteuern wollen — für einen Einsteiger (ohne spezielle COM-Port-Neigungen) führt das Thema deutlich zu weit. Bei Interesse kann der dazu gehörige Referenzeintrag zu Rate gezogen werden:

Auch OPEN LPT öffnet eine Hardware-Verbindung, und zwar zu einem Drucker, der im Betriebssystem registriert ist. Dazu müssen Sie den Druckernamen kennen. Der vollständige Befehl lautet:

OPEN LPT "LPT:Druckername,TITLE=Dokumenttitel,EMU=TTY" [FOR Dateimodus] AS #Dateinr
  • Druckername gibt den Namen des Druckers an, unter dem er im System angemeldet ist. Möglich ist auch die Ansprache des Standarddruckers, ohne dass dessen Name bekannt sein muss — dazu kann Druckername einfach weggelassen werden.

  • Dokumenttitel legt fest, unter welchem Namen der Druckauftrag im Spooler angezeigt wird. TITLE=Dokumenttitel kann auch weggelassen werden; dann legt FreeBASIC automatisch einen Namen fest.

  • EMU=TTY ermöglicht die Verwendung von Steuerzeichen wie CHR(13) (Carriage Return — Wagenrücklauf), CHR(10) (Line Feed — Zeilenvorschub), CHR(8) (Backspace), CHR(9) (Tabulator) und CHR(12) (Form Feed — Seitenvorschub). Wenn EMU=TTY ausgelassen wird, müssen die Daten in einer Druckersprache gesendet werden, z. B. ESC/P, HPGL oder PostScript. Allerdings funktioniert EMU=TTY nur unter Windows. Unter Linux können also generell nur in einer Druckersprache gesendete Daten gedruckt werden.

Eine einfache Kommunikation mit dem Drucker könnte unter Windows folgendermaßen aussehen: Die Datei "test.txt" wird geöffnet, zeilenweise ausgelesen und zum Ausdruck an den Drucker namens "ReceiptPrinter" geschickt.

Quelltext 16.16: Einfaches Druckbeispiel
DIM AS STRING eingabe

' Datei zum Einlesen oeffnen
DIM AS INTEGER dateiNr = FREEFILE
OPEN "test.txt" FOR INPUT AS #dateiNr

' Drucker oeffnen
DIM AS INTEGER druckerNr = FREEFILE
OPEN LPT "LPT:ReceiptPrinter,TITLE=ReceiptWinTitle,EMU=TTY" AS #druckerNr

' Daten zeilenweise einlesen und drucken
WHILE NOT EOF(dateiNr)
  LINE INPUT #dateiNr, eingabe
  PRINT #druckerNr, eingabe
WEND

CLOSE #druckerNr, #dateiNr

PRINT "Beliebige Taste zum Beenden druecken..."
SLEEP

Die meisten Druckertreiber werden erst dann mit dem Druck beginnen, wenn sie einen Seitenvorschub (CHR(12)) empfangen. Wenn der Drucker über CLOSE geschlossen wird, sendet FreeBASIC automatisch einen Seitenvorschub, sodass in Quelltext 16.16 der Druck wie geplant durchgeführt wird.

Das Drucken simpler Texte ohne jegliche Formatierung ist in der Regel nicht sehr befriedigend, jedoch können komplexere Ausgaben nur mit detailierter Kenntnis einer der Druckersprachen getätigt werden (oder durch Einsatz einer passenden Bibliothek). Wenn Sie in Ihren Programmen ernsthaft den Einsatz einer Druckfunktion in Erwägung ziehen, sollten Sie sich ausgiebig z. B. mit PostScript auseinandersetzen. Für Anwendungen im rein privaten Bereich ist es oft einfacher, die Daten in einer Datei zu speichern und diese dann seperat auszudrucken.

Tip Alternative Druckmöglichkeit in früheren Versionen:
Eine deutlich einfachere, aber dafür auch eingeschränktere Methode zum Drucken (die auch möglicherweise nicht auf allen Systemen funktioniert) stellt der Befehl LPRINT dar, der wie PRINT funktioniert, nur dass er auf dem Standarddrucker ausgibt anstatt auf dem Bildschirm. Mit diesem Befehl ist ausschließlich der Standarddrucker ansteuerbar. LPRINT steht in der Compiler-Option -lang fb (also im Standard) nicht zur Verfügung.

16.8 Fragen zum Kapitel

  1. Welche Datei-Modi gibt es? Fassen Sie kurz die Besonderheiten der einzelnen Modi zusammen.

  2. Zum Ende des Programmes werden noch geöffnete Dateien automatisch geschlossen. Nennen Sie Gründe dafür, als Programmierer trotzdem alle über OPEN geöffneten Dateien auch explizit über CLOSE wieder zu schließen.

  3. Welche Datentypen sind für eine binäre Datenspeicherung besser, welche weniger gut geeignet?

  4. Schreiben Sie ein Programm, das sich den Namen des letzten Benutzers über eine externe Datei merkt. Wenn die Namensdatei noch nicht existiert, soll der Benutzer nach seinem Namen gefragt und dieser gespeichert werden. Ansonsten wird der zuletzt geschriebene Name eingelesen und der Benutzer mit diesem Namen begrüßt. Allerdings soll er dann die Möglichkeit erhalten, seinen Namen zu ändern (was dann natürlich wiederum gespeichert werden muss).


Fußnoten:
1) Die Dateiendungen werden unter Windows leider standardmäßig ausgeblendet. Nichtsdestotrotz müssen sie angegeben werden, um die korrekte Datei anzusprechen.

2) Bilder lassen sich durchaus auch im Textformat speichern — etwa beim Format X PixMap (XPM)

3) Das gilt natürlich nicht, wenn das Komma korrekt von Anführungszeichen umschlossen wird. Tatsächlich ist das Verhalten bei Dateien ohne korrekter Formatierung etwas komplizierter, aber das lernen Sie am besten durch Ausprobieren.

4) Big-Endian und Little-Endian stehen für die Byte-Reihenfolge, in der einfache Zahlenwerte in den Speicher gelegt werden. Bei Little-Endian wird das kleinstwertige Byte an der Anfangsadresse gespeichert, danach das zweitkleinste usw.


Kapitel 15: Stringmanipulation

Inhaltsverzeichnis

Kapitel 17: Betriebssystem-Anweisungen