One-Pointed MindAls ein Zen-Schüler liebe ich die Idee eines fokussierten
Bewußtseins: Tu nur ein Ding zur gleichen Zeit, aber mache
es richtig.Das ist ziemlich genau die gleiche Idee, welche &unix; richtig
funktionieren lässt. Während eine typische &windows;-Applikation
versucht alles Vorstellbare zu tun (und daher mit Fehler durchsetzt
ist), versucht eine &unix;-Applikation nur eine Funktion zu erfüllen
und das gut.Der typische &unix;-Nutzer stellt sich sein eigenes System durch
Shell-Skripte zusammen, die er selbst schreibt, und welche die Vorteile
bestehender Applikationen dadurch kombinieren, indem sie die Ausgabe
eines Programmes als Eingabe in ein anderes Programm durch eine Pipe
übergeben.Wenn Sie ihre eigene &unix;-Software schreiben, ist es generell
eine gute Idee zu betrachten, welcher Teil der Problemlösung durch
bestehende Programme bewerkstelligt werden kann. Man schreibt nur die
Programme selbst, für die keine vorhandene Lösung existiert.CSVIch will dieses Prinzip an einem besonderen Beispiel
aus der realen Welt demonstrieren, mit dem ich kürzlich
konfrontiert wurde:Ich mußte jeweils das elfte Feld von jedem Datensatz aus einer
Datenbank extrahieren, die ich von einer Webseite heruntergeladen
hatte. Die Datenbank war eine CSV-Datei, d.h.
eine Liste von Komma-getrennten Werten/emphasis>. Dies
ist ein ziemlich gewöhnliches Format für den Code-Austausch zwischen
Menschen, die eine unterschiedliche Datenbank-Software nutzen.Die erste zeile der Datei enthält eine Liste der Felder durch
Kommata getrennt. Der Rest der Datei enthält die einzelnen Datensätze
mit durch Kommata getrennten Werten in jeder Zeile.Ich versuchte awk unter Nutzung des
Kommas als Trenner. Da aber einige Zeilen durch in Bindestriche
gesetzte Kommata getrennt waren, extrahierte awk
das falsche Feld aus diesen Zeilen.Daher mußte ich meine eigene Software schreiben, um das elfte Feld
aus der CSV-Datei auszulesen. Aber durch Anwendung
der &unix;-Philosophie mußte ich nur einen einfachen Filter schreiben,
das Folgende tat:Entferne die erste Zeile aus der Datei.Ändere alle Kommata ohne Anführungszeichen in einen anderen
Buchstaben.Entferne alle Anführungszeichen.Streng genommen könnte ich sed benutzen,
um die erste Zeile der Datei zu entfernen, aber das zu Bewerkstelligen
war in meinem Programm sehr einfach, also entschloss ich mich dazu und
reduzierte dadurch die Größe der Pipeline.Unter Berücksichtigung aller Faktoren kostete mich das Schreiben
dieses Progammes ca. 20 Minuten. Das Schreiben eines Programmes, welches
jeweils das elfte Feld aus einer CSV-Datei extrahiert
hätte wesentlich länger gedauert und ich hätte es nicht wiederverwenden
können, um ein anderes Feld zu extrahieren aus irgendeiner anderen
Datenbank.Diesmal entschied ich mich dazu, etwas mehr Arbeit zu investieren,
als man normalerweise für ein typisches Tutorial verwenden würde:Es parst die Kommandozeilen nach Optionen.Es zeigt die richtige Nutzung an, falls es ein falsches Argument findet.Es gibt vernünftige Fehlermeldungen aus.Hier ist ein Beispiel für seine Nutzung:Usage: csv [-t<delim>] [-c<comma>] [-p] [-o <outfile>] [-i <infile>]Alle Parameter sind optional und können in beliebiger Reihenfolge auftauchen.Der -t-Parameter legt fest, was zu die Kommata zu ersetzen
sind. Der tab ist die Vorgabe hierfür. Zum Beispiel wird
-t; alle unquotierten Kommata mit Semikolon ersetzen.Ich brauche die -c-Option nicht, aber sie könnte zukünftig
nützlich sein. Sie ermöglicht mir festzulegen, daß ich einen anderen Buchstaben
als das Kommata mit etwas anderem ersetzen möchte. Zum Beispiel wird der Parameter
-c@ alle @-Zeichen ersetzen (nützlich, falls man eine Liste
von EmAil-Adressen in Nutzername und Domain aufsplitten will).Die -p-Option erhält die erste Zeile, d.h. die erste Zeile
der Datei wird nicht gelöscht. Als Vorgabe löschen wir die erste Zeile, weil die
CSV-Datei in der ersten Zeile keine Daten, sondern Feldbeschreibungen
enthält.Die Parameter -i- und -o-Optionen
erlauben es mir, die Ausgabe- und Eingabedateien festzulegen. Vorgabe sind
stdin und stdout, also ist es ein
regulärer &unix;-Filter.Ich habe sichergestellt, daß sowohl -i filename und
-ifilename akzeptiert werden. Genauso habe ich dafür Sorge
getragen, daß sowohl Eingabe- als auch Ausgabedateien festgelegt werden können.Um das elfte Feld jeden Datensatzes zu erhalten kann ich nun folgendes eingeben:&prompt.user; csv '-t;' data.csv | awk '-F;' '{print $11}'Der Code speichert die Optionen (bis auf die Dateideskriptoren) in
EDX: Das Kommata in DH,
den neuen Feldtrenner in DL und das Flag für
die -p-Option in dem höchsten Bit von
EDX. Ein kurzer Abgleich des Zeichens wird
uns also eine schnelle Entscheidung darüber erlauben, was zu tun ist.Hier ist der Code:
;;;;;;; csv.asm ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;
; Convert a comma-separated file to a something-else separated file.
;
; Started: 31-May-2001
; Updated: 1-Jun-2001
;
; Copyright (c) 2001 G. Adam Stanislav
; All rights reserved.
;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
%include 'system.inc'
%define BUFSIZE 2048
section .data
fd.in dd stdin
fd.out dd stdout
usg db 'Usage: csv [-t<delim>] [-c<comma>] [-p] [-o <outfile>] [-i <infile>]', 0Ah
usglen equ $-usg
iemsg db "csv: Can't open input file", 0Ah
iemlen equ $-iemsg
oemsg db "csv: Can't create output file", 0Ah
oemlen equ $-oemsg
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
section .text
align 4
ierr:
push dword iemlen
push dword iemsg
push dword stderr
sys.write
push dword 1 ; return failure
sys.exit
align 4
oerr:
push dword oemlen
push dword oemsg
push dword stderr
sys.write
push dword 2
sys.exit
align 4
usage:
push dword usglen
push dword usg
push dword stderr
sys.write
push dword 3
sys.exit
align 4
global _start
_start:
add esp, byte 8 ; discard argc and argv[0]
mov edx, (',' << 8) | 9
.arg:
pop ecx
or ecx, ecx
je near .init ; no more arguments
; ECX contains the pointer to an argument
cmp byte [ecx], '-'
jne usage
inc ecx
mov ax, [ecx]
.o:
cmp al, 'o'
jne .i
; Make sure we are not asked for the output file twice
cmp dword [fd.out], stdout
jne usage
; Find the path to output file - it is either at [ECX+1],
; i.e., -ofile --
; or in the next argument,
; i.e., -o file
inc ecx
or ah, ah
jne .openoutput
pop ecx
jecxz usage
.openoutput:
push dword 420 ; file mode (644 octal)
push dword 0200h | 0400h | 01h
; O_CREAT | O_TRUNC | O_WRONLY
push ecx
sys.open
jc near oerr
add esp, byte 12
mov [fd.out], eax
jmp short .arg
.i:
cmp al, 'i'
jne .p
; Make sure we are not asked twice
cmp dword [fd.in], stdin
jne near usage
; Find the path to the input file
inc ecx
or ah, ah
jne .openinput
pop ecx
or ecx, ecx
je near usage
.openinput:
push dword 0 ; O_RDONLY
push ecx
sys.open
jc near ierr ; open failed
add esp, byte 8
mov [fd.in], eax
jmp .arg
.p:
cmp al, 'p'
jne .t
or ah, ah
jne near usage
or edx, 1 << 31
jmp .arg
.t:
cmp al, 't' ; redefine output delimiter
jne .c
or ah, ah
je near usage
mov dl, ah
jmp .arg
.c:
cmp al, 'c'
jne near usage
or ah, ah
je near usage
mov dh, ah
jmp .arg
align 4
.init:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
mov edi, obuffer
; See if we are to preserve the first line
or edx, edx
js .loop
.firstline:
; get rid of the first line
call getchar
cmp al, 0Ah
jne .firstline
.loop:
; read a byte from stdin
call getchar
; is it a comma (or whatever the user asked for)?
cmp al, dh
jne .quote
; Replace the comma with a tab (or whatever the user wants)
mov al, dl
.put:
call putchar
jmp short .loop
.quote:
cmp al, '"'
jne .put
; Print everything until you get another quote or EOL. If it
; is a quote, skip it. If it is EOL, print it.
.qloop:
call getchar
cmp al, '"'
je .loop
cmp al, 0Ah
je .put
call putchar
jmp short .qloop
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
ret
read:
jecxz .read
call write
.read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword [fd.in]
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .done
sub eax, eax
ret
align 4
.done:
call write ; flush output buffer
; close files
push dword [fd.in]
sys.close
push dword [fd.out]
sys.close
; return success
push dword 0
sys.exit
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
jecxz .ret ; nothing to write
sub edi, ecx ; start of buffer
push ecx
push edi
push dword [fd.out]
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
.ret:
ret
Vieles daraus ist aus hex.asm entnommen worden.
Aber es gibt einen wichtigen Unterschied: Ich rufe nicht länger
write auf, wann immer ich eine Zeilenvorschub
ausgebe. Nun kann der Code sogar interaktiv genutzt werden.Ich habe eine bessere Lösung gefunden für das Interaktivitätsproblem
seit ich mit dem Schreiben dieses Kapitels begonnen habe. Ich wollte
sichergehen, daß jede Zeile einzeln ausgegeben werden kann, falls
erforderlich. Aber schlussendlich gibt es keinen Bedarf jede Zeile
einzeln auszugeben, falls nicht-interaktiv genutzt.Die neue Lösung besteht darin, die Funktion write
jedesmal aufzurufen, wenn ich den Eingabepuffer leer vorfinde. Auf diesem
Wege liest das Programm im interaktiven Modus eine Zeile aus der Tastatur
des Nutzers, verarbeitet sie und stellt fest, ob deren Eingabepuffer leer
ist, dann leert es seine Ausgabe und liest die nächste Zeile.Die dunkle Seite des BufferingDiese Änderung verhindert einen mysteriösen Aufhänger in einem speziellen
Fall. Ich bezeichne dies als die dunkle Seite des Buffering,
hauptsächlich, weil es eine nicht offensichtliche Gefahr darstellt.Es ist unwahrscheinlich, daß dies mit dem csv-Programm
oben geschieht aber lassen Sie uns einen weiteren Filter betrachten: Nehmen wir an
ihre Eingabe sind rohe Daten, die Farbwerte darstellen, wie z.B. die Intensität eines
Pixel mit den Farben rot, grün und
blau. Unsere Ausgabe wird der negative Wert unserer Eingabe
sein.Solch ein Filter würde sehr einfach zu schreiben sein. Der größte Teil davon
würde so aussehen wie all die anderen Filter, die wir bsiher geschrieben haben,
daher beziehe ich mich nur auf den Kern der Prozedur:
.loop:
call getchar
not al ; Create a negative
call putchar
jmp short .loop
Da dieser Filter mit rohen Daten arbeitet ist es unwahrscheinlich, daß
er interaktiv genutzt werden wird.Aber das Programm könnte als Bildbearbeitssoftware tituliert werden.
Wenn es nicht write vor jedem Aufruf von
read durchführt, ist die Möglichkeit gegeben, das
es sich aufhängt.Dies könnte passieren:Der Bildeditor wird unseren Filter laden mittels der
C-Funktion popen().Er wird die erste Zeile von Pixeln laden aus einer
Bitmap oder Pixmap.Er wird die erste Zeile von Pixeln geschrieben in die
Pipe, welche zur Variable
fd.in unseres Filters führt.Unser Filter wird jeden Pixel auslesen von der Eingabe,
in in seinen negativen Wert umkehren und ihn in den
Ausgabepuffer schreiben.Unser Filter wird die Funktion getchar
aufrufen, um das nächste Pixel abzurufen.Die Funktion getchar wird einen leeren
Eingabepuffer vorfinden und daher die Funktion
read aufrufen.read wird den Systemaufruf
SYS_read starten.Der Kernel wird unseren Filter
unterbrechen, bis der Bildeditor mehr Daten zur Pipe sendet.Der Bildedior wird aus der anderen Pipe lesen, welche verbunden
ist mit fd.out unseres Filters, damit er die
erste Zeile des auszugebenden Bildes setzen kann
bevor er uns die zweite Zeile
der Eingabe einliest.Der Kernel unterbricht den Bildeditor,
bis er eine Ausgabe unseres Filters erhält, um ihn an den
Bildeditor weiterzureichen.An diesem Punkt wartet unser Filter auf den Bildeditor,
daß er ihm mehr Daten zur Verarbeitung schicken möge.
Gleichzeitig wartet der Bildeditor darauf, daß unser
Filter das Resultat der Berechnung ersten Zeile sendet.
Aber das Ergebnis sitzt in unserem Ausgabepuffer.Der Filter und der Bildeditor werden fortfahren bis in
die Ewigkeit aufeinander zu warten (oder zumindest bis
sie per kill entsorgt werden). Unsere Software hat den
eine Race Condition
erreicht.Das Problem tritt nicht auf, wenn unser Filter seinen
Ausgabepuffer leert bevor er vom
Kernel mehr Eingabedaten anfordert.