Programmeren in REXX/Debugging
In dit hoofdstuk gaan we dieper in op het opzoeken van fouten in een REXX programma (debugging). We zullen het hebben over de uitgebreide traceer-mogelijkheden, maar ook over hoe we fouten kunnen intercepteren tijdens uitvoering.
TRACE commando en functie
bewerkenREXX heeft zowel een trace commando als een trace functie waarmee tracering kan aan- of afgezet kan worden. We leggen later uit in welke geval we beter het bevel of de functie gebruiken, maar we beginnen met de syntaxis van het trace bevel.
TRACE bevel
bewerkentrace aantal of trace [?][type | "Normal"] of trace constante of symbool of VALUE uitdrukking
In het eerste geval vragen we een aantal programmalijnen te traceren.
In het tweede geval vragen we om bepaalde types te traceren. We kunnen kiezen uit:
- Normal, Failure of Error - toont alle host-commands die voor een fout zorgen, samen met de returncode. Onder Windows, zal het oproepen van een onbekend host-commando resulteren in een failure en een foutcode=1.
- All - alle lijnen worden vóór uitvoering getoond.
- Commands - alle host-commands worden vóór uitvoering getoond, en indien een fout optreedt wordt ook de foutcode getoond.
- Error - alle besturingssysteem-commando's worden getoond, samen met de eventuele foutcode.
- Intermediates - de meest krachtige tracering die álle programmaregels traceert vóór hun uitvoering, maar daarbovenop ook alle tussenliggende interpretatiestappen toont.
- Labels - traceert de labels waarlangs het programma voorbijkomt. Nuttig om de weg te volgen die wordt afgelegd door oproep van interne functies of routines en sprongen ten gevolge van signal instructies.
- Off - zet tracering af.
- Results - traceert alle programmalijnen vóór uitvoering en toont tevens het eindresultaat na interpretatie.
Het optionele ?-teken dat men voor het type kan schrijven zet de interactieve tracering aan, of terug uit bij een tweede oproep ervan. Er mag geen spatie zijn tussen het vraagteken en het type als er een wordt opgegeven.
Met interactieve tracering zal het programma wachten na uitvoering van elke programmalijn, en geeft het de programmeur de kans om instructies uit te voeren of het programma af te breken. Dit is een uiterst krachtige optie, omdat het bijvoorbeeld ook toelaat waarden van variabelen op te vragen of zelfs te veranderen om zo de koers van het programma te wijzigen. Het is tevens mogelijk het laatste statement nogmaals uit te voeren (bv. na veranderen van de variabelen). Voor een beginnend programmeur is het tevens een leerzame optie, omdat het duidelijk laat zien hoe REXX zijn werk doet.
Vooraleer verder te gaan met voorbeelden, geven we nog het formaat van de trace functie:
TRACE functie
bewerkentrace([type])
Zonder type zal de functie de huidige traceringsmethode teruggeven. Met een type kan de nu geldende methode worden aangepast tijdens uitvoering van het programma.
Voorbeelden van tracering
bewerkenOm voorbeelden te geven maken we gebruik van een stukje programma dat we uit een vorig hoofdstuk haalden:
/* Voorbeelden van tracering */ parse arg traceringsoptie call trace traceringsoptie parse value time() time('S') with TijdNu DagSeconden parse var TijdNu uu ':' mm ':' ss /* splits tijd in uren, minuten en seconden */ AantalSeconden=uu*3600 + mm*60 + ss /* hoeveel is dat in seconden */ say AantalSeconden=DagSeconden /* «1» want anders maakten we een fout */ Plus4min= AantalSeconden + 4*60 /* doe er 4 minuten bij */ /* Nu terug omzetten naar uu:mm:ss formaat... */ uu = Plus4min % 3600 /* Gehele deling */ if uu>23 then uu=uu-24 /* Misschien middernacht overschreden bij optelling */ rest = Plus4min // 3600 /* Rest na deling door 3600 (= aantal sec in 1 uur */ mm = rest % 60 /* Omzetten naar minuten via gehele deling */ ss = Plus4min // 60 /* Seconden zijn rest na deling door 60 */ Say 'Binnen 4 minuten zal het' right(uu,2,0)':'right(mm,2,0)':'right(ss,2,0) 'zijn.' exit
Bij een eerste oproep gaan we de Resultaten traceren:
C:\>d:\RexxProgrammas\test R >>> "N" 3 *-* parse value time() time('S') with TijdNu DagSeconden >>> "S" >>> "11:32:59 41579" >>> "11:32:59" >>> "41579" 4 *-* parse var TijdNu uu ':' mm ':' ss /* splits tijd in uren, minuten en seconden */ >>> ":" >>> "11" >>> ":" >>> "32" >>> "59" 5 *-* AantalSeconden=uu*3600 + mm*60 + ss /* hoeveel is dat in seconden */ >>> "41579" 6 *-* Plus4min= AantalSeconden + 4*60 /* doe er 4 minuten bij */ >>> "41819" 8 *-* uu = Plus4min % 3600 /* Gehele deling */ >>> "11" 9 *-* if uu>23 >>> "0" 10 *-* rest = Plus4min // 3600 /* Rest na deling door 3600 (= aantal sec in 1 uur */ >>> "2219" 11 *-* mm = rest % 60 /* Omzetten naar minuten via gehele deling */ >>> "36" 12 *-* ss = Plus4min // 60 /* Seconden zijn rest na deling door 60 */ >>> "59" 13 *-* Say 'Binnen 4 minuten zal het' right(uu,2,0)':'right(mm,2,0)':'right(ss,2,0) 'zijn.' >>> "11" >>> "2" >>> "0" >>> "36" >>> "2" >>> "0" >>> "59" >>> "2" >>> "0" >>> "Binnen 4 minuten zal het 11:36:59 zijn." Binnen 4 minuten zal het 11:36:59 zijn. 14 *-* exit
Van elk statement zien we nu de tussenstappen van de interpretatie. Voor het derde statement zien we achtereenvolgens:
- dat er een constante 'S' is meegegeven in de tweede oproep van de functie time. REXX interpreteert dus eerst wat tussen haakjes staat. In dit geval is het een parameter van een functie, maar bij wiskundige formules kunnen we ook haakjes gebruiken om de volgorde van uitwerking te beïnvloeden. De parameter had trouwens ook een uitdrukking met variabelen kunnen zijn. Die moest dan eerst worden omgezet. Op statement 13 hebben we daar voorbeelden van;
- dat de bron van de parse instructie tot de waarde «11:32:59 41579» heeft geleid;
- dat de eerste variabele van het sjabloon (TijdNu) het eerste woord van de bron krijgt;
- dat de tweede variabele van het sjabloon (DagSeconden) het tweede woord van de bron krijgt;
Kiezen we nu voor de tracering met Intermediaire resultaten dan zien we:
C:\>d:\RexxProgrammas\test I >>> "N" 3 *-* parse value time() time('S') with TijdNu DagSeconden >F> "11:44:02" >L> "S" >>> "S" >F> "42242" >O> "11:44:02 42242" >>> "11:44:02 42242" >>> "11:44:02" >>> "42242" 4 *-* parse var TijdNu uu ':' mm ':' ss /* splits tijd in uren, minuten en seconden */ >V> "11:44:02" >L> ":" >>> ":" >>> "11" >L> ":" >>> ":" >>> "44" >>> "02" 5 *-* AantalSeconden=uu*3600 + mm*60 + ss /* hoeveel is dat in seconden */ >V> "11" >L> "3600" >O> "39600" >V> "44" >L> "60" >O> "2640" >O> "42240" >V> "02" >O> "42242" >>> "42242"
We hebben de output beperkt tot de eerste 5 statements, want we merken onmiddellijk dat we nu héél wat meer te zien krijgen.
Om te beginnen moeten we even uitleggen wat die aanduidingen tussen >-tekens betekenen:
- *-* De te interpreteren lijn, eventueel gevolgd door:
- >L> De waarde van een constante (Literal).
- >V> De huidige waarde van een Variabele.
- >O> Een tussenresultaat bij de interpretatie (O staat voor Output)
- >>> Het uiteindelijk resultaat van de interpretatie van het hele statement.
- >.> De waarde die aan een plaatshouder in een parse instructie is gegeven.
- >C> De waarde van een samengestelde variabele (stem-variabele) na interpretatie van haar staart-stuk. (C staat voor Compound variable).
- >F> Het resultaat van een Functie.
- +++ Een bericht van trace, bv. een host-commando dat een fout genereerde, het bericht dat aanduidt dat interactieve tracering wordt gestart, een fout tegen de syntaxis...
We hebben hier slechts een beperkt aantal van die aanduidingen. We bekijken weer wat het derde statement ons heeft gegeven:
- De eerste oproep van de Functie time geeft «11:44:02» als resultaat;
- De tweede functie heeft een constante parameter (Literal);
- Het resultaat van de interpretatie is natuurlijk gewoon «S»;
- Het resultaat van de Functie is nu «42242» seconden;
- Dus de Output van de bron voor het parse bevel is «11:44:02 42242»;
- Het resultaat van de interpretatie is diezelfde waarde;
- De eerste variabele van het sjabloon heeft als resultaat het eerste woord van de bron;
- De tweede variabele van het sjabloon heeft als resultaat het tweede woord van de bron.
Kortom, meer dan voldoende informatie om desgevallend de fout te kunnen ontdekken.
Interactieve tracering
bewerkenNu gaan we nog een stap verder, en schakelen de interactieve tracering in. Wat vetjes gedrukt is voert de gebruiker in:
C:\>d:\RexxProgrammas\test ?r >>> "N" +++ Interactive trace. "Trace Off" to end debug, ENTER to Continue. +++ [programma stopt en wacht op actie - we drukken de Enter-toets] 3 *-* parse value time() time('S') with TijdNu DagSeconden >>> "S" >>> "11:57:18 43038" >>> "11:57:18" >>> "43038" [programma stopt en wacht op actie] TijdNu="11:57:00" [we drukken de Enter-toets] 4 *-* parse var TijdNu uu ':' mm ':' ss /* splits tijd in uren, minuten en seconden */ >>> ":" >>> "11" >>> ":" >>> "57" >>> "00" [programma stopt en wacht op actie] TijdNu="11:57:30" [we drukken de Enter-toets] = [we drukken de Enter-toets] 4 *-* parse var TijdNu uu ':' mm ':' ss /* splits tijd in uren, minuten en seconden */ >>> ":" >>> "11" >>> ":" >>> "57" >>> "30" [programma stopt en wacht op actie - we drukken de Enter-toets] 5 *-* AantalSeconden=uu*3600 + mm*60 + ss /* hoeveel is dat in seconden */ >>> "43050" [programma stopt en wacht op actie] exit [we drukken de Enter-toets]
Telkens het zinvol is om de gebruiker een actie te laten uitvoeren stopt het programma. Als we onmiddellijk de Enter-toets indrukken begint REXX het volgende statement uit te voeren.
Maar bij elk pauzemoment hebben we de mogelijkheid in te grijpen in het programma. We kunnen namelijk:
- een geldig REXX bevel geven, bijvoorbeeld een variabele aanpassen met een assignatie, met een trace commando de tracering veranderen of annuleren, de waarde van een variabele opvragen;
- een gelijkheidsteken (=) ingeven om het laatst uitgevoerde statement nogmaals te laten uitvoeren (eventueel na verandering van waarden).
Na het derde statement hebben we de waarde van TijdNu overschreven (de seconden op nul gezet). Na het vierde statement hebben de variabele nogmaals veranderd. Dan hebben we het =-teken ingevoerd. Daardoor is het vierde statement terug uitgevoerd, doch met de nieuwe waarde van TijdNu. Na het vijfde statement geven we er de brui aan met een exit bevel.
Let op !
- Stel dat we alle waarden van een stem willen onderzoeken tijdens een pauzemoment. Dan hebben we een lus nodig. Maar we moeten deze lus compleet in één lijn opgeven, dus bijvoorbeeld do i=1 to mystem.0;say mystem.i;end. Let ervoor op om niet ongewild een variabele van het programma zelf aan te passen. Dus, om die stem te tonen is het misschien beter een andere, minder gewone, lus-variabele te gebruiken, xyz bijvoorbeeld.
- Als we interactief traceren en we geven een trace ? commando mee, dan wordt overgegaan op niet-interactieve tracering en zullen we verder niet meer kunnen ingrijpen in het programma tenzij er terug een call trace ?i in de broncode staat.
- Voeren we tijdens een pauzemoment trace "Off" in, dan worden alle traceringen gestopt, ook de interactieve.
- We kunnen ook een trace commando geven met een getal als parameter. Is het getal positief, dan zal het opgegeven aantal statements worden getoond, doch nu zonder pauzes. Is het getal negatief, dan zal dat aantal statements niet getraceerd, maar natuurlijk wel nog uitgevoerd worden. Handig bijvoorbeeld om een aantal iteraties van een lus niet in detail te bekijken.
Wanneer TRACE bevel en wanneer TRACE functie ?
bewerkenStel dat we een programma hebben met subroutines. Een trace in het hoofdprogramma blijft geldig voor zijn subroutines. Maar met een trace() functie kunnen we in een subroutine de tracering anders laten verlopen. Dat zal helemaal geen invloed hebben op hoe de hoofdroutine traceert.
Misschien willen we de subroutine of functie helemaal niet meer traceren omdat we er zeker van zijn dat ze feilloos werkt. Dan schrijven we aan het begin van de subroutine een call trace "Off". De subroutine wordt niet getraceerd, de tracering van het hoofdprogramma loopt gewoon door na het beëindigen van de subroutine.
Nuttige traceringen
bewerkenMet een Trace "C" traceren we enkel de host-commando's. Het nut ervan is echter wel verminderd in de Windows omgeving. Zoals we zullen zien in een volgend hoofdstuk, vervangen we veel host commando's door functies van het RexxUtil pakket. Voor ons lijken deze functies host commando's, maar voor REXX zijn het functies zoals alle andere en dus zal een "trace ?C" niet stoppen na zo'n functie.
Trace "?E" zal host-commando's enkel traceren als er een fout optreedt. Een MKDIR commando zal onopgemerkt voorbijgaan als alles goed verloopt, maar het programma zal in interactieve mode stoppen indien er een ongeldige naam aan het commando is meegegeven. Maar ook hier dezelfde opmerking als we overschakelen op de RexxUtil functies die we in het volgend hoofdstuk bespreken.
Exitroutine
bewerkenHet is een goede gewoonte om in een programma slechts één algemene exit-routine te voorzien. Deze kan dan eventuele (fout)boodschappen op een gestandaardiseerde manier afdrukken.
Dit is een eenvoudig voorbeeld van zo'n exit-routine:
/* Programma met exit-routine */ ... instructies ... if fout-ontdekt then call foutexit 20,"Bericht 1","Bericht 2" ... instructies ... call exit 0,"Alles is goed verlopen" /*---------------------------------------*/ EXIT: FOUTEXIT: /* Algemene exit-routine */ parse source . . myname a=lastpos('\',myname) parse var myname +(a) myname '.' do i=2 to arg() say myname':' arg(i) end exit arg(1)
Wat leert ons dit voorbeeld ?
- Het is mogelijk meer dan één label op dezelfde regel te schrijven. Ze worden op die manier synoniemen van elkaar. Hier gaan exits met of zonder fouten samen toekomen.
- Onze exit-routine kan geen, één of meerdere parameters hebben. De eerste wordt verondersteld een returncode te zijn. De volgende parameters zijn de tekst die we op het scherm willen zien verschijnen.
- In de exit-routine halen we eerst de naam van ons programma op via een parse source. We strippen het pad van de naam af.
- De functie arg() wordt hier op verschillende manieren gebruikt. arg() zonder parameter retourneert het aantal binnengekomen parameters. arg(0) ware hier intuïtiever geweest, naar analogie met een stem, maar zo werkt het spijtig genoeg niet. Geven we een positief getal mee aan arg(i), dan geeft dat toegang tot de i-de parameter die aan de (exit-)routine is meegegeven.
- Hier behandelen we eerst alle parameters vanaf de tweede t.e.m. de laatste. Deze zijn berichten die we op het scherm zetten, voorafgegaan door de naam van het programma. De naam toevoegen is vooral nuttig indien we nog vanuit een ander REXX programma zijn opgeroepen. Dan weten we tenminste wie ons een bericht geeft. Meer nog, op die manier onderscheiden we onze berichten van die van host-commando's.
- Als laatste gebruiken we het exit bevel, met de eerste parameter die een returncode voorstelt. Het is spijtig dat Windows - in tegenstelling tot bijvoorbeeld z/VM - deze foutcode niet op het scherm toont. Maar indien ons programma door een ander was opgeroepen, dan geven we op die manier wel de returncode op een standaardmanier terug. Het blijft dus een goede gewoonte een adequate returncode te geven op het einde van het programma.
Willen we een onderscheid maken tussen een oproep vanuit de host of vanuit een ander programma, dan kunnen we als volgt uitbreiden:
/*-----------------------*/ EXIT: FOUTEXIT: /* Algemene exit-routine */ /*-----------------------*/ parse source . oproepvorm myname a=lastpos('\',myname) parse var myname +(a) myname '.' do i=2 to arg() say myname':' arg(i) end if oproepvorm='COMMAND' then if arg(1)<>"" & arg(1)<>0 then say myname': Foutcode='arg(1) exit arg(1)
Het tweede woord (oproepvorm) dat door parse source wordt gegeven kan volgende waarden aannemen:
- COMMAND als het programma vanuit de host is opgeroepen;
- SUBROUTINE als het programma vanuit een ander (REXX) programma is opgeroepen als een subroutine (dus met een call).
- FUNCTION als het programma vanuit een ander (REXX) programma is opgeroepen als een functie.
Hier zullen we zelf zorgen om de returncode op het scherm te zetten, maar alleen als we vanuit de host zijn opgeroepen en als het een fout betreft.
Fouten intercepteren tijdens uitvoering
bewerkenREXX laat ook toe een aantal fouten tijdens uitvoering te intercepteren. Dit kan dank zij de bevelen signal on conditie of call on conditie.
Al naargelang het besturingssysteem en de versies van REXX kunnen min of meer soorten fouten worden geïntercepteerd. Maar normaal gezien zouden alle implementaties van REXX de volgende moeten aanbieden:
- NOTREADY - conditie die optreedt indien het besturingssysteem een apparaat moet benaderen dat niet gereed is (bv. geen CD in lade).
- SYNTAX - conditie die optreedt indien er een fout tegen de syntaxis wordt ontdekt. Hoewel OORexx het heel goed doet om veel van die fouten te ontdekken, nog voor hij met de uitvoering begint, blijft het nuttig ze nog verder op te vangen. REXX kan bijvoorbeeld pas tijdens uitvoering ontdekken dat variabele nr in "say left('xxx',nr)" niet met een geheel getal is opgevuld, en dus dat de functie niet kan werken. In sommige gevallen kan de programmeur zelf syntaxis fouten willen behandelen om bv. meer specifieke informatie te geven. Zo kan men tijdens de behandeling van database records zeggen welk record er behandeld werd op het moment dat de fout optrad. Door een "Signal on Syntax" in een subroutine te gebruiken, kan het hoofdprogramma desgewenst zelfs verder gaan na de fout. Een voorbeeld hiervan krijgen we verder.
- NOVALUE - meldt onmiddellijk als een niet geïnitialiseerde variabele in een instructie wordt gebruikt. We zijn al verschillende gevallen tegengekomen waar dit tot problemen kan leiden. Een goed REXX programma zou eigenlijk steeds een signal on novalue moeten gebruiken. Dit vereist dan wel nog meer discipline van de programmeur, want alles wat constant is (zoals bv. host commando's), moet dan al zeker als constante worden geschreven. Het voordeel is echter dat men schrijffouten in namen van variabelen snel ziet. Anders zou een if misschien een fout pad inslaan en pas veel later alles in het honderd laten lopen.
- ERROR - conditie die optreedt als een host-commando een fout geeft.
Met deze kennis kunnen we onze exit-routine sterk uitbreiden:
signal on Syntax signal on Notready signal on Novalue signal on Error ... hier komt ons programma ... call exit 0,'Alles goed verlopen' /*----------------------------------------------------------------*/ Syntax: /* We komen hier bij een syntax fout */ /*----------------------------------------------------------------*/ call foutexit 99,'REXX syntaxfout op lijn' sigl,, sourceline(sigl),, 'REXX zegt:' errortext(rc) /*----------------------------------------------------------------*/ NotReady: /* We komen hier als een apparaat niet gebruiksklaar is */ /*----------------------------------------------------------------*/ call foutexit 99,'REXX probleem op lijn' sigl,, sourceline(sigl),, 'Een apparaat is niet gebruiksklaar.' /*-----------------------------------------------------------------*/ NoValue: /* we komen hier bij een niet-geïnitialiseerde var */ /*-----------------------------------------------------------------*/ undefvar= 'CONDITION'('D') call foutexit 99,'REXX probleem op lijn' sigl, 'variabele' undefvar 'is niet gedefiniëerd.' /*-----------------------------------------------------------------*/ Error: /* We komen hier als een host-command een returncode<>0 gaf */ /*-----------------------------------------------------------------*/ cmderr= 'CONDITION'('D') call foutexit rc,'REXX probleem op lijn' sigl,, 'Commando' cmderr 'gaf foutcode='rc
De Exit-routine zelf is onveranderd gebleven en hebben we hier niet meer herhaald.
We kunnen hier het volgende leren:
- De signal on instructies bereiden ons voor om verschillende soorten fouten te intercepteren. Deze kunnen gelijk waar in het programma worden geplaatst en men kan ze terug afzetten met een signal off conditie. Gebruik enkel "signal on" voor wat zin heeft voor het programma. Maar altijd en overal deze subroutines voorzien kan op zich nooit kwaad.
- De gereserveerde variabele sigl bevat het lijnnummer vanwaar we gekomen zijn, dus de lijn waar de foutconditie is opgetreden.
- De functie sourceline(sigl) haalt de inhoud van die bewuste lijn uit de broncode op.
- De functie errortext(code) geeft de (Engelse) tekst die bij de fout hoort.
- De functie condition('D') geeft de beschrijving van de fout boven (Description). We kunnen ook de Condition zelf opvragen (met ERROR, NOVALUE... als antwoord), of de Status die ON of OFF kan zijn.
- Tot slot: We hebben geen fout geschreven wanneer we twee komma's aan het eind van een lijn hebben gebruikt (bij "call foutexit") ? Probeer zelf te begrijpen waarom dit zo moet zijn !
Het intercepteren van fouten hebben we hier met een signal on instructie geactiveerd, maar we kunnen ook call on gebruiken om fouten te intercepteren (behalve voor syntaxfouten). In dat geval kunnen we in de subroutine die de fout behandelt, beslissen om toch terug te keren naar het hoofdprogramma. Maar of dit zinvol is hangt af van het hoofdprogramma. Meestal kunnen we toch niet veel zinnigs meer doen na zo'n fouten.
We kunnen de condities ook onderweg aan- en afzetten. Zo kunnen we enkel op subroutine-niveau fouten gaan onderscheppen. Het onderscheppen stopt automatisch bij het einde van de subroutine.
Onderstaand stukje programma geeft bijvoorbeeld een eenvoudige manier om te testen of een datum geldig is:
signal on Syntax parse arg startDatum andereParms startDatum=DatumValidatie(startDatum) say 'startDatum='startDatum exit DatumValidatie: /* we aanvaarden datums in formaat jjjjmmdd of jjjj-mm-dd */ /* de datum die we terug geven heeft altijd formaat jjjjmmdd */ signal on syntax name DatumNietSorted /* Interceptieroutine met naam */ return date('S',arg(1),'S') DatumNietSorted: signal on syntax name DatumNietISO return date('S',arg(1),'S',,'-') DatumNietISO: call foutExit 1949-11-16,'Foute ingavedatum:' arg(1),, 'we verwachten formaat jjjj-mm-dd of jjjjmmdd'
Wat leren we hier ?
- Aan een signal on of een call on kan een name onzenaam gegeven worden, zodat we een meer sprekende naam voor de interceptieroutine kunnen kiezen, of - zoals in bovenstaand voorbeeld het geval is - verschillende routines die fouten intercepteren mogelijk worden (gele en groene routines);
- In zekere zin hebben we hier een voorbeeld waarbij de interceptieroutine toch teruggaat naar het hoofdprogramma. De subroutine zelf wordt abrupt afgebroken;
- De signal on instructies worden pas aangezet in de subroutines of functies.
- De leuke returncode 1949-11-16 helemaal op het einde, stelt een datum voor (niet toevallig de verjaardag van de schrijver van deze tekst). En hij werkt zonder problemen, al zal het resultaat 1922 geven...