Extending OS/2 batch files

From EDM2
Jump to: navigation, search

By Roger Orr

Part 1

Part 2

Introduction

Most users of OS/2 know about batch files - and if asked many might say that they're just like DOS batch files except they have file extension of "CMD" not "BAT".

This is partly true but misses out a whole area where OS/2 "command" files are far more powerful than simple "batch" files. There are in fact two different command file languages in OS/2; the first is a superset of the DOS batch file and the second is called "REXX".

There are quite a few users of OS/2 who do not know about REXX, and this article is written for such people.

Rexx is a simple to use, but powerful, interpreted language which comes 'for free' with OS/2. When you run an OS/2 command script the command processor reads the first line of the file. If the first line starts with "/*" then the command script is treated as a Rexx program; if not then the command script is treated as a simple batch file.

This two-for-the-price-of-one approach has been in OS/2 since version 1.3 (and any very long term readers of Pointers may recall my article on Rexx in early 1992!) but one problem with this approach is that you can use OS/2 command files for years without finding out about Rexx.

Rexx is documented in the 'Information' folder - look inside the book called 'Rexx information' and this is a good place to look for more details about the items I will cover in this article.

First, a very simple example of a Rexx command script.

---------------------- hello.cmd ----------------------
/* Example Rexx program */
say 'Hello world'

If you create hello.cmd and then run it you will see:

C:>hello
Hello world

C:>

You can't get much simpler than that!

What is happening is that the command shell (cmd.exe) reads the first line, and seeing it contains a Rexx comment runs hello.cmd as a Rexx program. The second line contains one of Rexx's inbuilt commands (SAY) which prints out its arguments on the screen (by default).

Variables

Well this all very well so far - but no different than using the 'echo' command. The first feature available using Rexx is that it supports variables, which are untyped (ie they are not specified with a type of 'long', 'char*', etc) and do not need to be declared. The first time you use a new variable Rexx assigns it a default value - the only slight oddity is that the default value is the variable name itself!

--------------------- hello2.cmd -------------------
/* Example of Rexx variables */
Welcome = "Hello,"
username = "who are you?"
say Welcome username
pull username
say welcome Username

This when run may produce output like this:

C:>hello2
Hello, who are you?
Roger
Hello, ROGER

C:>

Here we see the variables 'welcome' and 'username' being initialised to text strings, being used in the 'say' command and then the 'pull' command being used to get a response from the user. Notice that Rexx is not case sensitive - welcome, Welcome, WELCOME all refer to the same variable.

The 'pull' command reads the input and converts it to upper case. We could have used another flavour of it ('parse pull') to preserve the case of the username.

'Say' accepts a variable number of arguments and by default prints them separated by a space - if we want to join two strings together Rexx does include support for string concatenation - simply use the '||' operator. So the line:

welcome = "He" || "llo t" || "here"

will set the variable 'welcome' to the string "Hello there".

Rexx also supports arithmetic with variables - although all Rexx variables are type-less Rexx will treat them as numbers where this is possible.

So for example:

-------------------- arith.cmd --------------------
/* Simple arithmetic */
say 'Enter two numbers:'
pull a b
say a '+' b '=' a+b
say a '*' b '=' a*b

An example of this in action:

C:>arith
Enter two numbers:
12 13
12 + 13 = 25
12 * 13 = 156

Type checking & error handling

Rexx checks variables at runtime, so if we use 'arith' as follows:

C:>arith
Enter two numbers:
me you
    4 +++   Say a '+' b '=' a + b;
REX0041: Error 41 running C:\rogero\articles\ARITH.CMD, line 4: Bad arithmetic 
conversion

C:>

Rexx converts 'a' and 'b' to numbers when it processes the '+' command on line four; but since 'me' and 'you' are not numberic Rexx throws an error.

We could improve the program to check for this using one of Rexx's built in functions, 'datatype'. Rexx functions can take a variable number of arguments, and datatype is no exception. With just one argument it returns the string 'NUM' if the argument can be treated as a number and 'CHAR' if not. With two arguments it checks whether the first argument matches the criteria given by the second argument, such as 'whole' for whole number (ie integer).

------------------- arith2.cmd -------------------
/* Simple arithmetic -data type checking */
say 'Enter two numbers:'
pull a b
if datatype( a ) \= 'NUM' then do
        say a 'is not a number'
        exit
end
if \datatype( b, "whole" ) then do
        say b 'is not a whole number'
        exit
end
say a '+' b '=' a+b
say a '*' b '=' a*b

Now we explicitly check by using the 'datatype' function whether a and b can be treated as numbers before we try using them. This is precise, but it can be a nuisance, as well as hard to do properly, to check for all the possible errors before doing anything.

An alternative approach would be to make use of Rexx's error handling and trap the error after it occurs rather than checking the data beforehand.

Here is one example:

------------------- arith3.cmd -------------------
/* Simple arithmetic -error handling */
say 'Enter two numbers:'
pull a b
signal on syntax
say a '+' b '=' a+b
say a '*' b '=' a*b
exit

syntax:
say 'Sorry - I cant process your input,'
say 'I got error' rc 'on line' sigl '-' errortext(rc)

Now if we enter the strings 'me' and 'you' we handle the resulting syntax error:

C:>arith3
Enter two numbers:
me you
Sorry - I can't process your input,
I got error 41 on line 5 - Bad arithmetic conversion

C:>

The new statement 'signal on syntax' tells Rexx that if a syntax error occurs then control should pass to a label 'syntax' in the program.

Rexx sets a special veriable 'RC' to the Rexx error code, and we have also used the function errortext which converts the number to a human-readable string.

Rexx will also set a special variable SIGL to the line number in the program where the error occurred, and this number can be displayed (as in this case) or possibly used for error recovery.

Note we have to add an 'exit' to the main control path through the program, or we would continue to execute into the signal handling code!

In practice most programs will use a combination of both methods, probably checking for common errors and using error handling for the rarer problems.

File I/O

Rexx comes with a number of built in functions, which include some file handling. Somewhat like the initialisation of variables, you can use a filename without explicitly opening it or you can open it if you require finer control.

Here for example is a Rexx script to display the 'LIBPATH' setting from the config.sys file (for simplicity it assumes OS/2 was booted from the C: drive):

------------------ libpath.cmd ---------------
/* Display libpath setting */

do while lines("c:\config.sys") > 0

  check = linein("c:\config.sys")

  if substr(check, 1, 8) = "LIBPATH=" then
     say check
end

This makes use of Rexx's loop construct with 'do while ...', together with the lines() function which returns 0 when the end of file is reached.

Inside the loop we read each line from config.sys with the 'linein' function, and then check for the "LIBPATH=" statement.

Additional control over files can be provided by using the 'stream' function. This allows you to, for example, open or close a file. Please refer to the online help for a fuller explanation of all the things you can do with the function - this article will only touch on one of the possible uses of the stream function.

Here for example is a short code fragment which copies config.sys to a unique safe filename, "config.nnn". This example uses the stream function to query for file existence.

----------------- savecfg.cmd --------------------
/* Save config.sys somewhere safe */

do i = 1 to 999
   filename = "c:\config." || right( i, 3, '0' )
   if stream( filename, "c", "query exists" ) = "" then
   do
       copy "c:\config.sys" filename
       leave
   end
   if i = 999 then
   do
       say 'ERROR: all 999 possible filenames are in use'
       exit 1
   end
end

Note we are using the Rexx script as a genuine batch language - when we find a filename which does not already exist we can just use the regular OS/2 'copy' command.

This is one of the places where Rexx's use as a command interpreter language is obvious. What Rexx does is to pass ANY command it does not understand on to the command processor, having first substituted any variables. On completion the return code is available in the 'rc' variable (we saw this variable earlier - in the syntax error handler) and can be checked to ensure the command was successful.

Problems with Rexx

One bug in the above example is that Rexx will substitute ALL variables before running the command - in the example the word 'copy' is a Rexx variable - it just isn't defined yet and so will be given its default value of "COPY". However, if I had previously assigned the value 'del' to the variable then Rexx would invoke a rather different command string 'del c:\config.sys c:\config.002'.

For this reason it is a very good idea to quote command names, and replace copy in the above example with the quoted string 'copy' or "COPY" to prevent any unexpected behaviour!

This is one instance of a more general problem with languages such as Rexx which do not require variables to be declared. It is all too easy to accidently reuse a variable and nothing will warn you that this has occurred. In addition, if your typing is anything like mine you will occasionally mis-spell variables, and Rexx will automatically create a brand new variable with a default value of the mis-spelled word!

The problems caused by these errors can take a while to locate, so it is worth looking at ways of reducing their incidence.

The first thing to do is to try be consistent in the names that you use for variables. If you use fldlen, fieldlen and field_length depending on how the mood takes you the chances are you will run into problems!

The second, and probably more helpful, thing that you can to to help is to write smaller pieces of code - Rexx supports the concept of subroutines, which can also can have their own local data, and so you can structure a large Rexx script as a number of smaller routines, each with their own local variables.

This makes it easier to check you haven't accidently reused a variable, and to verify your spelling is consistent within the routine.

A further problem with Rexx is that of performance - it is interpreted and not all that fast. The simplest approach is to use Rexx for programs where performance is not the main issue and to make use of the rapid development and flexibility of the language. Where the task being attempted is too slow in Rexx you can migrate the program to another language, using the Rexx original as a framework. (It is also possible to purchase compilers for Rexx, but this is not a standard part of the OS/2 Rexx offering.)

Alternatively, as a rather more complex solution, you can convert just a part of the Rexx program to another language, such as C/C++, and package it as a DLL which can be called by Rexx. This compromise can leave you with the best of both worlds - the power and flexibility of Rexx for the overall program control and the speed of C/C++ for the actual calculation or algorithm.

The IBM Warp toolkit comes with some examples of how to do this sort of thing, and it may be the subject of future article.

Stem variables

An introduction to Rexx would be incomplete without covering "stem" variables - they are to Rexx what arrays are to C, but more so!

A stem variable is simply a collection of variables with a common root name, differentiated by an index - which may be numeric like in a C array or may be any string for a much more flexible sort of indexing. The name and index are joined by a dot as, for example, "fred.idx".

All items in a stem variable can be initialised by assigning a value to the stem name followed by a single '.'

A simple, but useful, convention for stem variables when the index is numeric is to save the number of items in the 0th variable.

For example, the following fragment reads a complete file into memory and then prints the last 10 lines:

----------------- last10.cmd --------------------
/* List last 10 lines from a file */ 

arg filename 

lines. = 
i = 0
do while lines( filename ) \= 0
   i = i + 1
   lines.i = linein( filename )
end
lines.0 = i

start = 1
if lines.0 > 9 then
   start = lines.0 - 9

do i = start to lines.0
   say i || ':' lines.i
end

An example of this in action:

C:>last10.cmd last10.cmd
10: end
11: lines.0 = i
12:
13: start = 1
14: if lines.0 > 9 then
15:     start = lines.0 - 9
16:
17: do i = start to lines.0
18:     say i':' lines.i
19: end

C:>

We have also made use of another feature of Rexx here - we are reading arguments from the command line with the 'arg' command.

These stem variables are very common, and very powerful. They are particularly common when calling external functions which return a list of items, such as the functions supplied in the RexxUtil DLL.

Anything can be used as an index - once again you must be careful to ensure that you get what you expect... for example:

fred.left = "left"
fred.right = "right"

say fred.left
left = "RIGHT"
say fred.left

Initially the index 'left' is an undefined variable - and hence has the value 'LEFT'. By assigning it the value 'RIGHT' we make the last line display 'right' when we might have expected it to display 'left'. As I said under "Problems with Rexx", this sort of error can take a while to track down.

Debugging

Rexx is a programming language, and so if you're like programmers the world over your Rexx programs won't always work first time.

There are a couple of tools you can use to help.

The first is REXXTRY which is a text-mode program (itself written in Rexx!) which lets you try out single Rexx statements. This is very useful to allow you to experiment with Rexx functions and syntax. In addition you can look at the source code for the RexxTry.cmd program itself (in C:\OS2) for some useful ideas.

The second tool is the 'PMREXX' program, which runs a Rexx script while allowing you to trace its execution into a PM window. You run PMREXX by giving it the Rexx program as its argument. You may want to precede the program with '/T' to run it with interactive tracing.

(Alternatively, once PMREXX has started you can set the 'trace' option and restart the program - both from the 'options' menu)

When being traced the program will pause and wait for 'ENTER' to be pressed after each instruction is executed. You can also examine or change the contents of variables by using Rexx commands like 'say'.

See the Rexx information and PMREXX's own help pages for more information about PMREXX.

Finally you can also put calls to 'trace' in your own Rexx scripts. Try it! Simply add the statement 'TRACE RESULTS' after the first line of a Rexx script and see what it does. This is usually enough of a help - but see the full range of trace options available in the online Rexx help.

Conclusion

I hope that a simple article like this one, with some real examples of very short Rexx programs, will encourage people who use OS/2 to make more use of Rexx.

Look at the on-line documentation, and experiment!

Rexx significantly adds to the ease of use of OS/2, allowing you to write useful command scripts quickly and easily.

Visit also: Extending OS/2 batch files - Part 2