9. Parsing ========== A questo punto, possiamo considerare la possibilita' di lanciare un programma REXX con degli argomenti sulla linea di comando. Vediamo un esempio: /* Legge i propri argomenti */ parse arg a.1, a.2, a.3, a.4 do i = 1 to 4 say "L'argomento " i "vale:" a.i end Eseguitelo, aggiungendo per esempio "alfa beta gamma delta" dopo il nome del programma (per esempio, parse alfa beta gamma delta). Il programma stam- pera' come risultato L'argomento 1 vale: alfa L'argomento 2 vale: beta L'argomento 3 vale: gamma L'argomento 4 vale: delta L'argomento complessivo, la frase "alfa beta gamma delta", e' stato suddiviso in quattro componenti, separate tra di loro all'altezza degli spazi. Se provate a inserire meno di quattro argomenti noterete che le ultime componenti verranno stampate come vuote, e se scrivete piu' di quattro compo- nenti, l'ultima frase conterra' tutti i dati in eccesso. Se inoltre c'e' piu' di uno spazio tra i vari argomenti, questi spazi extra verranno cancellati, eccetto quelli dell'ultimo componente l'argomento completo. Questo comportamento e' chiamato "Tokenisation". Oltre agli argomenti, e' possibile elaborare in questo modo anche l'ingresso: provate a rimpiazzare "parse arg" con "parse pull" nell'esempio pre- cedente. Noterete che al lan- cio, l'ingresso (una singola stringa) verra' "tokenizzato". Se rimpiazzate "parse pull" con "parse upper", l'ingresso, oltre alla tokenizzazione, verra' trasportato tutto in maius- colo (dove possibile, ovviamente). Anche altri dati possono subire il processo di parsing. "parse source" elabora nello stesso modo informazioni su come il programma e' stato lanciato, mentre "parse version" ela- bora le informazioni di versione dell'interprete REXX. Comunque, i due usi piu' importanti dell'istruzione parse sono senza dubbio "parse var [variabile]" e "parse value [espressione] with...". Queste permettono l'elaborazione di dati arbitrari forniti dal progrmma. Esempio: /* Acquisisce informazioni su data e ora */ d = date () parse var d giorno mese anno parse value time () with ora ":" minuti ":" secondi Vediamo qui due modi di parsing differenti. Invece di tokenizzare tutto il risultato della funzione "time ()", lo dividiamo in tre pezzi all'altezza del carattere ":". Cosi', per esempio, "17:44:11" viene diviso, dopo il parsing, in 17, 44, e 11. Il comando "parse" accetta la presenza di una stringa, che fa da maschera (template) di ricerca. Questa stringa e' speci- ficata tra virgolette, per esempio in parse arg primo "beta" secondo "alfa" Questa frase assegna alla variabile "primo" qualsiasi cosa appaia PRIMA della scritta "beta", e alla variabile "secondo" tutto cio' che appare tra le scritte "beta" e "alfa". Ovviamente se la scritta "beta" non appare nella stringa di argomenti, tutta la stringa stessa verra' assegnata a "primo": a "secondo" competera' invece la stringa vuota. Se "beta" e' presente e "alfa" no, tutto quello che segue "beta" verra' assegnato a "secondo". E' poi possibile tokenizzare pezzi di input che appaiono tra maschere diverse: per esempio, parse arg "alfa" prima seconda "beta" tokenizza tutto cio' che appare tra "alfa" e "beta" e piazza i token, secondo le regole viste, nelle due variabili "prima" e "seconda". Piazzare un punto al posto di un nome di variabile durante la tokenizzazione comporta la rimozione di quel particolare token; parse pull a . c . e mantiene il primo, il terzo e il quinto token; perde il secondo e il quarto. Il punto e' un ottimo sistema per evitare argomenti in eccesso: piazzandolo alla fine delle variabili su cui effettuare il parsing, per esempio in parse arg a b c d e . ci si assicura che i token in eccesso non vengano usati, e in piu' cancella tutti gli spazi relativi all'ultimo token (dato che solo l'ultimo token puo' contenere spazi, il punto ci assicura che questi spazi non verranno MAI considerati). Infine, e' possibile effettuare il parsing per posizioni nu- meriche nella stringa, anziche' tramite i template: queste posizioni partono da 1 per il primo carattere. parse var a 6 alfa + 3 beta + 5 gamma Il valore di "alfa" sara' dato dai tre caratteri di "a", partendo dal carattere n.6: "beta" sara' riempito con i cinque caratteri successivi, "gamma" invece conterra' tutta la parte finale di "a". 10. Interpret ============== Supponiamo di avere una variabile "istr" che contenga una stringa come, per esempio, "a = a + 1", ossia un'istruzione REXX. Questa stringa puo' essere eseguita come una istru- zione tramite il comando Interpret: interpret istr informera' l'interprete REXX di eseguire la stringa contenuta in "istr". L'istruzione "interpret" puo' essere usata per accettare istruzioni REXX dall'utente, o per assegnare valori a variabili i cui nomi non siano noti in anticipo. Per esempio: /* Inserisce il nome di una variabile e assegna */ /* a quella variabile il valore 42 */ parse pull var interpret var "=" 42 Un esempio lampante di "interpret" e' nel file REXXTRY.CMD, che permette di provare in maniera interattiva le istruzioni REXX: REXXTRY.CMD e' presente nella distribuzione base di OS/2 ed eCS, alla directory \OS2. Nel file troviamo, per la precisione nel ciclo principale individuato dall'etichetta "main" (vedremo in altra sede cosa sono le etichette), le seguenti due istruzioni call set2 ; trace (trace) /* Need these on same line. */ interpret inputrx /* Try the user's input. */ con le quali, tra le altre cose, viene appunto interpretato cio' che l'utente inserisce. 11. Lo stack ============= Il REXX ha una "catasta" (o stack) per i dati, che viene accessata dalle istruzioni "push", "queue" e "pull". In particolare abbiamo gia' visto che "pull" (o la sua versione completa "parse pull") viene usata per l'input dei dati da utente: se e' presente qualcosa di valido nello stack, "pull" prelevera' dallo stack questa entita'. Esempio: /* Accesso allo stack */ queue "Ciao!" parse pull a /* preleva "Ciao!" dallo */ /* stack e lo immette in a */ parse pull b /* stack vuoto: chiede */ /* l'input all'utente */ push "67890" push "12345" /* due entita', stringhe, */ /* vengono inserite nello */ /* stack */ parse pull c /* c contiene "12345" */ /* E' rimasto un elemento nello stack ! */ La differenza tra "queue" e "push" sta nel fatto che, quando gli elementi vengono estratti dallo stack, gli elementi accodati tramite "queue" escono nello stesso ordine in cui sono stati inseriti (FIFO o First In First Out, come la coda dei messaggi delle applicazioni PM): quelli spinti nello stack dalla direttiva "push" appaiono in ordine inverso, dando uno stack di tipo LIFO (Last In First Out). 12. Subroutines e nuove funzioni ================================= Definiamo una funzione e diamole un nome: questa funzione e' interna al programma, ne fa cioe' parte integrante. /* Definizione di una funzione */ say "I risultati sono:" square (3) square (5) square (9) exit square: /* Torna il quadrato del suo argomento */ parse arg in return in * in /* o anche in**2 */ L'uscita del programma e' la seguente: I risultati sono: 9 25 81 Quando l'interprete trova la chiamata a "square (...)", cerca lungo il programma un'ETICHETTA, chiamata appunto "square". In questo caso la trova alla riga 5, seguita dal due punti che appunto la distingue come etichetta. L'interprete esegue le istruzioni successive fino ad arrivare all'istruzione "return ...": intanto che queste istruzioni vengono eseguite, gli argomenti della funzione possono essere recuperati con l'istruzione "parse arg" esattamente come gli argomenti al lancio di un programma: quando si arriva all'istruzione "return", il valore specificato viene valutato e usato come valore di ritorno della funzione (nel nostro caso "square"). L'istruzione "exit" che appare alla terza riga causa il termine dell'esecuzione del programma, che altrimenti prose- guirebbe eseguendo (scorrettamente) la funzione. Una funzione che usa piu' di un argomento puo' essere defi- nita semplicemente separando gli argomenti con una virgola: come ad esempio /* Funzione che accetta tre argomenti */ say "I risultati sono:" condizione (5, "Si'", "No") condizione (10, "X", "Y") exit condizione: /* Se il primo argomento e' minore di 10 */ /* viene ritornato il secondo, altrimenti */ /* il terzo */ parse arg c,x,y if c < 10 then return x else return y Una subroutine (in Pascal, una procedura) e' simile a una funzione, eccettuato il fatto che non ritorna alcun valore. Le subroutines vengono chiamate tramite l'istruzione "call subroutine...": per esempio, /* Definisce una subroutine che stampa una stringa */ /* in una cornice e la chiama */ call box "Questa frase e' incorniciata" call box "Questa domanda e' incorniciata?" exit box: /* Questa e' la procedura */ parse arg text say "+--------------------------------+" say "|"centre (text, 32)"|" /* centra il testo */ say "+--------------------------------+" return Nota bene: la funzione "centre" e' predefinita in REXX! Una funzione, anche una predefinita, puo' essere chiamata come una subroutine. Il risultato ritornato dalla funzione e' piazzato in una variabile chiamata "result". /* Stampa la data usando l'istruzione "call" */ call date "N" say result Se una funzione o una subroutine non necessita delle varia- bili usate dal chiamante, o se al contrario usa variabili che non interessano il chiamante, la funzione puo' iniziare con l'istruzione "procedure". Questo ripulisce tutte le varia- bili esistenti non viste, e prepara un nuovo set di varia- bili. Il nuovo insieme verra' distrutto all'uscita della funzione (in pratica verranno create delle variabili locali). Questo programma di esempio calcola il fattoriale di un nume- ro intero, in maniera ricorsiva (per cui, la ricorsivita' e' ammessa in REXX!): /* Calcola il fattoriale di x, 1*2*3*4*...*x */ parse pull x say "x! =" factorial (x) exit factorial: procedure parse arg p if p < 3 then return p else return factorial (p-1) * p La variabile "p", che conserva l'argomento della funzione fattoriale, non e' modificata dalla funzione (factorial (p-1)), in quanto viene nascosta dall'istruzione "procedure". Ad ogni chiamata della funzione vengono create variabili lo- cali, che cioe' esistono e sono viste SOLO all'interno di QUELLA chiamata alla funzione. Se la subroutine o la funzione ha bisogno pero' di un accesso ad alcune variabili, e' possibile estendere la direttiva "procedure" usando "procedure expose" seguito dalla lista di nomi delle variabili che dovranno essere esposte, cioe' rese visibili. Per questa particolare caratteristica consiglio la consultazione della Guida in Linea "Informazioni sul REXX", che qui e' abbastanza esauriente. Funzioni e subroutines possono essere contenute in programmi REXX differenti (e' buona pratica). Basta scrivere le fun- zioni e salvarle in un file il cui nome possa essere riconosciuto dall'interprete: queste funzioni vengono chia- mate funzioni ESTERNE (external), in contrasto con le funzio- ni definite nel programma in esecuzione, che vengono invece chiamate INTERNE (internal). Per esempio, vogliamo usare una funzione esterna chiamata "asino ()", o una subroutine con "call asino": nel salvarla le daremo nome "asino.cmd", e la salveremo in un percorso contenuto nella variabile di sistema PATH di OS/2 - eCS (specificata in CONFIG.SYS: la cosa piu' semplice da fare, e forse la migliore, e' salvarla nella stessa directory del programma principale che la chiamera'). Per le funzioni esterne, l'istruzione "procedure" non e' necessaria, perche' viene eseguita automaticamente prima dell'esecuzione della funzione dall'interprete REXX: una funzione esterna non e' in alcun modo in grado di accedere all'insieme delle variabili del chiamante, a parte il passaggio dei parametri. Il ritorno da una funzione esterna e' effettuato con l'is- truzione "return" o con l'istruzione "exit": "exit" puo' essere usata per ritornare dati al chiamante esattamente come "return", ma puo' essere usata per tornare al chiamante la funzione esterna anche quando questo chiamante e' una fun- zione interna (che a sua volta e' in una funzione interna). "exit" e' usato per uscire da un normale programma REXX come abbiamo gia' visto: in questo caso puo' essere passato un argomento a "exit", che fara' da codice di ritorno del pro- gramma (per esempio, per scopi di debugging). 13. Esecuzione di comandi ========================== Il REXX puo' essere usato per l'esecuzione dei comandi del sistema, se questo lo permette (e' il caso dell'interprete di comandi CMD.EXE di OS/2). Quando l'interprete incontra una linea che non corrisponde a un comando REXX, a un assegnamen- to o alla chiamata di una funzione (interna o esterna che sia) tratta la linea come una stringa che viene valutata e passata al processore dei comandi. Un esempio banale e': /* Preleva gli argomenti dalla linea di comando */ /* ed esegue i comandi */ parse arg comando comando "file1" comando "file2" comando "file3" exit (notate che stavolta abbiamo aggiunto exit alla fine). Ognuna delle tre linee dopo "parse arg comando" e' un'espres- sione stringa, che aggiunge il nome dei file "file1", "file2" e "file3" al nome del comando dato come parametro al momento del lancio. Dopo la fine dell'esecuzione di un comando la variabile "rc" assume il valore del codice di uscita dal co- mando stesso (rc vale 0 se il comando ha avuto successo: ha lo stesso significato del valore posto alla fine del comando "exit", di cui abbiamo appena parlato). L'ambiente a cui un comando viene inviato varia da sistema a sistema, ma nella maggior parte dei casi (come in OS/2) e' possibile selezionare in un insieme di possibili ambienti tramite il comando "address", per il quale vi rimando alla Guida in Linea del REXX. 14. Signal =========== L'istruzione di salto incondizionato in REXX viene chiamata "Signal": l'istruzione "signal etichetta" causa un salto immediato al punto del programma contrassegnato dall'etichet- ta specificata (al solito, seguita dal due punti). Se il salto viene invocato all'interno di una struttura di ciclo, o condizionale (select oppure do), non sara' possibile tornare all'interno della struttura, che verra' definitivamente ab- bandonata. Questo perche' l'effettivo scopo di signal in REXX e' sopratutto quello di puntare a routines di gestione di errori, cosi' che il programma possa quantomeno abortire limitando i danni. C'e', pero', una via piu' utile per signal, ossia l'inter- cettamento di particolari errori. Le condizioni che possono essere intercettate sono "syntax" (errore di sintassi), "error" (ogni comando dell'ambiente che, eseguito, da' un co- dice di uscita diverso da 0), "halt" (interruzione dell'ese- cuzione causata dall'utente), e "novalue" (ossia l'utilizzo di un simbolo al quale non e' stato ancora dato un valore). Queste condizioni possono essere intercettate tramite la direttiva signal onmentre il rilevamento e' disabilitato con signal off Quando si verifica una di queste situazioni, il programma salta all'etichetta che ha lo stesso nome della condizione verificatasi. In questo frangente, il rilevamento di errori viene disabilitato: se serve, e' necessario riabilitarlo specificamente. Quando si ha un "signal", la variabile "sigl" viene riempita con il numero di linea che ha causato il salto: se il segna- le e' dovuto a un errore, la variabile "rc" contiene il codi- ce dell'errore. Notare che "sigl" e "rc" non devono essere definite dall'utente, di esse si prende cura l' interprete REXX. Vediamo un esempio: /* Questo programma prosegue fino a che l'utente */ /* non lo interrompe */ say "Premere Ctrl-C per interrompere il programma" signal on halt do i = 1 say i do 10000 end end halt: say "Ahi!" say "Esecuzione abortita alla linea" sigl In effetti, in REXX esistono altre opzioni per la direttiva "signal on/off", per cui vi rimando alla Guida in Linea "Informazioni sul REXX" (e alle prossime pubblicazioni del PIDO/2 sul REXX e l'Object REXX). 15. Tracciamento ================= Il tracciamento degli errori e' descritto ampiamente nella Guida in Linea "Informazioni sul REXX" distribuita con OS/2, per cui vi ci rimando per una descrizione completa. Qui vediamo un piccolo accenno. Se un programma funziona male e vi servono maggiori informa- zioni per scovare gli errori, il REXX permette di eseguire in maniera controllata (tracing) il programma, per vedere piu' da vicino cio' che accade. La forma di tracing piu' comune e' attivata con l'istruzione trace results che comporta la stampa di ogni istruzione immediatamente pri- ma della sua esecuzione, e stampa il risultato di ogni even- tuale calcolo numerico dopo l'esecuzione. Un altro comando utile e' trace ?all che spinge l'interprete a listare ogni istruzione e fermarsi prima della sua esecuzione. L'istruzione puo' essere esegui- ta premendo il tasto Invio: in questo momento e' possibile inserire un comando REXX che verra' interpretato, dopo di che l'interprete si fermera' nuovamente. Questa opzione e' utile per esaminare le variabili definite nel programma in fase di tracing, o altro. Una funzione piu' limitata e' svolta dalla direttiva trace ?labels che porta l'interprete a fermarsi solo quando incontra un'e- tichetta: questo e' utile per impostare dei punti d'arresto (breakpoints) nel programma, oppure per fermare il programma ad ogni chiamata di funzione. Una nota: e' sufficiente in- serire solo l'iniziale dell'opzione di tracciamento, cosi' "trace l" e "trace labels" hanno la stessa funzione. Per le altre opzioni del comando trace, vi rinvio comunque alla Guida in Linea sull'OS/2 REXX fornita nella distribuzione.