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.
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. mitENCODING "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.
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.
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.
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.
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
.
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.
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.
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.
' 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.
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:
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.
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:
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
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()
.
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 mitOPEN
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 mitmenge
angegeben werden, wie viele Elemente des entsprechenden Datentyps geschrieben werden sollen. Sie können damit in einemALLOCATE
-Puffer z. B. zehnINTEGER
hintereinander ablegen und diese zehn Werte dann in einem Rutsch in die Datei schreiben. Ohne Angabe vonmenge
wird immer ein einzelner Wert geschrieben. Wenn Sie ein Array schreiben, gibtmenge
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.
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.
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.
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
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
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.
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
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.
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.
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
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.
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.
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!
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:
' 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:
' 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 kannDruckername
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 wieCHR(13)
(Carriage Return — Wagenrücklauf),CHR(10)
(Line Feed — Zeilenvorschub),CHR(8)
(Backspace),CHR(9)
(Tabulator) undCHR(12)
(Form Feed — Seitenvorschub). WennEMU=TTY
ausgelassen wird, müssen die Daten in einer Druckersprache gesendet werden, z. B. ESC/P, HPGL oder PostScript. Allerdings funktioniertEMU=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.
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.
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
-
Welche Datei-Modi gibt es? Fassen Sie kurz die Besonderheiten der einzelnen Modi zusammen.
-
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 überCLOSE
wieder zu schließen. -
Welche Datentypen sind für eine binäre Datenspeicherung besser, welche weniger gut geeignet?
-
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.