Une particularité du langage REXX est la présence d'au moins une pile de données (stack). Elle sert à récupérer la réponse de commandes ou programmes externes et, si on ne peut vraiment pas faire autrement, lire et écrire des fichiers, manipuler des données ou les transmettre entre programmes.
Le contrôle des données dans la pile est délicat et sans précautions peut amener des résultats inattendus. On prendra soin de démarrer un programme principal avec une pile vide (état à sa création) et on veillera à la laisser vide en sortie pour éviter de mauvaises surprises aux suivants.
Les programme "RXQUEUE" et fonction de gestion de pile RxQueue() décrits ci après sont spécifiques aux interpréteurs IBM. Leurs équivalents peuvent être proposés ou non chez d'autres éditeurs... La pile est créée au lancement de l'interpréteur. Pour la vider :
"rxqueue /clear" /* programme externe de gestion de pile ou alternative universelle : */ Do Queued(); Pull . ; End
Sinon pour protéger les données existantes et ne lire que celles ajoutées :
p0 = Queued() /* nombre lignes dans la pile */ Do n Push ... /* ajout n lignes en LIFO */ End Do Queued() - p0 /* lit les n nouvelles lignes */ Pull ... End
Alternative : sauvegarder le contenu initial dans un stem.
La pile créée par défaut est commune à toutes les sessions (fenêtres de commandes) ouvertes. On peut mettre ceci à profit pour communiquer entre programmes, ou au contraire créer une pile pour chacun et éviter ainsi les interférences :
Call RxQueue 'Create', nom /* autres options : 'delete', 'get' et 'set' nom */
La tentative de lecture d'une pile vide provoque l'attente d'une entrée au clavier.
Sous VM et TSO c'est par défaut la pile du moniteur de commande. Elle peut être découpée en tampons (buffers), les données seront stockées puis extraites du dernier créé :
p0 = Queued() "MAKEBUF" /* nouveau tampon */ x = Rc /* numéro du tampon dans Rc */ ... /* écriture dans le nouveau tampon */ i = 0 Do Queued() - p0 /* lecture du nouveau tampon seul */ i = i + 1 Pull ligne.i End "DROPBUF" x /* suppression du tampon x ou du dernier créé si non précisé */
Les données sont accumulées en stem et ne seront traitées qu'après destruction du tampon ; le processus est ainsi à l'abri d'une quelconque interruption et l'état initial de la pile restauré. Pour vider tous les tampons utiliser la commande "DESBUF".
Important :
- Queued() donne toujours le nombre total de lignes dans la pile entière et non pas dans le dernier tampon.
- Une fois le dernier tampon épuisé la continuation de la lecture se fera dans le précédent ou si la pile est vide provoquera l'attente d'une entrée au clavier.
Note : les fonctions de gestion de piles sont ici des commandes système et non plus spécifiques à l'interpréteur.
Tout programme autre que REXX, y compris les commandes propres au système d'exploitation, voir l'instruction Call § 3.5.5.
Les commandes sont composées et soumises en une seule ligne d'instructions, leur code retour lu ensuite dans Rc :
nom = 'fic2' "cp" nom||".txt" nom||".lst" /* sous Linux */ Say Rc
Attention, sous WinX et DOS les commandes internes de COMMAND.COM ou CMD.EXE retournent toujours zéro (en fait le code retour d'exécution du moniteur de commandes lui même), il faudra donc lire leur réponse comme expliqué au paragraphe suivant.
Les environnement destinataires sont :
- 'CMD' sous WinX et Linux (les autres environnements adressables restent à construire à notre connaissance) ;
- 'CMS' et 'TSO' pour les grands systèmes. L'instruction Address change la destination des commandes :
Address 'ISPEXEC' /* ISPF : nouvel environnement destinataire */ "LIBDEF ..." /* commande ISPF */ ... Address 'TSO' "DELETE ..." /* commande soumise à TSO par exception */ ... Address 'TSO' /* redéfinition vers TSO */
Sous VM, lorsque l'environnement CMS est défini, l'ordre de recherche pour exécution suit les types EXEC, MODULE puis commandes internes. En spécifiant l'environnement 'COMMAND' l'ordre devient commandes internes puis MODULEs ; les EXECs doivent être appelés explicitement (la chaîne de commande n'est plus transposée en majuscules, on peut utiliser ainsi des noms de fichiers en mixed case ) :
Address 'COMMAND' "COPY ..." /* commande COPY */ "EXEC MONPROG" /* appel explicite d'un EXEC */ If Rc ...
Sous WinX et Linux les informations retournées sont récupérées dans la pile par filtrage sur "rxqueue" (§ 4.1.1) si disponible, cet utilitaire place la réponse de la commande dans la pile :
"ATTRIB CONFIG.* /S | RXQUEUE" /* DOS */ Do Queued(); Pull ...; End
Bien utiliser le filtre ou tube (pipe) et non la redirection (>) dans la syntaxe. Eventuellement on précise le mode 'FIFO' (par défaut) ou 'LIFO' et le nom de la pile si plusieurs coexistent :
"DIR C:\TEMP\*.* | RXQUEUE /LIFO" pile /* DOS */
Cette syntaxe est spécifique aux interpréteurs IBM ; sans possibilité de passer par la pile la réponse de la commande sera écrite par redirection dans un fichier et ce dernier lu ensuite. Le code est plus lourd mais peut être inclus dans une sous routine :
Call EXECMD "RENAME" fich1 fich2 /* composition cmde et appel sous routine */ ... EXECMD: Procedure Expose cmde. Parse Arg cmde cmde "> CMDE.TMP" /* exécution commande avec redirection */ retc = Rc z = 0 Do While Lines('CMDE.TMP') > 0 /* et lecture, mise en stem */ z = z + 1 cmde.z = Linein('CMDE.TMP') End Call Lineout 'CMDE.TMP' cmde.0 = z Return retc
Sous VM les réponses sont placées directement dans la pile :
"LISTFILE FICH1 LISTING A (LIFO NOH DATE" Pull fn ft fm format nbl nbb . date heure
et sous TSO dans un stem à l'aide de la fonction Outtrap() :
x = Outtrap('liste.') /* ouverture du stem */ "LISTGRP GRP1" /* liste des utilisateurs dans le groupe RACF GRP1 */ x = Outtrap('OFF') /* fin de capture */ /* liste.0 contiendra le nombre de lignes de la réponse */
Bien étudier les options des commandes et le format de leur réponse, en particulier :
- sous WinX la présentation du résultat de "DIR" diffère suivant le système de fichiers (FAT, FAT32, NTFS) ;
- sous Linux les commandes peuvent être personnalisées dans le bash profile.
Ce ne sont plus de simples programmes à qui on passe des paramètres en argument mais des sous systèmes avec leur propre interpréteur interactif de commandes. Leur appel depuis une procédure REXX est un peu plus compliqué, voir le § 4.6.
Utiliser la fonction Stream() :
monfic = 'D:\dir1\monfic.txt' If Stream(monfic,'C','QUERY EXISTS')> '' Then ...
et pour plus de renseignements les options 'QUERY DATETIME', 'QUERY SIZE', ou les commandes système au détriment de la portabilité. L'existence d'un fichier ne présume pas du droit d'accès nécessaire ni de sa disponibilité, il peut être mobilisé par une autre tâche.
Une solution simple pour les petits fichiers utilisant les commandes système et la pile :
"TYPE D:\TEMP\FIC.TXT | RXQUEUE /LIFO" /* DOS, le fichier sera lu à l'envers */ "cat /var/temp/fic.txt | sort | rxqueue" /* tri sous Linux */
Sinon à l'aide de la fonction Linein(), les fichiers sont lus ligne par ligne (sans les caractères de fin de ligne) :
entree = 'NOMS.TXT' i = 0 Call Linein entree, x, 0 /* ouverture fichier */ Do While Lines(entree) > 0 i = i + 1 data.i = Linein(entree) /* lecture lignes */ End Call Lineout entree /* clôture fichier */ data.0 = i /* nombre d'enregistrements lus dans l'élément 0 du stem */
Le premier appel de Linein() ouvre le fichier et positionne le curseur à la ligne x ou la première par défaut, sans lecture (0), sinon pour n lignes. Lines() retourne 1 s'il reste des lignes à lire (ou leur nombre, suivant la version de l'interpréteur) sinon 0. Sans spécification de numéro de ligne Linein() lit à partir de la position courante, incrémentée au fur et à mesure. Le fichier est fermé ensuite par Lineout(), sinon il ne sera clos qu'en sortie du programme. C'est la démarche classique de lecture d'un fichier : ouverture, puis lecture tant que la fin n'est pas atteinte et enfin clôture.
Pour lire la ligne x en accès direct, en utilisant le parsing dans cet exemple :
Parse Value Linein(entree,x,1) With a b c
L'écriture est effectuée par Lineout() qui retourne 0 si réussi, sinon 1. Les caractères de fin de ligne sont ajoutés automatiquement. L'instruction d'ouverture, pour un fichier existant, fixe le numéro de ligne du début d'écriture qui est ensuite incrémenté sans notification particulière. Si elle est omise l'ouverture sera implicite et l'écriture se fera à la suite des données présentes. Si le fichier n'existait pas au préalable il sera créé. Pour remplacer un fichier il faudra d'abord effacer l'ancien niveau.
sortie = 'liste.txt' Call Lineout sortie,,1 /* ouverture, ligne 1 */ Do i = 1 To data.0 /* nombre de lignes dans data.0 */ Call Lineout sortie, data.i /* écriture */ End Call Lineout sortie /* clôture fichier */
Pour remplacer la ligne x d'un fichier existant en accès direct, en respectant la longueur initiale de cet enregistrement :
retc = Lineout(sortie,data,x)
La fonction Charin() lit caractère par caractère, de contrôle ou non. On peut préciser la position de début de lecture x et le nombre de caractères à lire à chaque fois (6 dans l'exemple, zéro pour positionner le curseur sans lecture). Chars() donne le nombre de caractères restants :
i = 0 Call Charin entree,x,0 /* ouverture fichier */ Do While Chars(entree) > 0 i = i + 1 data.i = Charin(entree,,6) /* lecture */ End Call Charout entree /* clôture */ data.0 = i /* nombre d'enregistrements lus dans l'élément 0 du stem */
Pour un accès direct à un enregistrement, aussi bien en lecture qu'écriture, la position sera calculée en fonction de son numéro et de la longueur (fixe).
A l'aide de Charout(). La position d'écriture est définie à l'ouverture du fichier, sinon elle sera à la suite des données existantes par défaut.
Call Charout sortie,,1 /* ouverture, position 1 */ Do i = 1 To data.0 Call Charout sortie, data.i End Call Charout sortie /* clôture */
La fonction Stream() ouvre un fichier en définissant l'accès de façon détaillée (attention, les options décrites ne sont pas toujours disponibles suivant l'interpréteur) :
Call Stream fich, 'C', 'OPEN' acces partage mode format If Result = 'READY:' Then ... /* ouverture OK */
- acces : 'READ', 'WRITE' ou 'BOTH', préciser 'APPEND' (ajout) ou 'REPLACE' pour l'écriture ;
- partage : accès partagé en lecture seule 'SHAREREAD', à éviter en écriture ;
- mode : 'BINARY' sinon texte par défaut ;
- format : si mode binaire préciser la longueur d'enregistrement 'RECLENGTH n', les lecture et écriture
pourront alors être effectuées par Linein() et Lineout(), n caractères à la fois, le positionnement
sera défini par un numéro d'enregistrement au lieu d'un nombre de caractères.
Pour positionner le curseur,
- en précisant lecture ou écriture si le fichier est ouvert pour les deux :
Call Stream fic, 'C', 'SEEK =4 READ LINE'
- ou en relatif, plus ou moins x par rapport à la position courante :
Call Stream fic, 'C', 'SEEK +8 WRITE CHAR'
- et pour clore le fichier :
Call Stream fic, 'C', 'CLOSE'
Utiliser les programmes disponibles du système : "SORT" et "FIND" sous DOS, "sort" et "grep" sous Linux.
Il peut être plus avantageux de charger le fichier entier pour des recherches multiples et éviter ainsi
des accès disque répétés. Une alternative est de procéder par accès direct indexé avec des enregistrements de
longueur fixe, en mode binaire.
Sans préciser de nom de fichier la cible est par défaut le périphérique d'entrée ( stdin, le clavier) ou de sortie ( stdout, l'écran) :
- Linein() attend une entrée clavier validée par la touche Entrée, c'est l'équivalent de Parse Pull ;
- Charin(,,n) attend n caractères entrés au clavier, validés par la touche Entrée ;
- SysGetKey(), fonction externe (versions IBM), prend directement le caractère saisi sans validation par la touche Entrée, l'option 'NOECHO' fait qu'il ne sera pas affiché à l'écran ;
- Lineout(,'Hello') est l'équivalent de Say 'Hello' ;
- Charout(,'ABC') affiche 'ABC' à l'écran sans retour à la ligne.
Pour tester la présence d'une saisie au clavier, Chars() et Lines() retournent 0 ou 1 :
If Chars() Then x = SysGetKey('NOECHO') If Lines() Then x = Linein()
Ou un nom de périphérique est spécifié pour les sorties, suivant les syntaxes DOS ou Linux :
Lineout('LPT1','Hello') Charout('/dev/ttSy0','ATDT...')
On aiguille ainsi les entrées et sorties d'un programme suivant le contenu d'une variable : vide, un nom de fichier ou de périphérique. Attention, Lines() et Chars() ne reconnaissent pas la présence de caractères ou non lors de tentatives de lecture des ports série, l'absence de données en entrée provoquera une attente sans fin.
Seules sont possibles les lecture et écriture en mode texte, les commandes s'accommodant des formats fixe ou variable. Sous TSO sont accessibles les fichiers séquentiels et les membres des fichiers "partitionnés".
Sous VM avec la commande "ESTATE" :
fic1 = 'MONFIC LISTING A' "ESTATE" fic1 If Rc = 0 Then ... Else Say 'Fichier inexistant'
ou pour plus de renseignements par "LISTFILE" et ses options.
Sous TSO avec la fonction Sysdsn() :
sortie = " 'F072610.MONFIC.LISTING' " /* nom complet */ If Sysdsn(sortie) = 'OK' Then dispo = "OLD" Else dispo = "NEW ..." "ALLOCATE ... " dispo "REUSE"
ou pour plus d'informations Listdsi() ou la commande "LISTDS". L'existence d'un fichier ne présume pas du droit d'accès nécessaire ni de sa disponibilité.
A l'aide de la commande "EXECIO" via la pile :
"EXECIO * DISKR" fichier x "(FINIS" If Rc = 0 Then Do Queued() /* lecture si OK */ Pull ... End
- '*' pour lire toutes les lignes, sinon préciser n lignes ;
- à partir de la ligne x, facultatif sinon au début par défaut ;
- sous TSO fichier est le nom logique alloué ;
- l'option 'FINIS' clos explicitement le fichier en fin de lecture, sinon clôture en sortie du programme.
Mais la meilleure méthode est la lecture directe en stem, le nombre de lignes sera indiqué dans l'élément 0 :
"EXECIO * DISKR" monfic "(FINIS STEM LIGN." If Rc = 0 Then Do i = 1 To lign.0 /* lign.0 = nbre lignes lues */ ... End
Sous VM la commande "EXECIO" possède de nombreuses options : lecture inversée, à partir d'un argument, etc...
Toujours avec la commande "EXECIO", via la pile :
Do ... ... /* traitement */ Pull ... /* envoi dans la pile */ "EXECIO 1 DISKW" sortie End
ou mieux à partir d'un stem de n lignes :
"EXECIO" n "DISKW" sortie "(FINIS STEM LIGN."
Pour remplacer la ligne x dans un fichier en respectant la longueur initiale de cet enregistrement :
"EXECIO 1 DISKW" sortie x "(FINIS STEM LIGN.x"
Sous VM les fichiers sont toujours écrits en mode 'APPEND' (ajout) si on ne précise pas la première ligne d'écriture, même fermés puis ré ouverts ; pour remplacer un fichier il faut commencer par l'effacer. Sous TSO le comportement dépendra de la disposition prise à l'allocation : 'MOD' (ajout) ou 'OLD' (remplace).
Employer les outils disponibles sur le système ou passer par l'éditeur (XEDIT ou ISREDIT). Sous CMS la commande "EXECIO" propose des options de recherche élaborées.
REXX offre la possibilité d'intercepter et traiter les incidents d'exécution. La méthode élémentaire consiste à interrompre le programme avec un message :
Signal On SYNTAX /* à placer en début de programme pour activer l'interception */ ... Exit /* fin du programme */ SYNTAX: /* traitement de l'erreur */ Say 'Erreur' Rc 'ligne:' Sigl ... Exit
La variable réservée Sigl indique le numéro de ligne incriminée, Rc contient ici le code erreur numérique. Attention, malgré l'appellation 'SYNTAX' les erreurs de syntaxe tels que l'oubli d'un mot clé ou une instruction incorrecte sont signalés par l'interpréteur lors de la revue préalable. En complément peuvent être affichés le message associé au code erreur et la ligne du programme en défaut :
Say Errortext(Rc) Say Sourceline(Sigl)
La même démarche Signal On option intercepte des événements particuliers parmi lesquels,
suivant les versions de REXX :
- ANY : n'importe quel événement ;
- ERROR, FAILURE : erreur dans la soumission, l'exécution d'une commande système ;
- NOVALUE : variable non initialisée ;
- NOTREADY : erreur de lecture ou écriture fichier.
Le label de la sous routine de traitement reprend le nom de l'option. Attention, certaines options
ne retournent pas de code, l'appel de Errortext() sera une erreur !
Si l'erreur est recouvrable on utilise l'instruction Call (sauf pour 'SYNTAX' et 'NOVALUE'). Dans l'exemple ci dessous l'utilisateur entre au clavier une commande système. En cas de faute d'orthographe, syntaxe, ou d'échec, la sous routine demande de ré entrer la commande. Le débranchement causé par la survenue d'un événement désactive l'interception, c'est pourquoi Call On est répétée avant la nouvelle tentative d'exécution, la routine sera appelée de façon récursive jusqu'au succès de la commande ou la sortie volontaire :
Call On ERROR Say 'Entrez votre commande :'; Pull cmd cmd /* soumission commande */ ... Exit ERROR: Say 'Commande incorrecte,' Say 're entrez la commande ou retour pour annuler :' Pull cmd If cmd > '' Then Do Call On ERROR /* réarmement de l'interception */ cmd /* et re soumission commande */ End Return
Mais en général ces pratiques ne doivent pas être appliquées, elles ne sauraient remplacer une programmation rigoureuse : code testé de façon exhaustive et cas d'erreurs prévus... A la limite, nul n'étant parfait, l'exemple ci dessous affiche un message plus convivial que celui de l'interpréteur :
Signal On ANY ... ... Exit /* fin du programme */ ANY: /* traitement de l'erreur */ Say 'Erreur d''exécution à la ligne:' Sigl Say Sourceline(Sigl) If Datatype(Rc,'N') Then Say 'Code' Rc ':' Errortext(Rc) Say 'Prière de prévenir le support' Exit
Pour clore un programme emballé dans une boucle sans fin ou autre :
- sous DOS : touches "control C" (spécifier BREAK=ON dans le CONFIG.SYS) ou
fin du processus dans le gestionnaire de tâches (WinX) ;
- sous Linux : touches "control C", "control D" ou terminaison du processus ;
- sous grands systèmes : touche "appel système" puis commandes "HI" ou "HX".
La plupart des logiciels lisent la pile avant l'entrée clavier. On leur transmet ainsi une séquence de commandes (la dernière étant obligatoirement une sortie afin de revenir !) avant leur appel :
/* tri d'un fichier à l'aide de XEDIT sous VM */ "MAKEBUF" Queue 'SORT * 1 12' Queue 'FFILE' /* save & exit */ "XEDIT" fichier "(NOPROF" "DROPBUF" /* Exécution du pgm DSNTIAD sous DB2 (MVS / TSO) */ "ALLOC..." /* allocations fichiers */ "ALLOC FI(SYSIN) DA(..." /* ordres SQL dans fichier alloué en SYSIN */ "MAKEBUF" Queue "RUN PROGRAM(DSNTIAD) LIB('DSN1.RUNLIB.LOAD') PLAN(DSNTIA22)" Queue "END" /* pour sortir */ "DSN SYSTEM(DSN1)" /* appel du système DB2 */ retc = Rc "DROPBUF" If retc ... /* test du code retour pgm */
L'emploi d'un nouveau tampon :
- préserve l'état initial de la pile, même si l'exécution est défaillante ;
- permet d'écrire les commandes dans l'ordre normal d'exécution (Queue).
Les programmes appelés ne lisent pas la pile. Une solution, leur passer la séquence de commandes dans un fichier, s'ils l'acceptent et si cela marche... On utilisera les redirections et pipes, ce n'est plus du REXX mais du DOS ou du bash.
"FTP monsite.free.fr < cmdes.ftp" /* çà marchait sous OS2, ne pas oublier un 'bye' ou 'quit' en fin */ "sqlite3 maDB < cmdes.sql | rxqueue" /* testé sous Win XP, doit marcher aussi sous Linux, terminer les commandes par '.exit' */
REXX gère très bien les fichiers à accès direct, mais mieux vaut utiliser un système de gestion
de base de données :
- chez IBM les interfaces REXX - DB2 Universal Database existent sous tous les environnements ;
- sous WinX et Linux le logiciel libre Rexx/SQL travaille avec les bases Oracle, DB2, MySQL et SQLite.
Remarque : SQL n'est pas un SGBD mais un langage normalisé de définition, manipulation et contrôle de données contenues dans des bases de type hiérarchique, relationnel ou autre.
© G. Navarre, 2003 - 2006. Màj 05/07/2020.