# Programmeren in REXX/Bestanden beheren

In dit hoofdstuk leren we hoe we bestanden kunnen beheren, lezen of schrijven.

# Soorten bestanden

Op een computerschijf staan duizenden bestanden. Deze kunnen heel verschillende gegevens (data) bevatten, gaande van een telefoonlijst over mailberichten, foto's en broncode voor programma's of uitvoerbare programma's zelf.
In dit hoofdstuk maken we een onderscheid tussen 2 grote families bestanden, namelijk de tekstbestanden en de binaire bestanden.
Met tekstbestanden bedoelen we bestanden die enkel leesbare tekst bevatten. De broncode van een REXX programma is er een typisch voorbeeld van. Maar ook onze telefoonlijst kan in die vorm opgeslagen zijn. Nog anders gezegd: het zijn bestanden die met het programma notepad (of kladblok) te lezen en te bewerken zijn.
Binaire bestanden komen véél meer voor. De inhoud is niet in leesbare vorm. Het meest typisch zijn de uitvoerbare programma's (.EXE, .COM en .DLL) die machine-instructies bevatten. Maar nogal wat programmapakketten bewaren de gegevens van de gebruiker in binaire bestanden. Een foto (.jpg) bevat misschien wel een klein deeltje leesbare EXIF informatie, maar de rest is enkel begrijpbaar voor een fotobewerkingsprogramma. Dit type bestanden kunnen we ook vanuit REXX benaderen, maar dat veronderstelt dan wel dat we de structuur ervan kennen. Dit zal slechts uitzonderlijk het geval zal zijn.
We kunnen bestanden nog op een andere manier indelen. We spreken dan van:
* Sequentiële bestanden, waarbij data in sequentiële volgorde wordt behandeld. Onze tekstbestanden zijn van dit type.
* Bestanden met directe toegang. Als alle lijnen (of logisch bij elkaar horende gegevens) dezelfde lengte hebben is het mogelijk een willekeurige lijn er rechtstreeks uit te halen. De verplaatsing in het bestand is eenvoudig het lijnnummer maal de lengte van één gegevenslijn. We kunnen de lijnen dus direct adresseren.
* Geïndexeerde bestanden waarbij de plaats van elk gegeven bijgehouden wordt in een apart bestand. Vergelijk het met een boek waarvan de index op aparte pagina's wordt bijgehouden. Dank zij de index is rechtstreekse toegang tot een hoofdstuk mogelijk.
* Relationele bestanden. Dit zijn bestanden met complexe opbouw, typisch voor relationele databasesystemen zoals DB2.
De twee laatste vormen zullen we hier niet tegenkomen. Ze vragen speciale hulpmiddelen om de gegevens te benaderen.
Verder heeft elk besturingssysteem ook nog zijn eigen manier om bestanden op gegevensdragers te organiseren. Zelfs de gebruikte karakterset kan verschillen. Op een mainframe gebruikt men de EBCDIC karakterset terwijl men op een PC gebruik maakt van een ASCII karakterset. En zelfs daarin kunnen nog varianten bestaan, specifiek voor een bepaalde regio of taal. Dit is een zeer uitgebreid en complexe materie die zeker niet tot dit boek behoort. Een begin van uitleg kan gevonden worden in het Wikipedia artikel over tekensets.
Mainframe systemen stockeren hun gegevens normaal lijn per lijn (men gebruikt de term record-oriented). De nu haast verdwenen ponskaart was de aanzet om gegevens op deze manier te ordenen. Elke kaart is te beschouwen als een regel in een tekst. Deze organisatie is later verder doorgedreven in de manier waarop gegevens op disk of band zijn opgeslagen. Een mainframe kent van elk record de lengte op voorhand.
PC's daarentegen, bewaren hun gegevens als een continue reeks karakters (streaming is hier de toepasselijke term). Wil men een logische opdeling in deze doorlopende reeks bytes, dan is een conventie nodig die door alle partijen is gekend. In onze tekst-bestanden bijvoorbeeld wordt elke regel van de volgende gescheiden worden door controle-karakters. Typisch zijn een combinatie van Carriage Return (=ga naar linker marge op een schrijfmachine) en Line Feed (=draai de drukrol een regel door). In de ASCII karaktertabel hebben ze de waarde '0D0A'x. Het einde van een bestand wordt meestal aangeduid met een End-Of-File karakter ('1A'x). Ook tabulatiekarakters kunnen we tegenkomen ('09'x). Een programma als notepad kan met deze karakters overweg.
REXX heeft bevelen en functies die tekstbestanden lijn voor lijn kunnen behandelen. De controle-karakters worden dus ook herkend. Maar REXX heeft evenzeer bevelen en functies die streaming toelaten, waardoor binaire bestanden kunnen worden gelezen of geschreven.
Om te beginnen bekijken we een nuttige functie die bestandsnamen kan opsplitsen.

# FILESPEC - bestandsnamen opsplitsen

De bestandsnaam wordt hier in de brede zin van de betekenis gebruikt, t.t.z. het is het volledig pad en de volledige naam van het bestand, inclusief extensie.
Met deze functie kunnen volgende opties worden gegeven om elementen uit de bestandsnaam te halen (we veronderstellen de bestandsnaam "d:\RexxProgrammas\Testen\priem.rex"):
* Drive geeft de stationsletter (bv. D:)
* Path geef het pad naar het bestand (bv. \RexxProgrammas\Testen\)
* Name geeft de naam van het bestand, inclusief extensie (bv. Priem.rex)
Zoals steeds is enkel de eerste letter van de optie nodig.

# STREAM functie

De stream functie kan voor een stream
* de huidige status ervan weergeven, of
* een beschrijving van zijn toestand geven, of
* opdrachten zoals openen en sluiten uitvoeren.
In de voorbeelden van dit hoofdstuk zullen we steeds werken met bestanden op schijf (of CD of stick). Maar een stream kan ook nog andere soorten sequentiële data omvatten. Wat door de gebruiker wordt ingetikt op het klavier is bijvoorbeeld evenzeer een stream. Een antenne die het heelal afspeurt naar levende wezens zal ook een continue reeks bits en bytes - dus een stream - binnenkrijgen.
We bespreken nu de verschillende mogelijkheden van de stream functie.

## Status opvragen

De standaard optie state laat weten in welke staat de stream op dit ogenblik is. Volgende kunnen voorkomen:
* ERROR - er is een fout opgetreden bij een vorige operatie;
* NOTREADY - de stream, of beter gezegd het toestel waar de stream zich bevindt, is niet gereed, de toegang is niet mogelijk;
* READY - de stream is klaar om bewerkt te worden;
* UNKNOWN - betekent meestal dat de stream (nog) niet is geopend, er is bv. geen CD in de lade.

## Een beschrijving geven

Deze optie is vergelijkbaar met "Status", maar in het geval van ERROR of NOTREADY wordt extra informatie gegeven, indien deze voorhanden is.

## Opdrachten uitvoeren

De mogelijke streaming_opdrachten zijn:
* OPEN om de stream te openen;
* CLOSE om de stream af te sluiten;
* QUERY om eigenschappen van de stream te ondervragen;
* FLUSH om de lees- of schrijfbuffer onmiddellijk leeg te maken;
* SEEK of POSITION om zich te verplaatsen in de stream;
Laten we deze opdrachten nu in meer detail bestuderen.

### OPEN opdracht

Om toegang te kunnen hebben tot de gegevens die in een stream zitten moet deze eerst geopend worden. Dit houdt in dat het besturingssysteem o.a.
* nagaat of het toestel (disk, CD, schotelantenne) waarop de stream zich bevindt bereikbaar is en klaar is om te werken (READY of NOTREADY);
* nagaat of de stream bestaat. Meer specifiek het geval voor bestanden op disk;
* geheugenbuffers aanmaakt om data te ontvangen bij lezen of om data te versturen bij schrijven. Deze buffers dienen o.a. om de snelheid te vergroten. Pas als een buffer is gevuld zal de inhoud fysisch naar het medium worden weggeschreven. Het lezen of schrijven gebeurt dus niet voor elke byte apart.
Wanneer we een stream openen kunnen we bepalen met welke intentie we hem openen. De mogelijke intenties zijn:
* READ, we willen de stream enkel lezen;
* WRITE, we willen naar de stream schrijven;
* BOTH, we willen zowel kunnen lezen als schrijven. Dit is de standaard intentie.
Voor een schrijfintentie kunnen we nog een extra optie meegeven, nl:
* REPLACE, we willen het bestand vervangen door nieuwe inhoud;
* APPEND, we willen data toevoegen aan het eind van het bestand. Dit is de standaardoptie.
Een eerste voorbeeld laat zien hoe we een bestand (onze adressenlijst) klaarmaken om er lijnen aan te kunnen toevoegen:
Met intentie both kan een bestand dus zowel gelezen als beschreven worden. In dat geval moet de programmeur zelf zorgen om zijn lees- en schrijfpositie bij te houden. Daarvoor zijn de "SEEK" of "POSITION" opdrachten te gebruiken. In dit boek behandelen we deze opdrachten niet.

### CLOSE opdracht

De stream wordt nu afgesloten.
* de data die nog aanwezig is in de buffers worden weggeschreven;
* de informatie over het bestand wordt in de FAT tabel aangepast. Het gaat om de plaats op schijf, creatiedatum en/of laatste toegangsdatum, attributen, etc.
Er zijn geen extra opties bij het sluiten van een bestand.
Merk op dat we aan de eerste letter van "Command, State" of "Description" genoeg hebben. Merk ook op dat we die opties constant maken, want stel dat er een variabele c of close zou bestaan, dan kan alles in het honderd lopen.
Daar de stream functie geen returncode teruggeeft, zullen we ze dikwijls via een call bevel oproepen.

### QUERY opdracht om eigenschappen op te vragen

Uit volgende opties kunnen we kiezen:
* EXISTS. Als het bestand bestaat krijgen we het volledig pad terug. In het ander geval krijgen we een nullstring terug.
* TIMESTAMP. Geeft datum en tijd van het bestand in internationaal formaat (jjjj-mm-dd hh:mm:ss).
* SIZE. Geeft de grootte van het bestand in bytes.
Er zijn nog andere opties, zoals het opvragen van de lees- of schrijfpositie, maar die behandelen we hier ook niet.
In de OORexx implementatie biedt het RexxUtil functiepakket eenvoudiger manieren om informatie over een bestand op te vragen. We leren daarover meer in het hoofdstuk over RexxUtil functies.
In dit voorbeeld geven we niet het volledig pad naar het bestand. De twee puntjes verwijzen naar de map boven de nu actieve map. Als we voortgaan op de naam die we in voorgaande voorbeelden gebruikten, mogen we dus «D:\map\mijn adreslijst.txt» als antwoord verwachten.

# Bestanden lezen

Nadat we het bestand hebben geopend kunnen we het gaan lezen. Hiervoor gebruiken we de functies charin of linein. In het eerste geval lezen we de stream zonder rekening te houden met controle-karakters zoals CrLf. Linein houdt wél rekening met de controlekarakters, we zullen dus lijn per lijn kunnen lezen.

## CHARIN methode om een bestand te lezen

Deze functie geeft de inhoud van het bestand, te beginnen aan de byte op positie start en voor een opgegeven aantal bytes. Zonder opgave van aantal wordt slechts één byte gelezen.
Voor streams wordt een leespositie bijgehouden. Net na het openen is dit aan de eerste byte. Als men daarna een aantal bytes leest, verplaatst de leespositie zich over dat aantal. Bij een volgende leesopdracht zal men vanaf die positie verder lezen, tenzij we weer een expliciete start-positie meegeven.
Wat we hier vertellen geldt eigenlijk alleen voor wat men persistente streams noemt. Een bestand op schijf is daar zeker een voorbeeld van. Een schotelantenne daarentegen, produceert een continue stroom data, en men spreekt dan van een transient stream. Voor deze laatste heeft een start-positie uiteraard geen enkele zin en mag ze zelfs niet worden opgegeven.
Nog een laatste opmerking, om volledig te zijn: als men geen bestandsnaam geeft, dan wordt gelezen van de standaard stream (STDIN). Meestal is dit het toetsenbord wat trouwens ook een voorbeeld van een transient stream is. Het lezen stopt wanneer de Enter-toets wordt ingedrukt.
In volgende voorbeelden veronderstellen we dat het bestand begint met de karakters "Mike Cowlishaw" en dat alle statements na elkaar uitgevoerd worden:
Willen we alle karakters van ons (persistent) bestand lezen, dan moeten we dus weten hoe groot het bestand is. Dit zou men kunnen opvragen met een stream(bestand,"C","QUERY SIZE"), doch de chars functie is heel wat eenvoudiger:

## CHARS functie - aantal karakters in een bestand

Voor een persistente stream geeft deze functie het aantal karakters dat aanwezig is, vanaf de huidige leespositie tot het eind van de stream. Om een bestand in één operatie volledig te lezen kunnen we dus het volgende schrijven:
Voor transient streams geeft de functie hoeveel karakters er in de STDIN-buffer te wachten staan.

## LINEIN methode om een bestand te lezen

Deze functie neemt de controlekarakters tussen de tekstregels wél voor zijn rekening. Dus, elke Carriage-Return/Line-Feed (CrLf) bepaalt het einde van een regel. De functie zal niet verder lezen dan het eerste EOF (End-Of-File = '1A'x) karakter dat ze tegenkomt.
We kunnen vragen om één of geen lijn te lezen, en dit vanaf een bepaalde lijn. Ook hier wordt een leespositie bijgehouden. Een volgende lezing begint bij die positie. Als vooraf met charin een deel is gelezen, kan het zelfs zijn dat de huidige leespositie midden in een regel staat. Als het aantal gelijk is aan nul, dan wordt uiteraard niets gelezen en blijft de leespositie ongewijzigd.
Als geen bestandsnaam is opgegeven, dan wordt ook hier van de standaard stream gelezen, dus meestal het toetsenbord. Ook nu wacht het programma tot de Enter-toets gebruikt wordt, want dat genereert een Carriage Return ('0D'x) en bepaalt dus het einde van een regel.
* In de PC-wereld is het toetsenbord een transient stream. Alle karakters die worden ingetikt gaan onmiddellijk naar een geheugenbuffer in het systeem. Op een mainframe blijft alles in de buffer van de terminal (=scherm/toetsenbord combinatie) tot een zogenaamde interrupt-toets is ingedrukt (de Enter-toets of functietoetsen). Daar is het gebruik van "linein" ook veel logischer, want we herkennen weer de notie record.
Voorbeelden:
Zelfs al worden deze statements na elkaar uitgevoerd, dan nog zal de tweede leesopdracht de 10de lijn lezen, en niet de 11de !

## LINES functie - zijn er nog data ?

Er is ook een lines functie. Deze werkt niet zoals de chars functie. Ze antwoord enkel 1 als er nog data in de stream zit, anders 0. Deze functie kan desgevallend wel gebruikt worden om een lus te draaien tot de data op is, dus totdat men het einde van het bestand heeft bereikt.

# Bestanden schrijven

Om data in een bestand te kunnen schrijven moet dit eerst geopend worden met de intentie write of both. Willen we achteraan toevoegen dan is de optie append nodig. Met replace zullen we (een deel van) het bestand overschrijven. Ook hier hebben we de 2 verschillende methodes, één om te streamen en één om lijn per lijn te schrijven.

## CHAROUT methode om een bestand te schrijven

Zonder opgave van een bestandsnaam zal geschreven worden naar de standaard output-stream (STDOUT), meestal is dit het scherm.
Bij schrijfoperaties wordt eveneens een positie bijgehouden. Een volgende schrijfoperatie start dan bij die positie, tenzij expliciet een start wordt opgegeven. Deze start-positie kan enkel voor persistente streams en moet een positief getal zijn. Bij het openen van de stream is de schrijfpositie op het einde van het bestand.
Het is ook mogelijk een start-positie te geven zonder data mee te geven. Daarmee wordt dus enkel de schrijfpositie aangepast. En als noch start, noch data worden meegegeven, dan wordt het bestand gesloten en is de returncode van de functie 0.
In normale gevallen geeft deze functie een returncode die gelijk is aan het aantal karakters dat nog niet is weggeschreven. Normaal zal dit 0 zijn voor een bestand op disk, maar voor een printer moet misschien eerst nog papier worden bijgestoken.
Als voorbeeld een aantal statements, die na elkaar worden uitgevoerd:
Merk op dat er toch wel karakters zullen geschreven worden tussen positie 6 en 16. Het zullen dan '00'x karakters zijn (hexadecimaal nul).

## LINEOUT methode om een bestand te schrijven

Deze functie geeft 0 als alles goed verliep, 1 als een fout optrad.
Telkens een lijn wordt geschreven worden ook de controlekarakters geschreven ('0D0A'x). Als data een nullstring is (""), dan maken we zo een lege lijn in de tekst, want de controlekarakters worden wel geschreven.
Zonder opgave van data wordt de schrijfpositie op regel lijn gezet. Zonder opgave van een bestandsnaam wordt naar STDOUT geschreven (meestal het scherm dus). Zonder data noch lijn wordt het bestand gesloten.
Voor persistente streams kan men een lijnnummer opgeven. Dit moet een positief getal zijn, en binnen de grenzen van het bestand liggen. In dit geval kan men dus geen lijnen "overslaan".
Dit zijn voorbeelden:
Als al deze statements na elkaar worden uitgevoerd zal er van de initiële tekst in het bestand niets overblijven, zelfs al hebben we geopend met de intentie "write append". Inderdaad, in het tweede statement positioneren we ons terug aan het begin van het bestand. Wat door het eerste statement is geschreven gaat daarbij ook weer verloren. Het uiteindelijke bestand zal slechts 2 regels bevatten, een eerste die leeg is, en een tweede die de tekst «Tot ziens» bevat.

# Bestanden uitvagen, hernoemen of kopiëren

We leerden in het hoofdstuk over host commando's hoe we aan ons besturingssysteem kunnen vragen om bestanden te hernoemen, kopiëren of uit te vagen. Ook hier biedt het RexxUtil pakket dat bij de OORexx versie geleverd wordt betere alternatieven. Heel dikwijls hebben andere REXX implementaties soortgelijke pakketten. We zullen daar later nog op terugkomen. Hier enkel een voorbeeldje:

# Performantie aspecten

We leerden al dat tekstbestanden zowel met de linein als met de charin functie kunnen gelezen worden. Maar in het tweede geval hebben we wel meer werk om de logische opsplitsing in regels te maken.
Toch is, op een PC, de charin methode te verkiezen indien snelheid belangrijk wordt. Voor een programma dat slechts een paar maal wordt uitgevoerd is het extra werk misschien zinloos, maar als het programma zeer frequent wordt uitgevoerd loont elke verbetering in snelheid. Ook wanneer het programma grote bestanden moet verwerken is snelheidswinst snel merkbaar.
Hoe pakken we dat dan aan ? Wel, we volgend deze logica:
* we lezen het volledig bestand met één charin. Enkel indien het computergeheugen het hele bestand niet zou kunnen bevatten moeten we het lezen opsplitsen in kleinere blokken;
* we starten een lus waarin we de lijnen er één voor één van afknippen middels het parse bevel. We knippen telkens wanneer de CrLf controlekarakters optreden.
* we behandelen de lijn en hernemen de lus.
We maken de proef met een tamelijk groot bestand. Het is meer dan 135000 bytes groot en bevat 3413 tekstlijnen. Ons voorbeeld-bestand wordt samen met OORexx geïnstalleerd. U kan deze test dus ook uitvoeren met hetzelfde bestand.
Als eerste test lezen we het bestand met de linein functie:
Met de time functie kunnen we meten hoeveel tijd nodig is voor de verschillende operaties. Met een Reset zetten we de timer op nul. Bij elke latere Elapsed optie krijgen we het aantal seconden dat is verlopen sinds de vorige reset. Noteer ook hoe we de bestandsnaam over 2 regels hebben gedefinieerd. Zonder het concatenatieteken had REXX een spatie geplaatst tussen de 2 delen.
Dit krijgen we als antwoord als we het programma op ons systeem (Intel Q6600 2.4Ghz) draaien:
N.B. Indien het programma meermaals na elkaar wordt uitgevoerd kunnen de resultaten licht verschillen. Dit heeft alles te maken met het feit dat het bestand nog deels in de buffers van het systeem kan zitten.
Nu stappen we over op de charin methode:
Dit is het resultaat dat we bekomen op ons systeem:
De uitvoeringstijd is met bijna 60% gedaald !
En toch is er nog verbetering mogelijk.
Met als resultaat:
Nog eens een besparing van 94% ! Vanwaar komt die ?
Wel, in de eerste versie met "charin" gebruikten we parse om de data op te eten. Het probleem is dat we een stukje van een grote string wegnemen en het overblijvend deel terug in dezelfde variabele data steken. REXX heeft daar veel werk mee, want intern moet een nieuwe tijdelijke variabele gemaakt worden om het tweede deel van data in te steken. Dan wordt het geheugen van de oude variabele data opgekuist. Tenslotte wordt de nieuwe variabele naar data hernoemd. Het aanvragen en loslaten van geheugen is een tijdrovende operatie. In de laatste versie hebben we dat kunnen vermijden, met de spectaculaire winst tot gevolg.
Nog even de details van het programma bekijken:
* We gebruiken geen teller meer, maar onze lus gebruikt een lusvariabele i die we als teller kunnen gebruiken. We geven geen eindbestemming aan onze lusvariabele, zodat het een eindeloze lus wordt (een "do forever" dus, maar mét teller). We moeten er dan natuurlijk voor zorgen de lus op een bepaald moment te verlaten. Dat doen we met het leave bevel als we geen CrLf meer vinden.
* We gebruiken de functie pos om naar de CrLf controlekarakters te zoeken, maar we beginnen aan een startpositie (startp)die we in de lus telkens weer net ná de laatst gevonden CrLf zetten.
* Na de lus bevat de lusteller één te veel. Rexx verhoogt de teller namelijk op het eind van de lus. Daarom moeten we terug één aftrekken in het say bevel.
De geoptimaliseerde charin methode duurt dus 40 maal minder lang dan wanneer we de linein methode gebruikten.
We willen nogmaals benadrukken dat wat we in deze laatste paragraaf hebben beschreven enkel geldig is voor PC systemen. Op een mainframe is het net andersom, want streaming is onnatuurlijk voor een mainframe, records behandelen is een fluitje van een cent. Daar blijkt een oplossing met parse het dikwijls ook te winnen van andere oplossingen. Vermoedelijk heeft dit te maken met een andere manier waarop het geheugen beheerd wordt.