Solving REXX and OS/2 problems with REXXLIB

From EDM2
Jump to: navigation, search

By Charles Daney

On this page you will find over 30 different topics discussing various problems that may be encountered by REXX programmers attempting to exploit the many features of REXX under OS/2 - and how functions in REXXLIB can be used to solve the problems.

OS/2 file handle problems

All OS/2 processes have a limit on the number of "file handles", which represent files that can be open simultaneously. In OS/2 Warp this number is 20, but in previous versions of OS/2 the number has been higher under some circumstances. (And in Quercus Systems' Personal REXX for OS/2 the number is 50.) A few of these handles are usually allocated to system files like "standard input" and "standard output".

Sometimes a REXX program legitimately needs to open more than the current limit. Also, if REXX programs fail to close files when they are no longer needed, all available handles can be used up. In either case, a variety of strange symptoms can result and the program will fail to operate as expected.

Some of the symptoms that can occur:

  • A file is not created or written to when LINEOUT or CHAROUT functions are used.
  • LINEIN and CHARIN functions fail to return data, and the LINES and CHARS functions return 0.
  • External library functions, such as those in REXXLIB which access files, fail to operate correctly.

If symptoms like these, or other problems involving I/O directly or indirectly, should occur, first verify that your program has closed all files it no longer needs to keep open. This can be done with the STREAM, LINEOUT, or CHAROUT functions (although there have been reports that the latter two do not always work in IBM's OS/2 REXX).

If you have determined that all unnecessary files have been closed and that you need to have many files open simultaneously, REXXLIB's DosFileHandles function can help. If it is called with no arguments, it returns the current limit on the number of handles. It can also be called with a numeric value to raise or lower the limit:

current = DosFileHandles(40)
say "Maximum of 40 file handles now available."
say "Limit was formerly" current

Sorting REXX arrays

It is possible to sort REXX arrays (i. e. compound variables with positive integral indices) with native REXX facilties, but it is not easy. First, you have to know exactly how to code a sorting algorithm - and algorithms that are significantly faster than the simple "bubble sort" are usually very tricky. Second, it is very inconvenient to pass arrays to subroutines in REXX. Third, you must copy the appropriate code into every program that needs to sort - because it isn't possible to pass an array to an external procedure at all (unless you use a file or external data queue).

REXXLIB's ArraySort routine takes care of all these problems, and provides many sorting options. If you think of each element of the array as a record, then ArraySort can handle multiple fields in each record. On each field it can sort in ascending or descending order where the order is either strictly alphanumeric, case-insensitive alphanumeric, or based on REXX arithmetic comparisons.

For instance, the following code sorts data first on year and then on month:

data.1 = '1995 March     California'
data.2 = '1994 October   Connecticut'
data.3 = '1993 January   Massachusetts'
data.0 = 3  /* number of individual records */
call arraysort 'data.', 1, data.0, 1, 4, 'a', 'n', 6, 9, 'a', 'c'

Mathematical functions (square root, logarithms, trig)

REXX has an exponentiation operator (**), but it allows only integral exponents. This means there is no native REXX way to take a square root (exponent 1/2), let alone any other kind of root or more general power. Other standard mathematical functions like logarithms and trigonometric functions are also missing from REXX.

REXXLIB fills in this gap by providing:

  • Trigonometric functions: Sin, Cos, Tan, Asin, Acos, Atan
  • Hyperbolic functions: Sinh, Cosh, Tanh
  • Logarithms, exponentials: Log, Log10, Exp, Pow
  • Miscellaneous: Erf, Erfc, Gamma

Date arithmetic and format conversion

People tend to write dates in a wide variety of formats, as is evidenced by the fact that REXX's Date function can return about 10 different formats. Some format conversions are simple, but others require very complex algorithms, even for finding the day of the week for a given calendar date. However, the REXX language provides no built-in method for converting formats.

Another common need is for a way to compute the calendar date 120 days from today (for example). This too is complicated to handle, when year boundaries and leap years must be dealt with. REXX does have one date format, called the "base date" - the number of days since January 1, 1 - that makes such computations easy - provided you can convert to and from this format. REXXLIB provides the DateConv function to handle this problem. Here's how to display the date 120 days from today, for example:

today = date('b')   /* today's date in basedate format */
say '120 days from today is' dateconv(today+120, 'b', 'n')

Finding active programs and tasks

Suppose you want to be able to determine whether a given program is already running. This might be in order to avoid having two copies running at the same time, to start the program if it is not running, or to stop it if it is.

REXXLIB provides two functions to address this need. One, DosPidList produces a list of active processes in the system ("PID" stands for Process ID). You can get a list of all process IDs in the system, along with the name of the executable file associated with the process, the ID of the parent process, and the ID of the "session" in which it is running. Normally, you would use the name of the executable file to identify the process you are looking for. The following code is an example:

progname = 'C:\UTILITY\EDITOR.EXE'
call dospidlist 'pids', 'names'
do i = 1 to pids.0
    if upper(names.i) = progname then do
        say progname 'is running with PID' pids.i

(Upper is a REXXLIB function to translate a string to upper case.) The search process could actually be simplified by using another REXXLIB function, ArraySearch:

progname = 'C:\UTILITY\EDITOR.EXE'
call dospidlist 'pids', 'names'
if arraysearch('names', 'list', progname, 'x') \= 0 then do
    index = list.1
    say progname 'is running with PID' pids.index

ArraySearch returns the number of matching array elements, and puts the indices of each match into the array called list. The option 'x' in the function call specifies that the search should be for an exact match, except for case.

Sometimes you may find it easier to search by the session title instead of the program name. (The session title is usually the contents of the main window title bar.) REXXLIB provides another function, DosSwitchList which can produce a list of all session titles. This can be used to search for sessions containing DOS or Windows programs or OS/2 command files, which DosPidList misses. You can use this function to find out the title of the current session:

call dosswitchlist 'titles', 'pids'
call arraysearch 'pids', 'list', dospid(), 'x'
index = list.1
say 'Current session title is:' title.index

DosPid is a REXXLIB function that returns the process ID of the current process.

Finding the boot drive

If you have several versions of OS/2 on your hard disk because you are using the Boot Manager, you have to find out which of them is the current boot drive in order to find important files like CONFIG.SYS, STARTUP.CMD, or AUTOEXEC.BAT, or system directories such as \OS2. This is very easy with the DosBootDrive function:

say "OS/2 boot drive is" dosbootdrive"."

Copying files

Unless a REXX program is run from the command line, it is necessary to start a new command shell (usually CMD.EXE) when an "internal" command like COPY is invoked from a REXX program. This is the case when using a program made with VX-REXX or VisPro/REXX, when the program is run from PMREXX, or for any REXX application script. Starting a new copy of the shell has significant overhead. Also, the return code of the COPY command is lost when it is run from a new shell. (Similar problems attend the use of commands like DIR, CD, ERASE, RENAME, and ATTRIB, which is one reason both REXXLIB and IBM's REXXUTIL provide alternative functions.)

There is an additional problem with the standard COPY command, in that by default it replaces the target file if it already exists, with no warning. It is easy enough to copy a file in REXX just by reading it all with CHARIN and writing it back out with CHAROUT, but this has the problem that it does not preserve extended attributes.

REXXLIB provides a way around all of these problems with the DosCopy function. It's also easy to append one file to another:

call doscopy 'newdata', 'oldfile', 'a'

Testing removable media

All programs that access files whose names are supplied by online users or from a configuration file - in other words most programs - may be asked to read or write a file on a removable medium like a floppy disk or CD-ROM. Although OS/2 will pop up a dialog box if such a file is accessed without a disk in the drive, this leaves the application little or no control over the situation, and is clearly undesirable for unattended operation.

Two REXXLIB functions make it possible to test whether a given drive actually contains a disk in it (without risking a popup):

  • DosVolInfo - get volume information such as serial number
  • DosDisk - get disk drive allocation information

For example:

if dosdisk('a:', 'b') = -1 then
    say 'Insert disk in drive A:'

See #Testing whether a string is a valid file name for more information about validity checking of file names.

Finding all extended attributes of a file

IBM's REXXUTIL package provides the SysGetEA function to read the extended attributes of a file, but you must know the name of the EA in advance - there is no means for determining what EAs are associated with the file. REXXLIB's DosEAList function allows you to obtain the complete list of a file's extended attributes. You may optionally also get the corresponding values of each EA, as well as an associated flag byte.

REXXLIB has another function, DosEASize, which gives the total size of all EA data for a file. This is handy for determining whether a file has any extended attributes.

Here is a complete program that lists all extended attributes of a file:

/* test ealist function */

types. = 
types.FFFE      = "length-preceeded binary"
types.FFFD      = "length-preceeded ASCII"
types.FFFB      = "length-preceeded bitmap"
types.FFFA      = "length-preceeded metafile"
types.FFF9      = "length-preceeded icon"
types.FFEE      = "length-preceeded ASCII"
types.FFDF      = "multi-valued, multi-typed field"
types.FFDE      = "multi-valued, single-typed field"
types.FFDD      = "ASN.1 field"

parse arg file
file = strip(file)
i = dosealist(file, "name.", "value.", "flags.")
if i > 0 then do
    say i "extended attributes of" file':'
    do j = 1 to i
        say name.j
        parse var value.j type.j 3 length.j 5 value1.j
        type = c2x(reverse(type.j))
        length = c2d(reverse(length.j))
        say '  Type:' type 'Length:' length 'Flags:' c2x(flags.j)
            when type = 'FFFD' | type = 'FFEE' then  /* length-preceded ascii */
                say '  Value:' left(value1.j, 40)
            when type = 'FFFE' | type = 'FFF9' | type = 'FFFA' | ,
                type = 'FFFB' then do   /* other length-preceded data */
                l = min(20, length(value1.j))
                say '  Type format:' types.type
                say '  Value:' c2x(left(value1.j, l))
            when type = 'FFDD' | type = 'FFDE' | type = 'FFDF'
                then do                 /* other recognized type */
                l = min(20, length(value.j)-2)
                say '  Type format:' types.type
                say '  Value:' c2x(substr(value.j, 3, l))
            otherwise do
                l = min(20, length(value.j))
                say '  Unrecognized type'
                say '  Value:' c2x(left(value.j, l))
    say "DosEAList failed, rc =" i "on '"file"'"

Listing environment variables

You can obtain the value of a single environment variable, if you know it's name, with the REXX VALUE function (or the REXXLIB DosEnv function). However, if the name is not fully known ahead of time, it is convenient to be able to get a list of all environment variables. For example, a variable number of file names might be saved in environment variables whose names begin with the string "MY-APP-FILE". (This is a simpler alternative to storing all the names in one long variable, separated by ";".)

REXXLIB's DosEnvList function could be used to find all such items:

call dosenvlist 'list'
call arraysearch 'list', 'results', 'my-app-file', 'f'
do i = 1 to results.0
    item = results.i
    say "Application file:" list.item

Changing file dates

There are times when one does not want to change a file's date even though the file itself has been updated, or when one wants to give a file a new date without changing it. For instance, the OS/2 COPY command itself behaves this way (assigning the date of the original file to the copy). Or perhaps it is desired to assign a single date to a group of files being distributed together.

This is easy to do with REXXLIB's DosFDate function. For example, here is how to give the date/time of one file to another (which may have been derived from the first):

parse value dosdir(file1, 'dt') with date time
date = dateconv(date, 'u', 's')
time = space(time, 0, ':')
call dosfdate file2, date, time

DosDir is a REXXLIB function that can be used to get the date and time a file was last updated (as well as other directory information). Unlike SysFileTree in IBM's REXXUTIL package, it provides the file's time stamp with resolution in seconds (instead of minutes). The DateConv call (see #Date arithmetic and format conversion) is used to put the date into the proper format for DosFDate.

Changing file sizes

There are probably three main situations in which one would want to change a file's size. The first is when the file is going to be completely replaced from the beginning. You could first either delete it entirely with the DosDel function, or just set it to 0 length with the DosCreat function. If you are going to be rewriting an existing file, you generally need to do this, since otherwise the standard CHAROUT and LINEOUT functions append data to the end of a file.

The second situation is when you want to pre-allocate space for a file without actually filling it with data. This might be done to try to avoid disk fragmentation, or simply to help ensure that there is enough disk space available to write the file at a later time. You can use the DosFSize function to do this:

call doscreat newfile               /* create the file */
call dosfsize newfile, 1024*1024    /* allocate a megabyte */

The third situation is when you want to truncate an existing file. For instance, suppose you want to remove a chunk of data from somewhere in the middle of the file. If it is a large file and the part you want to remove is near the end, it would be inefficient (and maybe difficult if disk space is limited) to copy the entire file. Instead you could first save the end of the file data in a REXX variable, truncate the existing file, and finally write the end back to the file:

/* "pos1" is the position of the beginning of the part to be removed,
   "pos2" is the position of the end of the part. */
filesize = dosfsize(filename)
temp = charin(filename, pos2, filesize - pos2)
call dosfsize filename, pos1 - 1
call charout filename, temp
call charout filename

Note that DosFSize merely returns the current size of the file when no other argument besides the name is passed.

Finding the date a file was created or last accessed

Unlike the old "FAT" file system, IBM's enhanced HPFS file system records the date and time a file was created and the date and time it was last read, in addition to the date and time it was last updated. However, none of the usual OS/2 commands, like DIR or IBM's REXXUTIL SysFileTree function, make it possible to access this information. But REXXLIB's DosFileInfo function does:

say filename 'was created on' dosfileinfo(filename, 'c')','
say 'last modified on' dosfileinfo(filename, 'w')', and'
say 'last accessed on' dosfileinfo(filename, 'a')'.'

Testing whether a directory exists

Carefully programmed applications that work with files and directories should test whether file or directory names that are supplied from an online user, external configuration file, etc. are suitable. In particular, if a name should refer to a file, the program should check it is not a directory, and vice versa.

It is possible to make this test with IBM's SysFileTree function by searching for a given name and restricting the search to directories. But one pitfall of this occurs if the supplied name contains wildcard characters, since then the supplied name isn't really a valid directory name at all, even though one or more existing directories might match. It's easy enough to separately exclude names with wildcards, but it's even easier to use REXXLIB's DosIsDir function:

if \dosisdir(name) then
    say '"'name'" is not a valid directory name.'

Robust applications generally should make other tests of purported file and directory names, such as whether the name is a valid name for the file system, and perhaps whether the name actually refers to a pipe or device instead of a file. See #Testing whether a string is a valid file name for more information.

Using named pipes

OS/2 named pipes offer a convenient tool for interprocess communication, since they can be read and written sequentially like files. This makes it possible to communicate, even across a network, with programs that don't otherwise have the ability to communicate with other programs - such as command line oriented DOS programs.

Named pipes can also be used as a general interprocess communication technique for building client-server applications that implement either client or server (or both) in REXX. Such applications can operate across a network, although if they need only be run on a single computer, REXX offers somewhat simpler IPC techniques, such as external data queues.

REXXLIB offers a number of functions for using named pipes:

  • NmPipe_Call - use named pipe without explicit open
  • NmPipe_Close - close a named pipe
  • NmPipe_Connect - establish a named pipe connection from server to client
  • NmPipe_Create - create a named pipe
  • NmPipe_Disconnect - disconnect named pipe from server to client
  • NmPIPE_Open - open a named pipe on client side
  • NmPipe_Read - read a named pipe
  • NmPipe_Transact - complete write/read named pipe transaction
  • NmPipe_Write - write a named pipe

Guaranteeing exclusive access with semaphores

Any time you have a "resource" such as a file that can safely be used by only one process at a time, it is wise to take some steps to prevent concurrent usage. A sequential file such as a log file that might be updated periodically by several processes is a good example. (If a second program tries to write to the file while it is in use by another program, the second program's attempt to write will probably fail, instead of simply waiting until the file is available.)

Database files that are managed by a single server tend to be well protected, but if you were to implement a database-like application where file updating is done from different REXX programs, you have another good example. In this case, you would probably want to make potential file readers, as well as writers, wait until a group of related updates are complete. (And not simply fail because the file is "in use".)

OS/2 provides "mutual exclusion semaphores" to allow for controlling access to any kind of resource, and REXXLIB provides a number of functions for using such semaphores:

  • MutexSem_Close - give up all access to a semaphore
  • MutexSem_Create - create a new semaphore
  • MutexSem_Query - determine current state of semaphore
  • MutexSem_Release - relinquish exclusive control of semaphore
  • MutexSem_Request - request exclusive control of semaphore

Using semaphores for intertask communication

It is not hard to implement "client-server" applications in REXX, and OS/2 provides a number of "interprocess communication" tools to help. Named pipes (see Using named pipes), and REXX "external data queues" are two examples. These are fairly high-level tools, but sometimes lower level facilities are warranted.

A "server" is simply a process that is capable of performing one or more sorts of work upon request. This might involve updating a file, using an I/O device, communicating with a remote computer, etc. "Clients" are other programs that ask the server to do some specific task. At any time, a server is either performing a task or waiting for a new request. An "event semaphore" is simply an operating system abstraction that allows a client to notify a server that some new request ("event") is pending.

All that the event semaphore does is indicate that a new request has been made. It is up to the client to pass somehow to the server more information about exactly what event has occurred or what work needs to be done. This information could be in the form of files or messages placed on an external data queue.

REXXLIB provides a number of functions for using event semaphores:

  • EventSem_Close - give up all access to a semaphore
  • EventSem_Create - create a new semaphore
  • EventSem_Post - signal the occurrence of an event
  • EventSem_Query - determine current state of semaphore
  • EventSem_Reset - cancel indication of event occurrence
  • EventSem_Wait - wait for occurrence of an event

Event semaphores need not be used only between separate processes. They are equally valuable for communication between different threads of the same process. You can find some discussion and an example of this in #Multithreading in REXX programs.

Collecting REXX code into libraries

Other programming language usually provide some means of creating "libraries" of routines that can be written once and easily reused by many subsequent probrams. OS/2 REXX has the concept of a "macro space", which is really just an obscure name for a code library.

REXX macro spaces consist of the "tokenized" forms of REXX programs which have been saved in a single file. (The "tokenized" form of a REXX program is an internal representation of the program which is designed to make subsequent execution of the code much more efficient, by avoiding translation steps which need to be done only once.)

There are several advantages of using a library to store program code:

  • Commonly-used routines do not have to be stored, inefficiently, as separate files on disk.
  • Routines in a macro space library are kept in memory, so they can be loaded much faster than if they had to be fetched from disk.
  • It is much easier to distribute a single library than multiple separate disk files, which can get lost or our of synch with each other.
  • The tokenized code in a library is not in source form, so it cannot be modified or misappropriated by a user. (I. e., this is a good way of "hiding" the source code.)

Unfortunately, not all of the support necessary to use a macro space has been provided with OS/2 REXX. REXXLIB supplies the missing functions:

  • MacroAdd - add a program to the macro space
  • MacroClear - remove everything from the macro space
  • MacroDrop - remove one program from the macro space
  • MacroLoad - load a macro space library into memory
  • MacroQuery - test for presence of a routine in macro space
  • MacroReorder - change order of routines in macro space
  • MacroSave - save all or part of macro space to disk

The mechanics of using macro space libraries are relatively simple, but require more space than available here. See the paper on Advanced REXX Programming for more information.

Multithreading in REXX programs

Although OS/2 is a multitasking operating system, there is no way in the standard REXX language to take advantage of this by using multiple threads. "Multithreading" means running several activities concurrently within the same program. It is a valuable technique for reducing the total time required to accomplish some overall task by doing several things at the same time. The reason it is possible to save time is that certain operations, especially reading from disk files, actually take very little CPU time, so they can be "overlapped" with computations.

For example, during the initialization of a program you might need to read several parameter and data files, and also compute or otherwise set up various data tables. The reading of each data file could well be moved into a separate thread.

REXXLIB provides the RexxThread function to start a new REXX program on a separate thread. The program can be supplied in one of four different forms:

  • source code in a file
  • source code in a REXX data string
  • tokenized program code in a REXX data string
  • tokenized program code in the REXX macro space

(There is are REXXLIB functions, TokenizeFile and TokenizeString for putting tokenized code into a REXX data string for faster execution.)

A REXX program started on a separate thread cannot share variables with the program which initiated it. Any return codes or data must be passed using interprocess communication or files. (See #Using semaphores for intertask communication and #Passing data to and from external routines for more information.)

Here is a simple example of how this might be used:

pid = dospid()
semname = '\sem32\initsem'pid       /* get unique semaphore name */
call eventsem_create semname        /* create the semaphore */
call rexxthread 'f', 'readini.cmd'
/* perform some other initialization while reading initialization data */
call eventsem_wait semname          /* wait for thread to complete */
call eventsem_close semname         /* destroy semaphore */
call varread 'inidata'              /* read processed init data */

In this example, DosPid was used to get the current process ID so that a unique semaphore name could be created. The routine that reads a data file is started on a separate thread with RexxThread. When the thread is done it will write processed information to a data file with VarWrite and post the semaphore before terminating. The main routine then reads the data with VarRead.

Controlling NumLock

The default state of the NumLock key is of passionate interest to many OS/2 users. Some prefer that it always be off, while others would rather it always be on. By default, OS/2 turns it off.

REXXLIB provides the ShiftState function so that any REXX program can set the key state whichever way it wants. It can also determine the current state, perhaps in order to restore it later. The function can also handle the state of the CapsLock and ScrollLock keys.

/* turn on the NumLock key */
old_state = shiftstate('n', 1)

Iterating over tails of a compound variable

REXX compound variables are a unique and valuable feature of the language. (See our paper on Advanced REXX Programming for a very thorough discussion.) You can use a compound variable as an "associative array" that can be indexed by arbitrary character strings as well as integral values. But a few important capabilities were left out of the original design of compound variables.

One of these is the ability to iterate over all tails of the compound variable, which would be easy to do if "subscripts" were strictly numeric. For instance, the compound variable catalog_number. might contain the catalog number of a book, using the title as an index. The CVTails function can be used to find all tails of a compound variable. It puts them in an ordinary REXX array which can be processed sequentially.

call cvtails 'catalog_number', 'list'
do i = 1 to list.0
    tail = list.i
    say 'The catalog number of' tail 'is' catalog_number.tail'.'

CVTails can do even more, because it is optionally capable of finding only tails that match a given pattern, including #regular expression patterns. For instance, in the above case, you could list only books containing specific strings in the title.

Passing data to and from external routines

There is no way, strictly within the REXX language, to pass an array, compound variable, or any other collection of data to an external routine. Indeed, it is awkward to pass data to even an internal routine, but it is at least possible by "exposing" the variables. This is not allowed when calling external routines.

One alternative that is often used is to pass the data on an external data queue (PUSH or QUEUE the data in the main routine, PULL it in the subroutine). This can be slow and tedious for an array of many elements, if they have to be handled one at a time. Another alternative is to pass the data via a disk file, but this may be even slower.

REXXLIB provides a couple of functions that make it much easier and more efficient to write all of one or more compound variables to a file and later read them back. CVWrite can write all of one compound variable to a file, which can be read back (perhaps into a variable with a different stem) with CVRead. VarWrite can write a whole list of variables, or even all variables in a program, to a file, which can be read back in whole or part with VarRead.

tempfile = dostempname('temp???')
call cvwrite tempfile, 'table'
call tabproc tempfile
call dosdel tempfile

In this example, table. is a compound variable holding a data table (perhaps of two or more dimensions). DosTempName is a REXXLIB function that generates a temporary file name from a pattern, and DosDel is a REXXLIB function that deletes the temporary file when it is no longer needed. Tabproc is the name of the external routine to be called. It might access the data like this:

parse arg table_file
call cvread table_file, data_table

Exactly the same technique can be used for passing back data structures created in a subroutine.

Determining disk format type (FAT/HPFS/CD-ROM)

It is often useful, in careful programming, to know whether a given file or directory is located on a CD-ROM (which is read-only). Sometimes it also matters whether the volume in question uses the FAT file system or HPFS, perhaps because of the difference in allowable file names. REXXLIB's DosVolInfo function can be used to obtain this information:

say "The file system on" drive_letter "is" dosvolinfo(drive_letter, 'f')

Terminating an active program

Many useful OS/2 utilities and services can be implemented by starting a program to operate in the background with the DETACH commanmd. But usually such programs do not have a convenient way of being shut down when they are no longer neeed. REXXLIB provides the DosKillProcess function to do this.

DosKillProcess requires a process ID as argument. You can use the DosPidList function to obtain the process ID. (See #Finding active programs and tasks for more information.) Here is how to kill all instances of a particular program:

call dospidlist 'pids', 'names'
call arraysearch 'names', 'list', program_name, 'x'
do i = 1 to list.0
    item = list.i   /* index in pids. and names. array */
    call doskillprocess pids.i

Testing session type (PM or text mode)

It is sometimes necessary in a REXX program to do things differently depending on whether the program is running in a PM session, on one hand, or a full screen or VIO (windowed text mode), on the other. For instance, you might use standard input and output (PULL, SAY) to communicate with a user in a text mode session, or some other technique (e. g. the RxMessageBox function) in a PM session. Since the REXX program may be an external subroutine that could be invoked from any type of session, there needs to be a way to determine the type, and the DosSessionType function provides this:

type = dossessiontype()
if type = 3                     /* PM session */
    then call rxmessagebox errormsg, 'Error'
else if type = 0 | type = 2     /* text mode */
    then say 'An error has occurred:' errormsg
else if type = 4                /* detached session */
    then call pmprintf errormsg

(See #Non-intrusive "printf"-style debugging for information about PMPrintf.) Note that RxMessageBox can't be used from a non-PM session, just as SAY can't be used in a PM session (unless output has been redirected).

Regular expression pattern matching

"Regular expressions" are a concise way of expressing a specific pattern to be used in search operations. They provide for the ability to match arbitrary characters, only numbers, only letters, white space, etc. Regular expressions can be much more elaborate than patterns which involve only "wildcard" characters, and they can also provide for matching only at the beginning or end of a string.

Several REXXLIB functions are able to use regular expression in search operations:

  • ArraySearch - search values of a REXX array
  • CVSearch - search values of a REXX compound variable
  • CVTails - search tails of a REXX compound variable
  • FileSearch - search lines of an ASCII file
  • Grep - search a single character string

Regular expressions are not ideal for all searching situations, however. They require that certain special characters (such as ":", "*", "+") be used to specify the pattern instead of standing for themselves. You can use these characters in regular expressions to stand for themselves only by preceding them with an escape character. This is inconvenient to do when the pattern is, for example, requested from a user at run time.

Because of this, all of the searching functions also provide options that offer some flexibility without using regular expressions. For instance, you can specify that alphabetic case is to be considered or ignored, that the pattern must match at the beginning of a line or string, that the pattern must match an entire line or string, or that the operation should succeed when a pattern match does not occur.

As an example of regular expressions, suppose you are looking for Social Security numbers like 123-45-6789. ":d" is used to match a single numeric digit, so ":d:d:d-:d:d-:d:d:d:d" would be the appropriate pattern. Then this code would find all lines in a file that contain Social Security numbers:

ssn = ":d:d:d-:d:d-:d:d:d:d"
count = filesearch(filename, ssn, 'lines')
say count 'lines found containing Social Security numbers.'

Saving program data across invocations

Good application programs allow user customization by setting of options and preferences. This data has to be saved from one invocation of the program to another. In a game program, one usually wants to be able to save the state of the game so it can be resumed later. This is another example of saving data across invocations. Even more commonly, an application deals with one or more collections of data that represent permanent information which the application uses as a database.

Ultimately, permanent data needs to be saved in a disk file of some sort. A REXX program can always save the data in its own custom format, such as a flat ASCII file. If the data is extensive and highly structured it can be saved in a formal database like DB2/2. IBM's REXXUTIL function package provides functions for reading and writing OS/2 .INI files. However, all of these methods require a moderate to large amount of effort for storing and retrieving data. This is particularly noticeable when the data is naturally structured in lists, tables, etc. as is discussed in our paper on Advanced REXX Programming.

REXXLIB provides the VarWrite and VarRead functions, to make it possible to write large REXX data structures - or even all the variables in a REXX program - to a disk file and later read it back. For instance,

tempname = dostempname("game???.sav")
call varwrite tempname

might be all you need to do in order to save all of a REXX program's variables in a temporary file (whose name is generated by the DosTempName function). When this data is later read back with

call varread tempname

the program can continue execution in exactly the state it left off.

Dumping contents of REXX variables and arrays for debugging

Many problems in debugging REXX programs boil down to being able to see exactly what values have been assigned to REXX variables - to "dump" storage, in effect. This is especially so in REXX, since with compound variables and the INTERPRET instruction, even the names used for variables may not be easy to tell by inspection of the source code. Many problems occur because of problems using compound variables.

REXXLIB provides the VarDump function to handle this. It produces a listing of all or selected variables with their full names and values. This listing can either be displayed on the screen or saved in a file. For instance, here's how to dump selected arrays and tables:

call vardump dumpfile, 'i', 'array1.', 'array2.', 'table1.'

The argument 'i' means that only the specified variable stems are to be dumped. One could use 'e' instead to request all variables except those specified be dumped.

Non-intrusive "printf"-style debugging

"Printf" (for the benefit of non-C-programmers) is roughly the C language equivalent of SAY. It is used everywhere in C programming, and often as a debugging aid, since it makes it easy to display the values of variables at key points or simply to verify that some particular part of a program's code has been reached. The same can be done with SAY in REXX, but the problem is that debugging output can destroy careful screen formatting (for a program running in a text mode window). For PM REXX programs, or programs started in the background with the DETACH command, the output from SAY is normally discarded entirely, unless the program uses "console window" features of REXX GUI tools like VisPro/REXX and VX-REXX.

Mike Cowlishaw, the originator of REXX itself, also created a useful tool for debugging under OS/2 called PMPRINTF. It consists of a subroutine ("pmprintf") that can be called from a C program and a simple PM application for displaying the output in a scrollable window. REXXLIB includes a PMPrintf function to allow the same thing in REXX programs. It is especially handy to use in REXX because of the ease of modifying a program, to insert PMPrintf calls, and quickly rerunning it.

To use PMPrintf it is necessary to obtain Cowlishaw's PMPRINTF, which is an IBM "EWS" package. You can download it from our REXX Freeware and Shareware page.

Testing whether a string is a valid file name

A robust program should carefully test file names supplied by onlines users, configuration files, etc., especially when the supposed name represents a file that is to be created. The file system will not create a file with an invalid name, but the reason for the failure may be difficult to distinguish from other problems, such as the disk being full or the name being already in use for another file or directory. Testing file name validity is surprisingly tricky, particularly since it depends on whether the FAT or HPFS file system is in use.

REXXLIB provides the ValidName function to handle this testing. It even has an option to control whether wildcard characters should be accepted in a particular case.

if \validname(name) then
    say '"'name'" is not a valid file name.'

Even if a given name is valid, it is often desirable to make other tests, and REXXLIB provides functions to test for:

  • existing files (DosIsFile)
  • I/O devices (DosIsDev)
  • named pipes (DosIsPipe)
  • file system directories (DosIsDir)

See Testing whether a directory exists for more information on testing for directories. You should also consider Testing removable media.