OS/2 Procedures Language - REXX

From EDM2
Jump to: navigation, search

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 arbritary 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:

hello.cmd:

/* 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 - an 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 **'.

math.cmd:

 /* 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:

(1) 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.

(2) 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.

(3) Charout writes the string to the file named in the first argument. The usual command to do this, 'say', appends a newline.

(4) 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).

(5) 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.

(6) A function is called to evaluate the expression typed in - this is to allow localisation of error handling.

(7) The || operator concatenates strings.

(8) The evaluate function starts here.

(9) 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'.

(10) 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.

(11) 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'.

(12) This is an example of generating an error condition - in this case to force the program to go to 'syntax'.

(13) 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 demostration 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.

modlist.cmd:

 /* 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:

(1) get arguments - discard all but the first one

(2) The stream function allows you to perform file specific operations - in this case query existence of a file.

(3) Here CALL is used for our own procedure getlibpath - in the previous example it was used for intrinsic comands.

(4) 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.

(5) Pass the filename to the recursive procedure doone.

(6) 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.

(7) 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.

(8) 'list.numq =' assigns to list.1, list.2, etc. as for a simple C array.

(9) '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.

(10) 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.

(11) The leave statement in a simplest form exits the nearest DO loop.

(12) 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.

(13) 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!

(14) 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.

(15) OTHERWISE should be self-explanatory...

(16) The LINES function returns the number of lines left to process in the file - the first call automatically opens the file.

(17) 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.

Conclusion

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.