OS/2 Procedures Language - REXX
By Roger Orr
Why this article needs to be written
One of the things about OS/2 which I continually find interesting is how similar and yet how different it is to DOS. The advantage of being like DOS is that things feel familiar to those migrating to OS/2; the drawback is that the enhancements are not used because people think they already know it all from DOS.
A good example of this is 'batch' files: first versions of OS/2 had a few extensions to batch file handling, and long standing readers of Pointers may recall Tony Harris's article in issue 6 (Jan/Feb '90) where he described some of the additions OS/2 has in this area. However, these additions were fairly limited - for example in the article a program had to be written to get the user's reply to a simple Yes/No question.
In more recent versions of OS/2 however, IBM have included the so-called OS/2 procedures language, or REXX (which stands for REstructured eXtended eXecutor language if you must know...). Despite its name it is a very useful feature of OS/2, but suffers from the problem described in paragraph one: most users of OS/2 don't even know it exists!
For this reason I felt a technical tip giving a 'taster' of some of the things which REXX allows you to do might dispel some of this ignorance. I apologise to those of you (mostly ex IBM mainframe users!) who already write programs in REXX - this is going to be a couple of 'starter' programs to get people started.
Introduction to REXX
So what is REXX? REXX is a programming language, with some of the simplicity of BASIC, which allows you to include OS/2 commands in your program. Support for REXX programs is built into the standard command shell CMD.EXE as an extension to the .CMD batch files everyone who uses OS/2 has used - if a batch file starts with a REXX comment ( '/* ... */' ) then it is treated as a REXX program.
REXX is an interesting programming language: it is a free-form structured language, with constructs like if..then..else and do..end; it is typeless, which means that variables have no type (ie unlike 'C' where variables are 'int' or 'char *' all REXX variables are just character strings of arbitrary length); and, rather unusually, uninitialised variables have a default value of the variable name itself, in capitals.
Any commands which are not recognised by REXX will be passed on to CMD.EXE to be processed.
REXX has a set of functions for input/output, string manipulation and also a range of expressions, some operating on all strings and others on those which are recognised as numbers.
A trivial REXX program is:
/* Comment - all REXX programs have one */ SAY hello everyone
If you type this in using a text editor and type 'hello' from the command prompt, the program displays HELLO EVERYONE on the screen. This is because 'say' is recognised as a REXX command, with arguments 'hello' and 'everyone'.
Since neither of these variables are initialised their values are 'HELLO' and 'EVERYONE', so these two words are written out. If you wanted to retain the case of the words, simply enclose then in quotes to tell REXX that they are strings, not variables, and the string itself is displayed.
A full list of REXX commands is beyond the scope of this article. There is a short description of REXX in the OS/2 User's Guide, and a User's Guide and Reference specifically for REXX are also available. A few of the commonest commands will be used later in this article.
REXX can also use function calls as well as commands - and REXX functions can take a variable number of arguments. An inbuilt function 'args' allows the called function to determine how many and what type of arguments were passed to it.
You should be aware that external functions, callable by REXX, can be written in REXX, C, Pascal or MASM. This feature allows creation of a program where the shell is written in REXX and the 'real work' is done by a compiled piece of code for efficiency. In addition, REXX can pass external commands to other environments than just CMD.EXE, and so can be used as a general macro language. However, I will not touch on either of these techniques further in a first introduction to REXX.
Since this is a 'technical tips' article, I will now describe two REXX programs which, although of some use by themselves, more importantly show the sort of things which can be done with REXX.
REXX example programs
Each program is shown containing comments of the form '/* n */' - these refer to the notes at the end of each program.
For clarity, all REXX reserved words, commands and functions are shown in upper case. This is a recommended, but not obligatory, style.
Example 1: math.cmd
This program runs a very simple full screen expression evaluator. You can type in arithmetic expressions like '2 + 2' or '2 ** 3 + (7.6 / 1.2)' and the value of the expression is displayed. If invalid expressions are used the program writes '** Unable to evaluate expression **'.
/* Simple expression evaluator using REXX */ /* 1 */ esc = x2c('1b') title = esc'[02;0H'esc'[K Simple expression evaluator' banner = esc'[10;0H'esc'[K Result of: ' answer = esc'[12;0H'esc'[K is: ' prompt = esc'[20;0H'esc'[K Enter expression or quit ->' '@cls' /* 2 */ SAY title DO FOREVER CALL CHAROUT 'screen$', prompt /* 3 */ inp = LINEIN() /* 4 */ PARSE UPPER VALUE inp WITH expression /* 5 */ IF expression = 'QUIT' THEN LEAVE CALL evaluate /* 6 */ CALL CHAROUT 'screen$', banner||expression /* 7 */ CALL CHAROUT 'screen$', answer||res END RETURN res EXIT evaluate: /* 8 */ SIGNAL ON SYNTAX /* 9 */ INTERPRET 'res = ' expression /* 10 */ IF \DATATYPE(res, 'num') THEN /* 11 */ SIGNAL SYNTAX /* 12 */ RETURN syntax: /* 13 */ res = '** Unable to evaluate expression **' RETURN
Notes on math.cmd:
- Esc is set to the 'escape' character using the inbuilt hex to char function X2C. This variable is then used to build up the ANSI escape sequences for the various display strings.
- cls is used to clear the screen in this example although it is easier to use the ANSI control sequence. The initial '@' causes the CMD.EXE echoing to be suppressed, and the command is quoted to ensure REXX does not try and parse the text.
- Charout writes the string to the file named in the first argument. The usual command to do this, 'say', appends a newline.
- The first statement reads a line of input (usually the 'pull' command would be used but this causes a prompt to be written in some versions of OS/2).
- The PARSE statement 'parses' the input line, into a variable called 'expression'. The UPPER modifier first coerces the text to upper case, and the VALUE modifier tells parse to operate on the value of the variable 'inp'. The parsing command is very general purpose and allows a range of parsing methods, only a small subset of which are used in this article.
- A function is called to evaluate the expression typed in - this is to allow localisation of error handling.
- The || operator concatenates strings.
- The evaluate function starts here.
- This tells REXX to jump on any syntax errors - to the default label which is called SYNTAX. Other conditions can be signalled on such as external command errors. Syntax error occur if the user types a 'silly' expression like '/ 2' or 'a + b'.
- This line does the work! It generates a string by putting the value of 'expression' together with 'res = ' and then interprets this string as a REXX statement. Any errors will cause a jump to the 'syntax' label.
- Since all variables in REXX are strings, 'res' may contain a string which is not a number. For example if expression was 'fred', the variable res would now be 'FRED'. The 'datatype' function checks whether the variable is a valid number: other options for datatype include 'alphanumeric', 'binary' and 'hex'.
- This is an example of generating an error condition - in this case to force the program to go to 'syntax'.
- This is the point where execution continues on syntax errors - res is set to an error string and the function returned from. Note that 'syntax' is not itself a function - it is jumped to and so it is returning from the function running when the syntax error occurred. An alternative construct, CALL ON xxx, can be used instead of SIGNAL ON xxx in cases where you do want to return to the statement causing the error.
Example 2: modlist.cmd
This example shows the modules used by a program. For example "MODLIST C:\OS2\PMSHELL.EXE" will display all the modules (DLLs) which are required by the program and the directory the are located in. As a demonstration of recursion in REXX the program checks all dependencies of the module tree.
Note that this program works with both OS/2 1.x and 2.0 binaries.
/* REXX procedure to give a list of modules (DLLs) used by a program */ PARSE ARG filename scrap /* 1 */ IF filename = '' THEN DO SAY 'filename expected' EXIT END IF STREAM( filename, 'C', 'QUERY EXISTS') = '' THEN DO /* 2 */ SAY filename 'not found' EXIT END CALL getlibpath /* 3 */ modlist. = '' /* 4 */ modlist.DOSCALLS = 'OS/2 kernel' /* Predefined */ RETURN doone( filename) /* 5 */ /**************************************************/ doone: PROCEDURE EXPOSE modlist. libpath. libcount /* 6 */ PARSE ARG filename IF checkit( filename) \= 0 THEN RETURN 1 numq = 0 DO QUEUED() /* 7 */ PULL x numq = numq + 1 list.numq = x /* 8 */ END DO j = 1 TO numq x = list.j IF modlist.x = '' THEN DO /* 9 */ DO i = 1 TO libcount test = libpath.i || '\' || x || '.DLL' IF STREAM( test, 'C', 'QUERY EXISTS') \= '' THEN DO modlist.x = libpath.i SAY x '-' libpath.i IF doone( test) \= 0 THEN /* 10 */ RETURN 1 LEAVE /* 11 */ END END IF modlist.x = '' THEN SAY '*** module' x 'could not be found on the LIBPATH ***' END END RETURN 0 /**************************************************/ checkit: PROCEDURE EXPOSE filename header = CHARIN( filename, 1, 2) /* 12 */ IF header \= 'MZ' THEN DO SAY filename 'is not executable' RETURN 1 END newoff = CHARIN( filename, 60 + 1, 4) newoff = C2D( REVERSE( newoff)) + 1 /* 13 */ header = CHARIN( filename, newoff, 2) SELECT WHEN header = 'LE' THEN DO /* 14 */ impmodtbl = CHARIN( filename, newoff + 28 * 4, 4) impmodtbl = C2D( REVERSE( impmodtbl)) impmodcnt = CHARIN( filename, newoff + 29 * 4, 4) impmodcnt = C2D( REVERSE( impmodcnt)) CALL STREAM filename, 'C', 'SEEK ' || impmodtbl + newoff DO impmodcnt len = C2D( CHARIN( filename, , 1)) module = CHARIN( filename, , len) QUEUE module END END WHEN header = 'NE' THEN DO modtab = CHARIN( filename, 20 * 2 + newoff, 2) modtab = C2D( REVERSE( modtab)) imptab = CHARIN( filename, 21 * 2 + newoff, 2) imptab = C2D( REVERSE( imptab)) modlen = imptab - modtab CALL STREAM filename, 'C', 'SEEK ' || modtab + newoff DO i = 1 TO modlen / 2 val = CHARIN( filename, , 2) modptr.i = C2D( REVERSE( val)) END /* work through every item in the table */ DO i = 1 TO modlen / 2 CALL STREAM filename, 'C', 'SEEK ' || modptr.i + imptab + newoff len = C2D( CHARIN( filename, , 1)) module = CHARIN( filename, , len) QUEUE module END END OTHERWISE DO /* 15 */ SAY filename 'is not an OS/2 executable program' RETURN 1 END END CALL STREAM filename, 'C', 'CLOSE' RETURN 0 /**************************************************/ getlibpath: PROCEDURE EXPOSE libpath. libcount filename= "c:\config.sys" libcount = 0 DO count = 1 UNTIL LINES( filename) = 0 /* 16 */ check = LINEIN( filename) IF SUBSTR( check, 1, 8) = "LIBPATH=" THEN DO libpath = SUBSTR( check,9) DO WHILE libpath \= '' libcount = libcount + 1 PARSE VALUE libpath WITH libpath.libcount ';' libpath /* 17 */ END LEAVE END END CALL STREAM filename, 'C', 'CLOSE' RETURN 0
Notes on modlist.cmd:
- get arguments - discard all but the first one
- The stream function allows you to perform file specific operations - in this case query existence of a file.
- Here CALL is used for our own procedure getlibpath - in the previous example it was used for intrinsic commands.
- This is an example of a 'compound' variable. "modlist." is the stem (a bit like an array name in C), modlist.DOSCALLS is a specified item. Here we set a default value for all variables with the stem modlist.
- Pass the filename to the recursive procedure doone.
- doone is defined with the 'procedure' keyword to indicate that all variables are hidden and per-instance, EXCEPT for the ones listed after the expose keyword.
- checkit places its results on the default REXX queue, which is processed by the DO loop. QUEUED() returns the number of items on the queue.
- 'list.numq =' assigns to list.1, list.2, etc. as for a simple C array.
- 'modlist.x' is using a non-numeric subscript - we are using this feature of REXX variables so the list of known modules is maintained for us by REXX. 'x' will be the current module, for example PMWIN, and modlist.PMWIN will be equal to '' (see note 4) unless it has been already processed. This allows us to build up a tree of dependent modules.
- Here we call ourselves recursively - variables like i, j and even 'list.' are local and so will not be affected by the call, but 'modlist.' is exposed and so changes will remain on exit.
- The leave statement in a simplest form exits the nearest DO loop.
- Here we process the executable file format of the given filename. CHARIN allows us to read binary data from the file, at a given offset and a given length.
- C2D and REVERSE are used to convert Intel byte-reversed binary into a REXX number. Note that explanation of the offsets and formats used in this function is outside the scope of this article!
- The SELECT statement allows matching on a flexible set of criteria although in this case we are checking the same variable in each WHEN clause. 'LE' is the 'magic cookie' for OS/2 2.0 style EXE headers, and 'NE' for OS.2 1.x headers.
- OTHERWISE should be self-explanatory...
- The LINES function returns the number of lines left to process in the file - the first call automatically opens the file.
- Another example of PARSE. Note that we can use the same variable both as the thing to be parsed and as one of the variables to contain the result. Also note that libpath and libpath. can be used without confusion as different variables.
Use of REXX is particularly helpful for cases where a simple program is required, for example installation programs, but the standard OS/2 batch facility lacks the flexibility and usability of REXX. However the range of features and extensibility of REXX means it is by no means restricted to these cases.
I hope that these examples of the use of REXX to perform actions which would more usually require the writing of a compiled program will encourage more people to experiment with REXX.