REXX Tips & Tricks:General hints for REXX

From EDM2
Jump to: navigation, search

This section contains general hints for REXX under OS/2.

Another good source for REXX related information is the Web pages of Quercus Systems - in particular, the pages How do I ... and Common REXX Pitfalls.

You should also take a look at the REXX FAQ maintained by Dave Martin (see Internet - Web Pages).

Contents

REXX and Y2K

The latest FixPaks for OS/2 Warp 3 (FP35 and above) and Warp 4 (FP6 and above) contain extensions for REXX to make it "Y2K Tolerant".

From a recent FixPak's Readme:

4.1 QUERYING FILE DATES FOR FILES AFTER DEC 31, 1999 IN REXX

Existing REXX functions return file dates with a two digit year only. While these functions are Year 2000 tolerant (i.e. the results will be correct for files dated after Dec 31, 1999) they require some additional logic in existing programs to handle the returned date correctly when they are compared with other file dates.

Since the output format of the existing functions could not be changed for compatibility reasons, new options have been added to the REXX interpreter to return file dates with the year formatted with 4 digits. Two functions have been extended to support the new format. The syntax to retrieve the file date in 4 digit format is as follows:

/********************************************/
/* Use STREAM QUERY TIMESTAMP to query file */
/* date in 4 digit format                   */
/********************************************/

Say Stream("C:\CONFIG.SYS", "C", "QUERY TIMESTAMP")

/***********************************************/
/* Use option "L" with SysFileTree to return a */
/* list of files with long date format         */
/***********************************************/

Call RxFuncAdd "SysLoadFuncs", "RexxUtil", "SysLoadFuncs"
Call SysLoadFuncs
Call SysFileTree "C:\*.*", "Files", "L"
Do i = 1 To Files.0
   Say Files.i
End /* do */

Reserved Words

"REXX has no reserved words. Words are only reserved in context. Hence:

if=1

if if then
   then=2;
else
   else=7

do forever=3 to 7 until end;
   end=1;
end

parse var x with var with left right value

are all valid instructions. A function is recognised solely by the presence of a '(' appearing immediately after it, not by the spelling of its name.

This is where Rexx has a big advantage over things like Visual BASIC. (I was typing a BASIC program the other day, and the first three variable names I tried were all keywords...)"

Source: Ian Collier

Using comments

Although it's possible to use multi-line comments in REXX programs, it's not advisable. Multi-line comments can make it very difficult to search an error in REXX programs.

Note: Because nested comments are allowed in REXX, you should use the following technique to use the strings to mark the beginning and the end of a comment in a string:

"/" || "*"

and

"*" || "/" 

Writing OS independent programs

To write OS independent REXX programs, you can use the PARSE SOURCE statement to distinguish between the different platforms (see example below and the chapter about PARSE SOURCE. The chapter titled Force a REXX program to run in a special way also discusses the use of PARSE SOURCE to identify the environment in which a REXX program is running and then process conditional commands.

When writing programs for use on other platforms in addition to OS/2, remember that some of the features and functions in OS/2 REXX are not implemented in REXX on other platforms (or may be implemented in a different manner)!

(see REXXTRY.CMD for another example for OS independent REXX programs)

/* ------------------------------------------------------------------ */
/*                                                                    */
/* APPLICATION - Cross Platform - CMS, OS/2 2.0 and TSO               */
/* FUNCTION    - Merge 4 comma-delimited input files into an          */
/*               outputfile, tagging each record with the name of     */
/*               it's corresponding input file                        */
/* USE:   OS/2 - MERGE f1.ext,f2.ext,f3.ext,f4.ext,outfile.ext        */
/*        TSO  - MERGE f1.qlf,f2.qlf,f3.qlf,f4.qlf,outfile.qlf        */
/*        CMS  - MERGE fn1 ft1 fm1,fn2 ft2 fm2,...fm4,ofn oft ofm     */
/* AUTHOR      - Michel Plungjan April '93                            */
/*               (see EMail Addresses)                                */
/*                                                                    */
/* History:                                                           */
/*  12.12.1995 /bs                                                    */
/*    - reformatted and added some comments                           */
/*  26.02.1996 /bs                                                    */
/*    - corrected a bug in the TSO read section according to          */
/*      information from Michel Plungjan                              */
/*                                                                    */
/* ------------------------------------------------------------------ */

  arg InFile.1 "," InFile.2 "," InFile.3 "," InFile.4 "," Merged

  if InFile.1 = "/?" then
    signal HELP;

  call INIT

  j = 0;
  do i = 1 to 4
    FileName = Infile.i
    call READFILE;
    if InRec.0 > 0 then
    do k = 1 to InRec.0
      j = j + 1
      OutRec.j = strip(InRec.k,"T") substr(FileName,1,4)
    end; /* do k = 1 to InRec.0 */
  end; /* do i = 1 to 4 */

  if j > 0 then
  do
    OutRec.0 = j;
    call WRITEFILE;
  end /* if j > 0 then */
  else
  do
    XReason = "Input files empty...";
    XRc = 8;
    SIGNAL ABNORMAL_END
  end; /* else */

SIGNAL NORMAL_END;

/* ------------------------------------------------------------------ */
/*                                                                    */
READFILE:
  select

    when sys = 'CMS' then
    do

/* --------------------- code for CMS ------------------------------- */

      'EXECIO * DISKR' FileName '(FINIS STEM INREC.'
       hrc = rc
       if hrc <> 0 then
       do
         XRc = hrc
         XReason = 'Error when reading' FileName 'dataset'
         SIGNAL ABNORMAL_END
       end /* if hrc <> 0 then */
    end /* CMS */

    when sys = 'TSO' then
    do

/* --------------------- code for TSO ------------------------------- */

      'ALLOC DA('FileName') FI(INDD) SHR'
      hrc = rc
      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Allocation error of' FileName 'dataset'
        SIGNAL ABNORMAL_END
      end
      'EXECIO * DISKR INDD (FINIS STEM INREC.'              /* v2.10 */
      hrc = rc
      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Error when reading' FileName 'dataset'
        SIGNAL ABNORMAL_END
      end /* if hrc <> 0 then */
      'FREE FI(INDD)'
      hrc = rc
      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Error when freeing' FileName 'dataset, DDName INDD'
        SIGNAL ABNORMAL_END
      end /* if hrc <> 0 then */
    end /* TSO */

    when sys = 'OS/2' then
    do

/* --------------------- code for OS/2 ------------------------------ */

      do ii = 1 while lines(filename) > 0
        InRec.ii = linein(FileName)
      end; /* do ii = 1 while lines( fileName) > 0 */

      InRec.0 = ii - 1
      if (stream(FileName,'S') <> 'READY') then
      do
        XRc = 1
        XReason = 'Error when reading' InFile ,
                  'file, Error indicator is 'stream(FileName,'D')
        SIGNAL ABNORMAL_END
      end /* I/O Error */
    end /* OS/2 */

    otherwise
    do

/* --------------------- unknown OS --------------------------------- */

      XReason = 'This program does not know how the environment' sys,
                'uses I/O, please contact author'
      XRc = 8
      SIGNAL ABNORMAL_END
    end /* otherwise */

  end /* Select */
return

/* ------------------------------------------------------------------ */
/*                                                                    */
WRITEFILE:

  select

    when sys = 'CMS' then
    do

/* --------------------- code for CMS ------------------------------- */

      'EXECIO 'OutRec.0 'DISKW 'Merged '0 F 80 (FINIS STEM OUTREC.'
      hrc = rc
      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Error when writing' Merged 'dataset'
        SIGNAL ABNORMAL_END
      end /* if hrc <> 0 then */

    end /* CMS */

    when sys = 'TSO' then
    do

/* --------------------- code for TSO ------------------------------- */

      'ALLOC DA('Merged') FI(OUTDD) SHR' /* File must already exist */
      hrc = rc
      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Allocation error of' Merged 'dataset'
        SIGNAL ABNORMAL_END
      end /* if hrc <> 0 then */

      'EXECIO' OutRec.0 'DISKW OUTDD (FINIS STEM OUTREC.'
      hrc = rc

      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Error when writing' Merged 'dataset'
        SIGNAL ABNORMAL_END
      end /* if hrc <> 0 then */

      'FREE FI(OUTDD)'
      hrc = rc
      if hrc <> 0 then
      do
        XRc = hrc
        XReason = 'Error when freeing' Merged 'dataset, DDName OUTDD'
        SIGNAL ABNORMAL_END
      end /* if hrc <> 0 then */

    end /* TSO */

    when sys = 'OS/2' then
    do

/* --------------------- code for OS/2 ------------------------------ */

      do i = 1 to OutRec.0
        Dummy = lineout(Merged,OutRec.i);
      end; /* do i = 1 to OutRec.0 */

      rc = stream(Merged,'c','close')

      /* please put your own OS/2 error checking here */
    end /* OS/2 */

    otherwise
    do

/* --------------------- unknown OS --------------------------------- */

      XReason = 'This program does not know how the environment' sys,
                'uses I/O, please contact author'
      XRc = 8
      SIGNAL ABNORMAL_END
    end /* otherwise */

  end /* Select */
return;

/* ------------------------------------------------------------------ */
/* init global variables and get the current OS (in the var SYS)      */
/*                                                                    */
INIT:
  true  = 1;
  false = 0;
  XReason = 'Files merged, you may now sort the file 'Merged;
  XRc = 0
  parse source sys .
return

/* ------------------------------------------------------------------ */
/* show the usage help                                                */
/*                                                                    */
HELP:
  do i = 1 until pos('* ---',sourceline(i)) > 0
    say strip(sourceline(i))
  end /* do i = 1 ... */
exit;

/* ------------------------------------------------------------------ */
/*                                                                    */
ABNORMAL_END:
  say 'Program stopped due to'

/* ------------------------------------------------------------------ */
/*                                                                    */
NORMAL_END:
  say XReason 'return code:' Xrc
exit

Writing filter programs in REXX

Filter programs are programs that read from standard input, convert the input in some way, and write the converted lines to standard output. Filter programs are heavily used in UNIX systems, but you can use them on any operating system supporting redirection of standard input and standard output with pipes, for example, OS/2.

In OS/2 you can write filter programs very simply in REXX (see the example below). But you should take care to be aware of the following points:

Write all messages (error messages, logos, etc.) to STDERR instead of STDOUT, e.g. use

call LineOut "STDERR", "This is an error message"

(see also Using the handles 3 to 9 in REXX programs)

Always use

call trace "OFF"

as first statement in a filter program (after the required REXX comment delimiters on line 1, of course). This statement makes sure that your program ignores the environment variable RXTRACE.

Also be aware that the function LINES() does not work as expected in Object-Oriented REXX (see The function LINES() in Object REXX). Therefore, you must distinguish between the different REXX versions in your filter program.

see General input line enhancer for a special filter program

/* ------------------------------------------------------------------ */
/* Simple filter program in REXX                                      */
/*                                                                    */
/* A filter program reads lines from STDIN, does something with them, */
/* and writes the changed lines to STDOUT                             */
/*                                                                    */
/* In this example we simply convert the lines to uppercase.          */
/* This program works for Classic REXX and for Object REXX.           */
/*                                                                    */

  call trace "OFF"

  signal on notready name ProgramEnd

                    /* check the REXX interpreter version             */
  parse version rexxVersion .
  if rexxVersion = "OBJREXX" then
  do;
                    /* current REXX version is Object REXX            */

                    /* main loop for Object REXX                      */
                    /* (The loop is aborted by a NOTREADY condition)  */
    do forever
      .output~lineout( convert( .input~linein ) )
    end /* do forever */

  end /* if rexxVersion = "OBJREXX" then */
  else
  do
                    /* current REXX version is Classic REXX           */

                    /* main loop for Classic REXX                     */
    do while lines( "STDIN" ) <> 0
      call lineOut "STDOUT", convert( lineIn( "STDIN" ) )
    end /* do while lines() <> 0 */

  end /* else */

programEnd:

exit 0

/* ------------------------------------------------------------------ */
/* this function returns the parameter in uppercase                   */
/*                                                                    */
Convert: PROCEDURE
  parse arg inputLine
  return translate( inputLine )

Using the handles 3 to 9 in REXX programs

OS/2 defines 10 general file handles for batch files (and therefore also for REXX programs):

Handle Default assignment
0 Standard Input (STDIN)
1 Standard Output (STDOUT)
2 Standard Error (STDERR)
3 no default assignment
4 no default assignment
5 no default assignment
6 no default assignment
7 no default assignment
8 no default assignment
9 no default assignment

To use the handles 0 to 2 in REXX programs, use the filenames STDIN, STDOUT, or STDERR. Note that these are the default values and therefore you do not have to explicitly code them. The handles 0 to 2 always exist and point either to the keyboard (handle 0), the screen (handles 1 and 2), or to a file if redirected via <, >, or |.

It's also possible to use the handles 3 to 9 in REXX programs. To do that, you must call the program with a redirection for the handle and inside the REXX program you can access them only via OS/2 commands, like for example the ECHO command.

Examples:

Example for using ony the handle 3:

/* testHandle3.cmd                                                    */
/*                                                                    */
/* REXX sample on using handle 3 in a REXX program                    */
/* call this cmd with                                                 */
/*                                                                    */
/*   testHandle3.cmd 3>handle3.out                                    */
/*                                                                    */

  testString = "This text goes to the file referenced by handle 3"
  "echo. " testString ">>&3"
exit 0

Example for using the handles 3 to 9:

/* testHandles3to9.cmd                                                */
/*                                                                    */
/* REXX sample on using the handles 3 to 9 in a REXX program          */
/* call this cmd with                                                 */
/*                                                                    */
/*   testHandles3to9.cmd 3>ha3 4>ha4 5>ha5 6>ha6 7>ha7 8>ha8 9>ha9    */
/*                                                                    */

  testString = "This text goes to the file referenced by handle "
  do i=3 to 9
    "echo. " testString || i ">>&" || i
  end /* for */
exit 0

You can also redirect more then one file handle into a file. Example:

testhandles3to9.cmd 3>test3x.out 4>>&3 5>>&3 6>>&3 7>>&3 8>>&3 9>>&3

In this example every echo command writes to the file test3x.out.

testhandles3to9.cmd 3>test3x.out 4>>&3 5>>&3 6>>&3 7>>&3 8>>&3 9>>&3 3>test3y.out

In this example every echo command except the echo command for handle 3 writes to the file test3x.out. The echo command for handle 3 writes to test3y.out

Please be aware that the output goes to STDOUT or you will get an error message if you try to use an unassigned file handle - for example if you call the first example testHandle3.cmd above without the redirection of the handle 3:

testHandle3.cmd

Also be aware that files you open inside your REXX program will get the next free file handle. Therefore the first file you open in a REXX program normally gets the handle 3. But if you redirect the handle 3 into a file via redirection on the command line the first file you open inside your REXX program will get the handle 4. Try the example below to see the difference.

/* testHandle3a.cmd                                                   */
/*                                                                    */
/* REXX sample                                                        */
/*                                                                    */
  call stream 'TESTSTREAM', 'c', 'OPEN WRITE'

  testString = "This text goes to the file referenced by handle 3"
  "echo. " testString ">>&3"

  call stream 'TESTSTREAM', 'c', 'CLOSE'
exit 0

If you call this program with

testHandle3a.cmd 3>handle3.out

the output goes to the file HANDLE3.OUT. if you call this example with

testHandle3a.cmd

the output goes to the file TESTSTREAM.

To check if one or more of the handles 3 to 9 are redirected to a file, you can check which handle you'll get if you create a new file:

/* This code works in Object-Oriented REXX only!                      */
  DATAFILE = 'NUL'
  if stream( DATAFILE, 'c', 'OPEN READ' ) = "READY:" then
  do
    DATAFILE_handle = stream( DATAFILE, 'c', 'QUERY HANDLE' )
    say "DATAFILE handle is" DATAFILE_handle
    call stream DATAFILE, 'c', 'CLOSE'
  end /* if */
  else
    say "Error opening " || DATAFILE || " for reading!"

Normally, if the handles between 3 and 9 are not redirected, the handle for the new file is 3. If one or more of the handles 3 to 9 is redirected to a file the handle you get is 5 (if only handle 3 is redirected), 6 (if handle 3 and 4 are redirected), and so on.

BUT

The handle is also 5 or above if STDIN, STDOUT, or STDERR is redirected to another device, file or pipe. So, to be sure, check this also (How to check if STDIN, STDOUT or STDERR are redirected to a file) a

see Using redirection for a simple process controller, General input line enhancer for other samples. see Output & Input control, Reserved names for files in REXX for additional information.

Calling REXX programs in the CONFIG.SYS

You can call REXX programs in the CONFIG.SYS file by using the CALL statement. To do this, load the REXX support with (for example) BOS2REXX.EXE prior to the line that calls your REXX program in CONFIG.SYS.

Example:

 REM *** load the REXX support
 RUN=E:\OS_2\tools\bootos2\bos2rexx.exe

 REM *** call the REXX program
 CALL=C:\OS2\CMD.EXE /C myprog.cmd

Note that you can only use the non-WPS related functions from REXXUTIL.DLL in programs called in the CONFIG.SYS. Note further, that you must use RxFuncAdd to load each necessary REXXUTIL function by hand - you cannot use SysLoadFuncs! See the next chapter for an example for using this technique.

(see also the important note at the end of the section Using REXX if booted from diskette and RxFuncAdd)

Example for using REXX programs in the CONFIG.SYS

A REXX program called from within the CONFIG.SYS is always called after loading all drivers. So, if you want to use a REXX program to change one or more DEVICE or BASEDEV statements (or a file used by a device driver) in the CONFIG.SYS while booting the workstation, you must use a 2-step-method.

Here's an example: Some time ago a CompuServe forum member said he wanted to change the file PROTOCOL.INI in respect to the input of an user while OS/2 was booting. (PROTOCOL.INI is an ini file used by a device driver necessary for the Network support).

I suggested to call the REXX program below in the CONFIG.SYS for this purpose (see Calling REXX programs in the CONFIG.SYS on how to call a REXX program in the CONFIG.SYS). This program uses a status file and an additional boot process to change the configuration on the fly.

The advantage of this method in contrast with using OS/2's ALT-F1 feature to perform maintenance at bootup:

It runs not only on WARP 3, but also under OS/2 versions prior to WARP; there's no overhead and you only have to maintain one configuration.

(see RxFuncAdd if you want to use other DLLs in a REXX program called in the CONFIG.SYS)

/* REXX program which can be called in the CONFIG.SYS to get some     */
/* input from the user, check the current configuration against the   */
/* user input, and change the configuration and reboot the            */
/* workstation with the changed configuration if necessary.           */
/*                                                                    */
/* Note that you can use this method also to change some lines in     */
/* your CONFIG.SYS.                                                   */
/* Note further, that you can also replace the code to get the user   */
/* input with some code to automatically get the needed configuration.*/
/*                                                                    */
/* You can also use the routine Getkey in programs called in the      */
/* CONFIG.SYS.                                                        */
/*                                                                    */
/* (c) 1996 Bernd Schemmer, Germany, EMail: Bernd.Schemmer@gmx.de     */
/*                                                                    */

                    /* install an error handler for user breaks       */
  SIGNAL ON HALT Name UserBreak

                    /* name of the status file for the current boot   */
                    /* process. If this file exists, this is the      */
                    /* second boot, if not it's the first.            */
  statusFile = "C:\BOOTSEM"

  if stream( statusFile, "c", "QUERY EXISTS" ) = "" then
  do
                    /* first boot                                     */

                    /* load the necessary REXXUTIL function(s). Note  */
                    /* that you CANNOT use SysLoadFuncs!!!            */
    call rxFuncAdd "SysGetKey", "REXXUTIL", "SysGetKey"

                    /* get the user input                             */
    call CharOut, "Which PROTOCOL.INI do you want? Press A or B ->> "
    do forever
      userInput = translate( SysGetKey( "NOECHO" ) )
      if UserInput = "A" | UserInput = "B" then
        leave
      else          /* invalid user response - ring the bell          */
        call CharOut , "07"x
    end /* do forever */

    call LineOut , UserInput


                    /* check the configuration, in this example:      */
                    /* check if the existing PROTOCOL.INI is correct  */
    call LineOut , "Checking the configuration. Please wait ..."

                /* ... insert the code to check the configuration ... */
                /* set ConfigurationOK to 1 if PROTOCOL.INI is okay   */

    if ConfigurationOK = 1 then
    do
                    /* the current configuration is okay              */
                    /* continue with the boot process                 */

      call LineOut, "The current configuration is okay." ,
                    "Boot process continues ..."

    end /* if ConfigurationOK = 1 then */
    else
    do
                    /* the current configuration is NOT okay          */

      call LineOut, "The current configuration is NOT okay." ,
                    "Now changing the configuration ..."


                    /* correct the configuration, in this example:    */
                    /* replace the PROTOCOL.INI                       */

                /* ... insert the code to change the configuration ...*/

                    /* now create the status file                     */
      call LineOut statusFile, "We need a second boot ..."

                    /* close the status file                          */
     call stream statusFile, "c", "CLOSE"

     call LineOut, "Rebooting your workstation please wait ..."

                    /* and reboot the workstation using SETBOOT       */
     "SETBOOT /IBD:C"

    end /* else */
  end  /* if stream( statusFile, "c", "QUERY EXISTS" ) = "" then */
  else
  do
                    /* second boot                                    */

                    /* normally nothing to do because the             */
                    /* configuration should now be okay               */
  end /* else */

                    /* delete the status file also on user breaks     */
UserBreak:

                    /* delete the status file if it exists            */
  if stream( statusFile, "c", "QUERY EXISTS" ) <> "" then
    "@del " statusFile "2>NUL 1>NUL"

Using REXX if booted from diskette

When OS/2 is booted from diskette, you must use a special program to load the REXX support (normally, OS/2 boots from the hard disk and the WPS loads REXX support automatically).

The best program available to load REXX from diskette is BOS2REXX.EXE.

To create a boot diskette with REXX support do the following:

  • Create a boot diskette with, for example BOOTOS2.
  • Copy BOS2REXX.EXE to the boot diskette
  • Add the statement RUN=A:\BOS2REXX.EXE to the file A:\CONFIG.SYS
  • Add the directory with the files REXX*.DLL (\OS2\DLL) to the LIBPATH in the file A:\CONFIG.SYS
  • Add the directory with the file REX.MSG (\OS2\SYSTEM) to the DPATH in the file A:\CONFIG.SYS

Note: You may omit the third step. In this case you must init the REXX support manually with the command

detach A:\BOS2REXX.EXE

This method allows you to load REXX support only if necessary.

  • see also Using REXXUTIL; use LX lite - an compressor for OS/2 executables to create one (!) boot diskette with REXX support and other useful tools!)

Important: Do NOT kill the process with BOS2REXX! Your REXX programs might not act as expected if you do so! The process which initially starts the REXX support is required throughout the session because it handles the global data, semaphores and so on. This is true even if you restart BOS2REXX after killing it!

Source: APAR PJ15496

  • see also Adding Object REXX Support to a maintenance partition and RxFuncAdd)

Force a REXX program to run in a special way

Sometimes it is important for a REXX program to run within a specific environment (e.g. PMREXX) or in a special way (e.g. minimized or in the background).

To ensure that your REXX program uses the environment you intend it to use, you can check the current environment in your REXX program and, if it's not the envionment you need, restart the REXX program in the needed environment:

In the first case mentioned above, you can use the return code of the function ADDRESS to check the current environment (see Run a REXX program under a specific environment and the warning regarding WARP 4 in the section PMREXX!); in the second case (i.e., running your program in a special way, such as minimized), you cannot detect the current environment and thus you've to restart your program using an additional parameter to distinguish between the two passes. (see Force a REXX program to run minimized).

Run a REXX program under a specific environment

Use the return code of the function ADDRESS to force a REXX program to run under a specific environment:

/* ------------------------------------------------------------------ */
/* sample prolog to force a REXX program to run under a specific      */
/* environment.                                                       */
/*                                                                    */
/* In this example we force the program to run under PMREXX           */
/*                                                                    */
/* (see also Force a REXX program to run minimized and the warning    */
/* in the section PMREXX)!                                            */
/*                                                                    */

                        /* name of the needed environment (return     */
                        /* code of the function ADDRESS)             */
  neededEnvironment = "PMREXX"

                        /* program to call to setup the correct       */
                        /* environment                                */
  executor = "PMREXX.EXE"

                        /* check the address environment              */
  if address() <> neededEnvironment then
  do
                        /* THIS part is only executed if NOT running  */
                        /* under the needed environment               */

                        /* get the name of this program               */
    parse source .. thisProg

                        /* now call the program again using the       */
                        /* defined program to setup the environment   */
    "start " executor  thisProg arg(1)

    exit rc
  end /* if address() <> neededEnvironment then */

                        /* THIS part is only executed if we are       */
                        /* running under the needed envrionment!      */
  call RxMessageBox "This program was forced to run under PMREXX",,
                    "This is a test message"

exit 0

Force a REXX program to run minimized

Use the following method to force a REXX program to run in a special way (e.g. minimized, in the background, in a full screen session, ...)

/* sample prolog for a REXX program to run minimized                  */
/* (see also Run a REXX program under a specific environment)         */

                    /* turn CTRL-BREAK OFF                            */
  signal off halt

                    /* get the name of this file                      */
  parse source . . thisFile

                    /* get the parameter                              */
  parse arg thisParameter

                    /* check the pass of this file                    */
  pass2Parameter = "$$PASS2$$"

  if word( thisParameter,1 ) <> pass2Parameter then
  do
                    /* this is the first pass: restart the            */
                    /* CMD minimized                                  */
     address "CMD" "start /C /MIN /BG cmd /c " ,
                     thisFile pass2Parameter thisParameter

                    /* and end the program                            */
     exit

  end /* if */

                    /* this is the second pass: cleanup the parameter */
                    /* and continue                                   */
  parse var thisParameter (pass2Parameter) thisParameter

  /* !!! insert your code here !!! */
exit

Start a REXX program in PM mode

To start a REXX program in a PM environment there are at least three methods available (note that a REXX program must run in a PM environment to use the function RxMessageBox()):

1. Method
Start the program from within a PM program (like EPM) or use the program PMREXX to start the REXX program. (see also #Run a REXX program under a specific environment)
You can also use one of the visual REXX development tools for the REXX program (like VX-REXX, VisPro/REXX or GpfREXX; see also the REXX development tools & extensions listed in the section PM Tools). These environments normally provide better PM controls than RxMessageBox().
2. Method
Start the REXX program with the following command:
start /pm cmd /c {programPath}programName
Note that this is not possible in 4OS2 sessions!
3. Method
Create an object with PROGTYPE set to PM for the REXX program and use the function SysOpenObject or SysSetObjectData to start the program:
/* REXX code to create a WPS object to start the REXX program         */
/*   C:\TESTPM.CMD                                                    */
/* in a PM environment (assuming OS/2 is installed on drive C:)       */

                    /* load the necessary REXXUTIL functions          */
  call rxFuncAdd "SysLoadFuncs", "REXXUTIL", "SysLoadFuncs"
  call SysLoadFuncs

                    /* create the object                              */
  if SysCreateObject( ,
      "WPProgram"                               ,, /* object class    */
      "REXX program in a PM environment"        ,, /* object title    */
      "<WP_DESKTOP>"                            ,, /* object location */
      "EXENAME=C:\OS2\CMD.EXE;"            || ,    /* object setup    */
      "PROGTYPE=PM;"                       || ,    /*  string         */
      "PARAMETERS=/C C:\TESTPM.CMD;"       || ,
      "OBJECTID=<MyObject>;"                    ,, /* replace flag    */
      "U" ) = 1 then
  do
                    /* execute the program (all OS/2 version, you can */
                    /* also use SysOpenObject in OS/2 WARP)           */
    call SysSetObjectData  "<MyObject>", "OPEN=DEFAULT";

  end /* if SysCreateObject( ... */

Starting other programs in REXX programs

This sections contains some hints about starting other programs from within a REXX program.

Start an OS/2 program synchronously

Some PM programs (for example VIEW.EXE and NOTES.EXE) start asynchronously if called from a REXX program. Use the following code to start one of these programs and wait until it ends (You can also use this technique for a simple Process controller in REXX; see also Start a Windows program synchronously and StartDOS):

/* sample code to start an OS/2 program synchronously                 */

                    /* load the necessary REXXUTIL function(s)        */
  call rxFuncAdd "SysSleep", "REXXUTIL", "SysSleep"

                    /* name of the "semaphore" file                   */
                    /* Note: If you want to run this program in       */
                    /*       separate sessions at the same time, use  */
                    /*       a routine that lets you get a unique     */
                    /*       name for a temporary file, to get a      */
                    /*       unique name for your semaphore files.    */
                    /*       (for example                             */
                    /*       Get a name for a temporary file)         */
  SemFile = "TEST.SEM"

                    /* start the program and redirect STDOUT into the */
                    /* "semaphore" file                               */
                    /* Note that this technique does not work    3.20 */
                    /* if there is already an instance of the    3.20 */
                    /* view program running!!!                   3.20 */

  "view cmdref.inf >" || semFile

                    /* wait until the program ends                    */
  do until stream(  semFile, "c", "OPEN READ" ) = "READY:"
    call SysSleep 1
  end /* do until ... */

                    /* close and delete the "semaphore" file          */
  call stream semFile, "c", "CLOSE"
  "del " semFile "2>NUL 1>NUL"
exit 0

Note that this technique does not work if there is an instance of the program to start already running and if the call of the program only passes the parameter to the running instance. This is true for view.exe, for example.

Start a Windows program synchronously

To start a Windows program synchronously, you must use a DOS batch file. The batch file might look like (see also Start an OS/2 program synchronously and StartDOS):

REM *** DOS Batch
REM     Usage: DOSSTART.BAT semFile windowsProgram {progParameters}
REM
SET semFile=%1
SET programName=%2
SET parameter=%3
:pLoop
SET parameter=%parameter% %3
SHIFT
IF NOT "%3" == "" GOTO pLoop

%programName% %parameter% >%semFile%
exit

The calling REXX program might look like

/* sample code to start a Windows program synchronously               */

                    /* load the necessary REXXUTIL function(s)        */
  call rxFuncAdd "SysSleep", "REXXUTIL", "SysSleep"

                    /* name of the "semaphore" file                   */
                    /* Note: Use a routine like for example           */
                    /*       Get a name for a temporary file          */
                    /*       to get a unique name for the semaphore   */
                    /*       file if you want to run this program     */
                    /*       in separate sessions at the same time.   */
  semFile = 'd:\sem'
  winProg = 'd:\win\apps\excel4\excel'
  'DOSSTART.BAT' semFile winProg
   do forever
      say 'Waiting for the Windows Session ... '
      if stream( semFile, 'c', 'OPEN READ' ) = 'READY:' then
        leave
       call SysSleep 1
   end

                    /* close and delete the "semaphore" file          */
   call stream semFile, 'c', 'CLOSE'
   'del ' semFile '2>NUL 1>NUL'

Detach a program & capture the PID

Use the following routine to DETACH a program and capture the PID of the detached program.

/*                                                                    */
/* sample routine to detach a program and return the process          */
/* identification number (PID)                                        */
/*                                                                    */

                    /* get the parameter                              */
  parse arg progName progParameter

                    /* detach the program                             */
  thisRC = DetachProgram( progName, progParameter )

                    /* and process the return code                    */
  if word( thisRC,1 ) = "ERROR:" then
    say "DetachProgram returned the error message: " thisRC
  else
    say "The process identification number of the",
        "detached process is <" || thisRC || ">."
exit

/* ------------------------------------------------------------------ */
/* function: detach a program and return the process identification   */
/*           number                                                   */
/*                                                                    */
/* call:     DetachProgram programName {, programParameters}          */
/*                                                                    */
/* where:    programName - name of the program                        */
/*          programParameters - parameter for the program             */
/*                                                                    */
/* returns:  ERROR n: error n occurred                                */
/*             where n is                                             */
/*              -255 : unknown error occurred                         */
/*                -1 : parameter missing                              */
/*                -2 : program to detach not found                    */
/*           else the process identification number                   */
/*                                                                    */
/* Note:     This routine uses static names for the temporary files   */
/*           and thus cannot run in separate programs at the same     */
/*           time (see below for a solution of this limitation)!      */
/*                                                                    */
/*           This routine assumes that the PID is always the last     */
/*           word in the output of the detach command.                */
/*                                                                    */
/*                                                                    */
DetachProgram: PROCEDURE
  parse arg ProgramName , ProgramParameter

  programName = strip( programName )

                        /* set the necessary ADDRESS environment      */
                        /* Note that the previous ADDRESS environment */
                        /* is automatically restored after this       */
                        /* routine ends.                              */
  ADDRESS "CMD"

                        /* init the return code                       */
  thisRC = "ERROR: -255"
  if programName <> "" then
  do
    if stream( programName, "c", "QUERY EXISTS" ) <> "" then
    do

                    /* get the path for the temporary files           */
      tempPath = value( "TEMP",, "OS2ENVIRONMENT" )
      if tempPath = "" then
        tempPath = value( "TEMP",, "OS2ENVIRONMENT" )
      if tempPath = "" then
        tempPath = directory()

      if right( tempPath, 1 ) <> "\" then
        tempPath = tempPath || "\"

                    /* Note:                                          */
                    /* Change the algorithm to get the names for the  */
                    /* logfile and the temporary copy of the logfile  */
                    /* if you want to run this routine in more than   */
                    /* one program at the same time!!!                */
                    /* (e.g. use the routine GetTempFile from RXT&T)  */

                    /* create the name for the STDERR logfile         */
                    /* of the detached program                        */
      STDErrLogFile = tempPath || "$$out$$.ERR"

                    /* create the name for the temporary copy of the  */
                    /* logfile                                        */
      TempLogFile =   tempPath || "$$out$$.TMP"

                    /* detach the program redirecting STDOUT to NUL   */
                    /* and STDERR into the logfile                    */
                    /*                                                */
                    /* The use of another copy of the CMD.EXE is      */
                    /* necessary to handle batch files and REXX       */
                    /* programs correctly                             */
                    /*                                                */
      "@detach cmd /c " ProgramName ,
                       ProgramParameter ,
                     "2>" || STDErrLogFile ,
                     "1>" || "NUL"

                    /* because we can't read the logfile in REXX at   */
                    /* this time (it is still opened), we use the     */
                    /* TYPE command to copy the current contents      */
                    /* of the logfile into another file               */
                    /*                                                */
                    /* Note that you cannot redirect the output of    */
                    /* the TYPE command into RXQUEUE.                 */
                    /* You cannot use the COPY command to copy the    */
                    /* file because it is still in use by the         */
                    /* detached program.                              */
                    /*                                                */
      "@type " STDErrLogFile ">" || TempLogFile

                    /* and now we read the first line of the          */
                    /* temporary copy of logfile to get the PID       */
      call stream tempLogfile, "c", "OPEN READ"

      if lines( tempLogFile ) >= 1 then
      do
        curLine = lineIn( tempLogFile )

                    /* we assume the last word of the output message  */
                    /* from DETACH is the PID                         */
        thisRC = Word( curLine, words( curLine ) )
        if right( thisRC,1 ) = "." then
          thisRC = dbrright( thisRC, 1 )

      end /* if */

                    /* last thing to do: close temporary copy of      */
                    /* the logfile and delete it                      */
      call stream tempLogfile, "c", "CLOSE"

      "@del " tempLogFile "1>NUL 2>NUL"

    end /* if stream( programName, "c", "QUERY EXISTS" ) <> "" then */
    else
      thisRC = "ERROR: -2"
  end /* if programName <> "" then */
  else
    thisRC = "ERROR: -1"

RETURN thisRC

Check if a REXX program is already running

Use the REXXUTIL function SysQuerySwitchList (introduced in the new REXXUTIL.DLL from Object-Oriented REXX) to check if a REXX program is already running.

Note: This function is only available in the REXXUTIL.DLL from Object REXX for OS/2 Version OBJREXX 6.00 11 Nov 1997 or newer.

Parameters...

Distinguish between empty and omitted parameter

You can check if a parameter has been ommitted using the function ARG. This is useful if you work with default values for parameter and if an empty string is a valid value for a parameter. (Note that this is only possible in internal or external REXX functions or procedures! see also Starting new VIO/Fullscreen sessions)

Example:

/* sample code to show how to distinguish between omitted and       */
/* empty parameter                                                  */

                    /* call the subroutine with omitted and empty   */
                    /* parameter                                    */
  call TestRoutine  '', 'v2', , 'v4'

exit

/* sample sub routine                                               */
TestRoutine:
  parse arg v1, v2, v3, v4, v5

  do i = 1 to 5
    if arg( i, 'E' ) = 1 then
    do
      say 'Parameter ' || i || ' was entered. '
      say '  The value is "' || arg( i ) || '"'
    end
    else
      say 'Parameter ' || i || ' was omitted. '
  end /* do i = 1 to 4 */
return

Using %0 ... %9 with OS/2 commands

Be aware that the strings %0 ... %9, when used with OS/2 commands in a REXX program, are replaced by the CMD.EXE with the name of the program (%0) and the parameters (%1 ... %9). Example:

/*                                                                    */
                    /* The lineout statement prints                   */
                    /*    %0 %1 %2 %3                                 */
                    /* to the screen                                  */
  call LineOut , "%0 %1 %2 %3"

                    /* The ECHO command prints                        */
                    /*   - the name of the CMD (%0)                   */
                    /*   - and the parameter 1 to 3 (%1 ... %3)       */
                    /* to the screen                                  */
  '@ECHO %0 %1 %2 %3'

The same is true for the names of environment variables enclosed in % used with OS/2 commands.

(see the sections Get the invocation syntax, Get the name of the MAIN program called, and Get the parameters as seen by CMD.EXE or Get the parameters as seen by CMD.EXE - 2 - for usage of this feature)

Parameters for a REXX program

There's a difference in the way REXX programs handle parameters when they're called from the command line (e.g. you enter erexxtry in an OS/2 window) and when they're called from another REXX program (e.g. you write myRC = rexxtry() in your REXX program):

In the first case (when called from the command line), the REXX program always retrieves the parameters given on the command line as single string. Thus, the programmer must include code in the REXX program to parse the string into individual parameters. In the second case (when called from within a REXX program), the REXX interpreter itself splits the parameter string into its parts, much as if it has been called by a REXX routine (see also How to use parse arg).

To get around the differences in these behaviors, you can check the "call type", returned by PARSE SOURCE (see also Simple parameter parsing routine). Then, having identified the type of call that was used, your program can branch to code based on the identified call type:

/* simple example code to handle parameters to a REXX program         */
/* in a manner equivalent to the handling of parameters for a         */
/* REXX routine                                                       */
/*                                                                    */

                    /* get the call type of this routine              */
                    /* DO NOT turn NOVALUE on at this time!           */
  parse source . callType .
  if callType <> "COMMAND" then
    signal Main     /* called as function or procedure                */
  else
  do
                    /* called from the command line                   */
    args = ""
    if pos( ",", arg(1)) = 0 then
    do
                    /* no comma found in the parameters -- use blanks */
                    /* as argument delimiter                          */

                    /* split argument in multiple arguments using     */
                    /* blanks as argument separator                   */
      do i = 1 to words( arg(1) )
        args = args "'" || word( arg(1),i ) || "'"
        args = args ","
      end /* do i = 1 to words( arg(1) ) */
    end /* if pos( ... */
    else
    do
                    /* at least one comma found in the parameters --  */
                    /* assume commas are used to separate the         */
                    /* parameters                                     */

                    /* split argument in multiple arguments using     */
                    /* commas as argument separator                   */
      argT = strip( arg(1) )
      do while argT <> ""
        parse var argT argC "," argT
        argC = strip( argC )
        args = args || "'" || argC || "'"
        args = args ","
      end /* while argT <> "" */
    end /* else */

    drop argT argC
    interpret "call Main " || args

    if symbol( "RESULT" ) = "VAR" then
    do
                    /* return the return code to the caller           */
      return result
    end /* if symbol( "RESULT" ) = "VAR" then */
    else
      return
  end /* else */

/* main function entry point                                          */
/* Note: Do not use PROCEDURE for this routine                        */

Main:
  say "Main called with " || arg() || " arguments:"
  do i = 1 to arg()
    say "  Argument " || i || " is <" || arg(i) || ">"
  end  /* do i = 1 to arg() */
return arg(1)

Parameters eaten by the CMD.EXE

The following parameter, when fed to a REXX program (or a batch program) is interpreted and "eaten" by the command interpreter CMD.EXE (see also Parameters eaten by the REXX interpreter):

  • q

Suppress all output of OS/2 commands while running the program (Use ECHO ON in your REXX program to turn this feature off)

Important: The parameter /Q is always interpreted by the CMD.EXE. It is never seen by the REXX program. This is also true for any parameter beginning with /Q (or /q). Therefore, if you call a REXX program with the parameter '/QUIET' it will only see 'UIET' as parameter!

To avoid this limitation for parameters beginning with /Q you can call the program with the parameter in double quotes--e.g., myProgram "/QUIET". But be aware that in this case the CMD will also pass the double quotes to your program!

Another workaround:

Simply use the parameter /Q twice. Example:

   D:\Test> test2.cmd test /q
        3 *-* parse arg thisargs
          >>>   "test "

   D:\Test> test2.cmd test /q /q
        3 *-* parse arg thisargs
          >>>   "test  /q"

   D:\Test> test2.cmd test /q/quiet
        3 *-* parse arg thisargs
          >>>   "test /quiet"

Source: Barry Lemon

Please note that other command line interpreter, such as 4OS2, for instance, may or may not have the limitation mentioned above.

Parameters eaten by the REXX Interpreter

The following parameters, when passed to a REXX program, are interpreted and eaten by the REXX Interpreter himself (see also Parameters eaten by the CMD.EXE):

This parameter delimiter is reserved for the REXX interpreter. You can't use it for program-specific parameters. Note that this limitation is removed in Object-Oriented REXX

  • t

Create the tokenized version of the program without executing it

To use the parameter //t in Object-Oriented REXX you can add other parameter(s). Example:

    D:\Test> test2.cmd //t dfad
        3 *-* parse arg thisargs
          >>>   "//t dfad"

   D:\Test> test2.cmd //t //t
        3 *-* parse arg thisargs
          >>>   "//t //t"

   D:\Test> test2.cmd //t

(see also Workaround for the // limitation for a method to workaround for these limitations).

Workaround for the // limitation

You may use the following workaround to pass parameters containing // to a REXX program. (see Parameters eaten by the REXX interpreter). I found the base idea of this workaround (using an OS/2 batch to call the REXX program) in a public message in one of the CompuServe forums. In contrast to this version, the version published in the CompuServe message used REXX queues to pass the parameter. But I think that's a lot of overhead for this purpose.

Note: You can also use the method described in the section Get the parameters as seen by CMD.EXE or Get the parameters as seen by CMD.EXE - 2 - to get around this limitation.

First, create an OS/2 batch file to launch your REXX program:

 @ECHO OFF
 REM *** OS/2 Batch program to launch a REXX program with
 REM     normally impossible parameters for REXX programs
 REM
 
 REM *** init the environment variable for the parameters
 SET REXXParms=
 
 REM *** copy the parameters to the environment variable
 REM     (use a loop to handle more than 9 parameters)
 REM
 :PLOOP
   IF '%1' == '' GOTO CALLREXX
     SET REXXParms=%REXXPARMS% %1
     SHIFT
   GOTO PLOOP
 
 :CALLREXX
   REM *** now call the REXX program
   REM *** This line was buggy in RXT&T versions prior to 3.20!
   REM     The parameter for the REXX program is the name of the
   REM     environment variable containing the parameters!
   myRexxProg REXXParms

Second, use the following prolog to get the parameters in your REXX program:

 /* sample REXX program prolog to get the parameters from an           */
 /* environment variable                                               */
 
                     /* get the name of the environment variable with  */
                     /* the parameters                                 */
   parse arg parmVar .
 
                     /* get the parameters from the environment        */
                     /* variable                                       */
   thisParms = value( parmVar , , "OS2ENVIRONMENT" )
 
                     /* if necessary, delete surrounding "             */
   thisParms = strip( strip( thisParms ), 'B', '"' )
   say "The parameters for this program are:" thisParms

And third: Use only the OS/2 batch program to launch your REXX program.

Passing parameters to Rexx Dialog programs

Use the following technique to pass parameters to Rexx Dialog programs:

/* ------------------------------------------------------------------ */
/* sample prolog for Rexx Dialog programs to access parameters from   */
/* the command line.                                                  */
/*                                                                    */
/* Note that you must call this CMD without the prefix RX to make     */
/* this work!                                                         */
/*                                                                    */
/* Use PMREXX to create a WPS object for this program that does not   */
/* open an OS/2 window when running this program.                     */
/*                                                                    */
/* Note further that you cannot use the default REXX queue to pass    */
/* the parameters to the Rexx Dialog program.                         */
/*                                                                    */

                        /* name of the environment variable to pass   */
                        /* the parameters                             */
  envVarName = "ThisParms"

                        /* check the address environment              */
  if address() <> "RXDLG" then
  do
                        /* THIS part is only executed if NOT running  */
                        /* under RXDLG!                               */

                        /* get the name of this program               */
    parse source . . thisProg

                        /* check for parameters and save them in an   */
                        /* envionment variable if any exist           */
    parse arg thisParms
    call value envVarName, thisParms, "OS2ENVIRONMENT"

                        /* now call the program again using RX.EXE    */
    "@RX " thisProg

    exit rc
  end /* if address() <> "RXDLG" then */

                        /* THIS part is only executed if we are       */
                        /* running under RXDLG!                       */

                        /* get the parameters                         */
  thisParms = value( envVarName, , "OS2ENVIRONMENT" )

  rxSay "Parameters for this program are: " thisParms
exit 0

Special environment variables for REXX programs

The following environment variables have a special meaning for REXX programs:

RXTRACE

If the environment variable RXTRACE is set to ON before executing a REXX program, the REXX program is executed in single-step mode. This is equivalent to including the REXX TRACE command with the parameter ?R. Any other value for the variable is ignored by the REXX interpreter.

Note
The REXX Report 1994 contains a detailed description about the debug facilities of REXX.
Hint
To prevent the execution of a REXX program in single-step mode, even if the environment variable RXTRACE is set to ON, use the statement:
dummy = trace( 'OFF' )

as the first statement of your program. This is very useful to prevent tracing programs like PMREXX for a REXX program "compiled" with programs like REXXCC.

(see also Catching debug output)

RXQUEUE

This environment variable contains the name of the queue that the program RXQUEUE.EXE should use if the queue name is NOT specified when RXQUEUE is launched. The default queue used by RXQUEUE is the default REXX queue SESSION. (see also The RXQUEUE filter)

Special filenames

INIT.CMD

"There is a hint in the OS/2 developer documentation that there was at one time a planned use for the file INIT.CMD. I would recommend that you use a different file name than INIT.CMD regardless."

Source: Richard K. Goran

OS2INIT.CMD

"In earlier (1.x) versions of OS/2, the OS2INIT.CMD file was used as a session-by-session AUTOEXEC.BAT, by setting the shell in the config.sys to be CMD.EXE /K OS2INIT.CMD instead of just CMD.EXE. (It's still possible to do that now)."

"Most recommendations I have read say you should not name any file OS2INIT.CMD because it is still considered to be 'reserved' (like STARTUP.CMD)."

(see also Simulating an AUTOEXEC.BAT for OS/2 sessions)

String constants

The maximum length for a string constant in OS/2 REXX is 250. To use strings longer than 250 chars, split them into pieces less than 250 chars and concatenate them in a variable. (There's no limitation for the length of the contents of a variable in OS/2 REXX.) Example:

 myConstant =,
       "This is a really long string constant.                  " || ,
       "Well, because it is to long for a constant in OS/2 REXX " || ,
       "We split it into smaller pieces and concatenate them    " || ,
       "with the operator || to a big string and save the       " || ,
       "result in a variable. This is possible, because there   " || ,
       "is no limit for the length of a variable in REXX.       "

Source: IBM: OS/2 2.0 Procedures Language/2 Reference Manual

Variables

Special variables

A special variable is one that may be set automatically during processing of a REXX program.

There are 3 (5 in Object-Oriented REXX) special variables used by the REXX interpreter:

RC

This variable contains the return code of the last external non-REXX command called (e.g. OS/2 commands like DIR or EAUTIL) In case of a SYNTAX error this variable is set to the syntax error number (1-99). After an ERROR or FAILURE condition RC contains the command return code. Commands executed manually while tracing interactively do not change the value of RC. (see also Pipes & Errorlevel)

RESULT

This variable contains the return code of an internal or external REXX function if called with the CALL statement. The variable RESULT is dropped if the REXX function doesn't return a return code. It's also dropped if the function is called in an implicit or explicit assignment (e.g. a = myFunc() or if myFunc() = 4 then)

SELF (Object-Oriented REXX only)

"This variable is set when a method is activated. Its value is the object that forms the execution context for the method (that is, the receiver object of the activating message)."

SIGL

This variable is set to the line number of the last instruction that caused a transfer of control to a label (that is, any SIGNAL, CALL, internal function call, or trapped condition).

SUPER (Object-Oriented REXX only)

"This variable is set when a method is activated. Its value is the class object that is the usual starting point for a superclass method lookup for the SELF object."

Note
You can change these variables just like any other REXX variable. You should save the values of the special variables in a normal variable if you want to use them in other statements!

Read-only and Write-only environment variables

You can use REXX to create environment variables that the CMD.EXE either can not read or not write.

To create read-only environment variables use names with an imbedded equal sign, for example =MYREXXVAR.

The CMD.EXE can only read (but not change) environment variables that contain equal signs in the name. It is not possible to modify or delete those variables from the command line or from within a plain batch program.

To create write-only environment variables use the names 0, 1, to 9.

The CMD.EXE can only write (or delete) these environment variables but not read them. If you code something like

 ECHO %1
 SET myvar=%2

%1 or %2 always reference the parameter of the batch program. And if you use them on the command line they are either empty or contain their name as value.

Compound variables

To avoid mysterious errors you should always use some special chars as first chars of a tail (for example two underscores __) and never use these chars for normal variables (or vice versa).

Another nice method to avoid errors: Always use a number (0..9) for the first character of the tails. This method works because variable names can't begin with a number.

Source: I found this hint in the documentation for the DLL RXU

Defining compound variables

A useful technique to define compound variables is

                       /* stem with the WPS objects to create        */
 j = 0
 objects.0 = j

 j = j + 1
                       /* use the index "CodeServerStemEntry"  v1.80 */
                       /* for reference to this entry          v1.80 */
 CodeServerStemEntry = j                                    /* v1.80 */

 objects.j.__class = "WPFolder"
 objects.j.__title = ServerName || "^CodeServer"

 j = j + 1
 objects.j.__title = "Start the ^CodeServer" || ServerName
 objects.j.__class = "WPProgram"

 j = j + 1
 objects.j.__title = "Stop the ^CodeServer" || ServerName
 objects.j.__class = "WPProgram"

 j = j + 1
 objects.j.__title = "Status the ^CodeServer" || ServerName
 objects.j.__class = "WPProgram"

 objects.0 = j

                       /* access the entry for the folder      v1.80 */
 FolderTitle = objects.CodeServerStemEntry.__title          /* v1.80 */

This technique makes it possible to insert and delete further elements without changing the index for the other elements. (see also Using "open" compound variables)

Using "open" compound variables

If you're using compound variables completely defined in your source code, you may use a technique I call "open compound variables" shown in the example below.

The advantage of this method: You can add new variables to the end of the compound variable without maintaining a counter (stem.0 is not used). I often forget to correct the counter after adding new entries to the compound variable. Note that using this method in Classic REXX is only useful if you define the compound variable completely in the source code (no changes at runtime)!

In Object-Oriented REXX you can use it anyway because there you can get all stem entries very simply with the DO ... OVER construct (see the code at the end of the example).

/* sample for using "open" compound variables                         */

                    /* drop the stem (to be sure)                     */
  drop nameStem.

                    /* define the compound variable                   */
  nameStem.1 = "Albert"
  nameStem.2 = "Robert"
  nameStem.3 = "William"

                    /* use the compound variable                      */

                    /* use the function symbol to find the last entry */
                    /* of the compound variable                       */

  do i = 1 while symbol( "nameStem." || i ) = "VAR"

    say "Entry no. " || i || " is " nameStem.i

  end /* do i = 1 while symbol( "nameStem." || i ) = "VAR" */

                     /* Note: i-1 is the number of stem entries. You  */
                     /*       can save this value in the variable     */
                     /*       nameStem.0 and use the normal methods   */
                     /*       in the rest of your program.            */

  say "The compound variable contains " || i-1 || " entries."

/* another useful method for a loop over all entries            v2.50 */
/* Note that this method is only possible in Object REXX!!!     v2.50 */

   parse version thisVersion .
   if thisVersion = "OBJREXX" then
   do
     say "In Object REXX you can use also the DO OVER construct:"

     j = 0;
     do i over nameStem.
       j = j + 1;
       say "Entry no. " || i || " is " namestem.i
     end /* do i over namestem. */
     say "The compound variable contains " || j || " entries."

  end /* if thisVersion = "OBJREXX" then */

(see also #Defining compound variables)

Initializing compound variables

The initialization of a compound variable with stemName. = "" only works for the first level of the compound variable. Example:

 stemVar. = ""
 /* ok                                 */

 stemVar.ebene1. = ""
 /* won't work as expected             */

Using compound variables

You can't use a compound variable as the tail of another compound variable. (see Using a compound variable as tail of another compound variable for a workaround for this limitation; see also New features in Object REXX that are useful in Classic REXX programs also for a workaround in Object-Oriented REXX)

Using variables for the stem=

Variable substitution for compound variables works only for the tail (that is the part beginning after the first period).

Example:

 stemName = "MYSTEM"
 TailName = "MYTAIL"

 stemName.Test = "1234"
 stemName.TailName = "5678"

 say "MYSTEM.TEST       = " || MYSTEM.TEST
 say "stemName.TEST     = " || stemName.TEST

 say ""

 say "stemName.MYTAIL   = " || stemName.MYTAIL
 say "stemName.TailName = " || stemName.TailName

/* prints:
 MYSTEM.TEST       = MYSTEM.TEST
 stemName.TEST     = 1234

 stemName.MYTAIL   = 567
 stemName.TailName = 567
*/

To get around this, you can use the function VALUE. Example:

 stemName = "MYSTEM"
 TailName = "MYTAIL"

 call value stemName || ".TEST", "1234"
 stemName.TailName = "5678"

 say "MYSTEM.TEST       = " || MYSTEM.TEST
 say "stemName.TEST     = " || stemName.TEST

 say ""

 say "stemName.MYTAIL   = " || stemName.MYTAIL
 say "stemName.TailName = " || stemName.TailName

/* prints:
 MYSTEM.TEST       = 1234
 stemName.TEST     = STEMNAME.TEST

 stemName.MYTAIL   = 5678
 stemName.TailName = 5678
*/

(see Using variables for the tail for the conditions of variable substitution for tails)

Using variables for the tail

"REXX treats the case used for stem variables and any variables used within the tail of compound variables differently. Stem variables (up to the first period) are uppercased like any simple variable in REXX. The contents of the tail are processed in the case written."

See the following example:

/* */

tail1 = 'abcd'
tail2 = 'ABCD'

myStem.abcd = 1
say myStem.tail1       /* prints "MYSTEM.abcd"                       */
say myStem.tail2       /* prints "1"                                 */

You can use any character for the names of tails:

/* */

 possibleTail1 = XRANGE( "00"x, "7F"x )
 possibleTail2 = XRANGE( "80"x, "FF"x )

 myStem.possibleTail1 = 111
 myStem.possibleTail2 = 222

 say myStem.possibleTail1      /* prints "111"                       */
 say myStem.possibleTail2      /* prints "222"                       */

(see Sample for using compound variables for an example of using this feature; see also #Using variables for the stem)

Writing general routines for compound variables

To write general routines working on compound variables you may use the techniques shown in the two examples below.

In the first example we use two nested routines to implement a save routine with a local variable scope for processing the data. The only global variable that the routines knows is the stem that it should process

In the second example we use a global variable with the name of the stem variable.

Note that Object-Oriented REXX allows stem variables as parameters and also as return values. Therefore you don't need these techniques in programs running in Object-Oriented REXX (see also the part about the local and global environment in the section New features in Object REXX that are useful in Classic REXX programs also).

(see also Read a textfile using CharIn() for another example).

/* sample for a general routine to work on compound variables         */

                    /* create some global variables                   */
  myStem.0 = 4
  myStem.1 = 'MyElement1'
  myStem.2 = 'MyElement2'
  myStem.3 = 'MyElement3'
  myStem.4 = 'MyElement4'

  yourStem.0 = 3
  yourStem.1 = 'YourElement1'
  yourStem.2 = 'YourElement2'
  yourStem.3 = 'YourElement3'

  i = 5
  j = 7

                    /* display the values of i and j before           */
                    /* calling the routine                            */
  say 'I is ' || i || ', J is ' || J || '.'

                    /* display the values of the stems before         */
                    /* calling the routine                            */
  say 'The stem MyStem. contains ' || MyStem.0 || ' elements:'
  do i = 1 to  mystem.0
    say ' Element No ' || i || ' is ' myStem.i
  end /* do i = 1 to  mystem.0 */

  say 'The stem YourStem. contains ' || YourStem.0 || ' elements:'
  do i = 1 to  YourStem.0
    say ' Element No ' || i || ' is ' YourStem.i
  end /* do i = 1 to  mystem.0 */

  'pause'

  say 'Now calling ShowMyArray for myStem. and YourStem. ...'

                    /* now call the routine to show the stem          */
  call ShowMyArray 'myStem.'
  call ShowMyArray 'YourStem.'

  say 'Results:'

                    /* display the values of i and j after            */
                    /* calling the routine                            */
  say 'I is ' || i || ', J is ' || J || '.'

                    /* display the values of the stems after          */
                    /* calling the routine                            */
  say 'The stem MyStem. contains ' || MyStem.0 || ' elements:'
  do i = 1 to  mystem.0
    say ' Element No ' || i || ' is ' myStem.i
  end /* do i = 1 to  mystem.0 */

  say 'The stem YourStem. contains ' || YourStem.0 || ' elements:'
  do i = 1 to  YourStem.0
    say ' Element No ' || i || ' is ' YourStem.i
  end /* do i = 1 to  mystem.0 */

exit

/* This is the procedure called by the main program                   */
/* Note: Do _NOT_ use PROCEDURE for this routine!!!                   */

ShowMyArray:
                                                             /* v2.60 */
  I!.__stemName = arg(1)

                    /* now call the internal routine with the local   */
                    /* variable scope                                 */
  call  I!.__ShowMyArray I!.__stemName
RETURN

/* This is the procedure with only knows the global                   */
/* variable which name is saved in the variable I!.__stemName.        */

I!.__ShowMyArray: PROCEDURE expose (I!.__stemName)
  parse arg theStem

                    /* theStem contains the name of the stem to       */
                    /* process                                        */
                    /* Note that you've to use the value function to  */
                    /* access the variable                            */

                    /* j and i are local variables!                   */
  j = value( theStem || '0' )

  say 'The stem "' || theStem || '" contains ' || j || ' elements:'

  do i = 1 to j
    say ' Element ' || i || ' is: "' || value( theStem || i ) || '"'
  end /* do i = 1 to j */

                    /* add some elements to the stem                  */
  say 'Now adding 4 further elements to the stem ...'
  do i = j+1 to j+4
    call value theStem ||  i , 'New Element No ' || i-j
  end /* do i = j+1 to j+4 */

                    /* correct the counter                            */
  call value theStem || '0', i-1

RETURN

 
/* another sample for a general routine to work on compound variables */

                    /* create some global variables                   */
  myStem.0 = 4
  myStem.1 = 'MyElement1'
  myStem.2 = 'MyElement2'
  myStem.3 = 'MyElement3'
  myStem.4 = 'MyElement4'

  yourStem.0 = 3
  yourStem.1 = 'YourElement1'
  yourStem.2 = 'YourElement2'
  yourStem.3 = 'YourElement3'

  i = 5
  j = 7
                    /* display the values of i and j before           */
                    /* calling the routine                            */
  say 'I is ' || i || ', J is ' || J || '.'

                    /* display the values of the stems before         */
                    /* calling the routine                            */
  say 'The stem MyStem. contains ' || MyStem.0 || ' elements:'
  do i = 1 to  mystem.0
    say ' Element No ' || i || ' is ' myStem.i
  end /* do i = 1 to  mystem.0 */

  say 'The stem YourStem. contains ' || YourStem.0 || ' elements:'
  do i = 1 to  YourStem.0
    say ' Element No ' || i || ' is ' YourStem.i
  end /* do i = 1 to  mystem.0 */

  'pause'

  say 'Now calling ShowMyArray for myStem. and YourStem. ...'

                    /* now call the routine to show the stem          */

                    /* 'stemName' contains the name of the stem to    */
                    /* process                                        */
  stemName = 'myStem.'
  call ShowMyArray

                    /* 'stemName' contains the name of the stem to    */
                    /* process                                        */
  stemname = 'YourStem.'
  call ShowMyArray

  say 'Results:'

                    /* display the values of i and j after            */
                    /* calling the routine                            */
  say 'I is ' || i || ', J is ' || J || '.'

                    /* display the values of the stems after          */
                    /* calling the routine                            */
  say 'The stem MyStem. contains ' || MyStem.0 || ' elements:'
  do i = 1 to  mystem.0
    say ' Element No ' || i || ' is ' myStem.i
  end /* do i = 1 to  mystem.0 */

  say 'The stem YourStem. contains ' || YourStem.0 || ' elements:'
  do i = 1 to  YourStem.0
    say ' Element No ' || i || ' is ' YourStem.i
  end /* do i = 1 to  mystem.0 */

exit

ShowMyArray: PROCEDURE expose (stemName)

                    /* stemName contains the name of the stem to      */
                    /* process                                        */
                    /* Note that you've to use the value function to  */
                    /* access the variable                            */

                    /* j and i are local variables!                   */
  j = value( StemName || '0' )

  say 'The stem "' || StemName || '" contains ' || j || ' elements:'

  do i = 1 to j
    say ' Element ' || i || ' is: "' || value( StemName || i ) || '"'
  end /* do i = 1 to j */

                    /* add some elements to the stem                  */
  say 'Now adding 4 further elements to the stem ...'
  do i = j+1 to j+4
    call value StemName ||  i , 'New Element No ' || i-j
  end /* do i = j+1 to j+4 */

                    /* correct the counter                            */
  call value StemName || '0', i-1
RETURN
Sample for using compound variables

Use the string-oriented search features of REXX where possible.

Example: Suppose you've got a file in the following format:

Doe John , 01.03.65, Engineer
Miller Franklin, 03.04.67, Butcher
Gernot Martha, 06.05.65, Saleswoman

To read this file into a compound variable and search an entry you can use the following code:

/*                                                                    */
/* sample for using compound variables - slow method                  */
/*                                                                    */

/* ------------------------ variable section ------------------------ */

                    /* name of the input file                         */
  inFile = "pdata.txt"

                    /* name for the output file                       */
  outFile = "data.new"

                    /* compound variable for the file contents        */
  persons. = ""
  persons.0 = 0

/* ------------------------- read the file -------------------------- */

                    /* read the file                                  */
  do i = persons.0+1 while lines( inFile ) <> 0
    persons.i = lineIn( inFile )
  end /* do while lines( inFile ) <> 0 */

  persons.0 = i-1

/* ------------------------ search an entry ------------------------- */

                    /* compound variable with the entries to search   */
  entriesToSearch.0 = 2
  entriesToSearch.1 = "Miller Franklin"
  entriesToSearch.2 = "MILLER Franklin"

  do j = 1 to entriesToSearch.0
    curSearchEntry = entriesToSearch.j

    found = 0
    do i = 1 to persons.0 while found=0
      if pos( curSearchEntry, persons.i ) = 1 then
        found = i
    end /* do i = 1 to persons.0 while found=0 */

    if found <> 0 then
      say "Entry <" || curSearchEntry || "> found: " || ,
           persons.found
    else
      say "Entry <" || curSearchEntry || "> NOT found!"

  end /* do j = 1 to entriesToSearch.0 */

/* --------- write the compound variable back into a file ----------- */

  do i = 1 to persons.0
                    /* ignore deleted entries                         */
    if persons.i <> "" then
      call LineOut outFile, persons.i
  end /* do i = 1 to persons.0 */

You can use this method - but it's not very fast (especially if you've got hundreds or thousands of entries).

A better method to do this is: Use a key field as name for the tail. Example:

/*                                                                    */
/* sample for using compound variables - faster method                */
/*                                                                    */

/* ------------------------ variable section ------------------------ */

                    /* name of the input file                         */
  inFile = "pdata.txt"

                    /* name for the output file                       */
  outFile = "data.new"

                    /* compound variables for the file contents       */
  persons. = ""
  persons.0 = 0

                    /* "index" variable                               */
  personsIndex = ""

                    /* separator for the index variable               */
  separator = "00"x

/* ------------------------- read the file -------------------------- */

  do i = 1 while lines( inFile ) <> 0
    parse value lineIn( inFile ) with entryKey "," entryValue

                    /* in this example the key field is the name of   */
                    /* the person                                     */
    entryKey = strip( entryKey )


                    /* check for duplicate keys and create the stem   */
                    /* entry if possible                              */
    if persons.entryKey <> "" then
    do
                    /* duplicate key found                            */
      say "Error: Duplicate key <" || entryKey || "> in the file!"
    end /* if persons.entryKey <> "" then */
    else
    do
                    /* save the key in the "index" variable           */
      personsIndex = personsIndex || entryKey || separator

      persons.entryKey = entryValue
    end /* else */

  end /* do i = 1 while lines( inFile ) <> 0 */

  persons.0 = i-1

/* ------------------------ search an entry ------------------------- */

                    /* compound variable with the entries to search   */
  entriesToSearch.0 = 2
  entriesToSearch.1 = "Miller Franklin"
  entriesToSearch.2 = "MILLER Franklin"

  do i = 1 to entriesToSearch.0
    curSearchEntry = entriesToSearch.i

    if persons.curSearchEntry <> "" then
      say "Entry <" || curSearchEntry || "> found:" || ,
           persons.curSearchEntry
    else
      say "Entry <" || curSearchEntry || "> NOT found!"
  end /* do i = 1 to entriesToSearch.0 */


/* --------- write the compound variable back into a file ----------- */

  do while personsIndex <> ""

                    /* get the next variable name                     */
    parse var personsIndex curPerson (separator) personsIndex

                    /* ignore deleted entries                         */
    if persons.curPerson <> "" then
      call LineOut outFile, curPerson || "," || persons.curPerson

  end /* do while personIndex <> "" */

Note that you can use any character for the tail of a compound variable. But keep in mind, that using this method the name of the tail is case sensitive! (see also Using variables for the tail)

Local variables

It is best to use one compound variable for all local variables of routines declared without the PROCEDURE keyword instruction. The reason: In this case you need only the stem of this compound variable as parameter for the keyword instruction DROP at the end of the routine to delete all local variables. Likewise, you can use this one stem to initialize your local variables all at once or to assign them all the same value at some point in the program. And, of course, if you're using unique stems for all procedures, your local variables can't interfere with your global variables (or with the local variables of other procedures).

Note: See Copy the Queue into a compound variable for a working example.

Global variables

It is best to use one compound variable for all global variables of your program. Why? Because then you need only the stem of this compound variable in the variable list for the option EXPOSE to make all global variables known to the procedures declared with PROCEDURE -- regardless of how many global variables you define. And if you define some more global variables whit this same stem while developing your program, they will automatically be known to all procedures.

Global variables across more than one REXX program

It is not possible to have global variables across more than one REXX program in Classic REXX, without the support of external modules that provide this capability. (In Object-Oriented REXX, however, such global variables are possible; see New features in Object REXX that are useful Classic REXX programs). To get around this limitation, a lot of DLLs for Classic REXX have been written which contain functions to share global variables between REXX programs.

You may, for example, use the function RxPassByName from the DLL RXU to share variables between two or more CMDs:

"One thing I'd want to emphasize in the description of RxPassByName() is that it not only allows one Rexx program to call another, but most importantly it allows two Rexx command files (.CMD) to share their variable pools in a manner similar to the way the "EXPOSE" instruction lets different procedures in a single .CMD file share their variables."

Dave Boll, Author of RXU

Another way to implement global variables across more than one REXX program running in the same session is to use Environment Variables.

In this case, I suggest you use "read-only" environment variables (see Read-only and Write-only environment variables)

Note: Be aware that there seems to be a restriction for the maximum length of the contents of an environment variable! If you have environment variables with more than 1024 characters you'll get SYS3175 in CMD.EXE - sooner or later.

A way to implement global variables across more than one REXX program running in different sessions is to use REXX Queues - see Using REXX queues for global variables for some sample code to use this technique.

Be aware that there is no way to get a list of existing REXX queues. Therefore you should not loose the name of your global variables if using this technique. Note also that I don't know how much overhead there is for a REXX queue.

Source: Harold Putmann

Another method to implement global variables is described in the section #Using Persistent Variables.

Using Persistent Variables

Persistent variables are variables, that don't lose their values between two runs of a program (even if you reboot your PC between the run).

There is no native support for persistent variables in REXX. Therefore you must implement the save and load process of those variables in your program yourself.

You can use a data file or an INI file (but not OS2.INI!) for this purpose.

Another place to save persistent variables is in the Extended Attributes of your REXX program. The adavantage of this method is, that you can move or copy the REXX program and the persistent variables will always be there. I've found this very clever hint in a message from Erik Schneller.

Note that you should use the defined format for Extended Attributes also for your new Extended Attributes - see Extended Attribute Data Types - and you shouldn't use one of the predefined Extended Attributes - see Extended Attributes used by the WPS.

One disadvantage of this method is, that you'll lose the persistent variables if you use a DOS/Windows program to process the file. Note further that the maximum size of all Extended Attributes is limited to 64 K and that your new Extented Attributes may prevent the REXX interpreter from saving the token image of your REXX program in the EAs (see Creating a token image)

(see Sample for persistent variables for a working example using Extended Attributes for persistent variables)

Using Persistent Variables

Persistent variables are variables, that don't lose their values between two runs of a program (even if you reboot your PC between the run).

There is no native support for persistent variables in REXX. Therefore you must implement the save and load process of those variables in your program yourself.

You can use a data file or an INI file (but not OS2.INI!) for this purpose.

Another place to save persistent variables is in the Extended Attributes of your REXX program. The adavantage of this method is, that you can move or copy the REXX program and the persistent variables will always be there. I've found this very clever hint in a message from Erik Schneller (see EMail Addresses). Note that you should use the defined format for Extended Attributes also for your new Extended Attributes - see Extended Attribute Data Types - and you shouldn't use one of the predefined Extended Attributes - see Extended Attributes used by the WPS.

One disadvantage of this method is, that you'll lose the persistent variables if you use a DOS/Windows program to process the file. Note further that the maximum size of all Extended Attributes is limited to 64 K and that your new Extented Attributes may prevent the REXX interpreter from saving the token image of your REXX program in the EAs (see Creating a token image)

(see Sample for persistent variables for a working example using Extended Attributes for persistent variables)

When to Quote Variables in Functions and/or Procedures

Some functions and procedures need the name of a variable as parameter. They use this variable to store the result. Examples for this type of function are SysFileTree and SysGetEA.

To make sure, that the function/procedure stores the result in the variable you intended, in these functions and procedures you should always enclose the variable name in quotes.

Example:

/* */
  call rxFuncAdd 'SysLoadFuncs', 'REXXUTIL', 'SysLoadFuncs'
  call SysLoadFuncs
                     /* init some variables for testing                */
   mystem  = 'TEST'
   mystem1 = 4
                     /* correct use of the variable (see below);       */
                     /* because the variable mystem is enclosed in     */
                     /* quotes, mystem.0 will correctly return the     */
                     /* total number of entries matching the wildcard  */
                     /* file spec 'c:\*.*' and mystem.# will contain   */
                     /* the value of each entry (where # is a number   */
                     /* from 1 to the value of mystem.0)               */
   call SysFiletree 'c:\*.*', 'mystem'

                     /* incorrect use of variable; when the variable   */
                     /* mystem is not quoted in the function call,     */
                     /* REXX uses the value of the variable mystem     */
                     /* (i.e., 'TEST', initialized earlier) as         */
                     /* the name of the stem for the result.  Thus,    */
                     /* the value of mystem.0 will be 'MYSTEM.0',      */
                     /* the number of entries matching the wildcard    */
                     /* spec will be stored in test.0, and test.# will */
                     /* contain the entries themselves (where # is a   */
                     /* number from 1 to the value of test.0)          */
   call SysFiletree 'c:\*.*', mystem

                     /* incorrect use of variable; REXX uses the value */
                     /* of the variable mystem1 as the name of the     */
                     /* stem for the result.  This statement will      */
                     /* raise a syntax error because the value of      */
                     /* mystem1 is '4' and variables in REXX cannot    */
                     /* begin with a number. */
   call SysFiletree 'c:\*.*', mystem1

List all defined variables

See the routine ShowDefinedVars in TEMPLATE.CMD for a routine which lists all defined variables in the scope of a routine (using Dave Boll's excellent DLL RXU).

File, directory and device handling

File and directory names

Double quotes around filenames are never allowed in the parameter for the REXX functions. In contrast to this, OS/2 commands always need double quotes around filenames if the name contains blanks or other special chars. This is also true for directory names.

The conclusion from this scenario: Always save filenames and directory names inside your program without double quotes. Then use a function to add quotes to any filename(s) before using the name(s) as parameter(s) for OS/2 commands executed outside your program (see the routine ConvertNameToOS in the Template for a REXX program)

Output & Input control

To ensure that the output of your program goes to the screen regardless of the current target for STDOUT, use

call lineOut "CON",,
  "This output always goes to the screen!"

/* or */

  /* print a string to the screen */
call LineOut "SCREEN$",,
  "This output goes to the screen"

To print messages to STDOUT or STDERR you can use these names for the output device. For example:

  /* print a string to STDERR */
call LineOut "STDERR",,
  "This output goes to STDERR"

  /* print a string to STDOUT */
call LineOut "STDOUT",,
  "This output goes to STDOUT"

To ensure that the input for your program comes from the keyboard regardless of the current source for STDIN use

userInput = lineIn( "CON" )
/* or */
userInput = lineIn( "KBD$" )

Important: The functions LINEOUT and LINEIN use normal file handles for the output and input devices! Therefore, close the device with the STREAM function after using it!

  • see also Reserved directory & file names, Reserved names for files in REXX;, and Using the handles 3 to 9 in REXX programs see General input line enhancer for an usage example)

Reserved names for files in REXX programs

Use a period after the filename to access files with the names STDOUT, STDIN or STDERR in REXX programs. Another method to distinguish the filenames from the keywords for the standard handles is using the filenames with a path. For example use

       /* using a period after the name */
 call directory( "C:\TEMP" )
 curLine = LineIn( "STDOUT." )

       /* using a fully qualified path  */
 curLine = LineIn( "C:\TEMP\STDOUT" )

       /* using a relative path         */
 call directory( "C:\TEMP" )
 curLine = LineIn( ".\STDOUT" )

to read the next line from the file C:\TEMP\STDOUT.

(see also Reserved directory & file names and Output & Input control)

Maximum files per session

The number of file handles for an OS/2 session is limited to n (where n depends on the OS/2 version you're using, normally n is 20; or 40 in WARP 4). So you can only open about n-5 files at one time, because 5 handles are already used by the standard input and output channels (STDIN, STDOUT, STDERR, ...).

To check or increase this value you may use a tool like MaxFH, SysAddFileHandle (in the REXXUTIL.DLL from Object-Oriented REXX), or similar functions in packages like RXU.

Beginning with WARP 4 Fixpack #6 you can add the statement

 SET SHELLHANDLESINC=n

to your CONFIG.SYS to increase the default numbers of free file handles by n for every shell process (see CONFIG.SYS statements used by OS/2)

Note: There seems to be no file handle limit for Object-Oriented REXX.

Note: see also It runs, runs not, runs, runs not, ...

Opening files

The default mode for opening files in REXX is READ/WRITE (even if you open the file implicitly via the LINEIN function!). This prevents other programs from opening the file even if the program that opens this file first only wants to read from the file.

To avoid being locked out of a file like this, I suggest you use an explicit call of the function STREAM to open a file in READ mode, if you only want to read from it.

Closing files

Because the number of possible file handles is restricted in OS/2 sessions, you should always close every file you've opened as soon as possible. You also need to close a file (and perhaps reopen it again) if you want to call another program to write into or read from this file.

You should only use

 rc = STREAM( filename , "c", "CLOSE" )

to close a file. The other method that is sometimes used, using LINEOUT or CHAROUT without the second parameter, is only valid for files to which you have write access (even if you've only read from the file) (?).

Note: See also Output & Input control

LineIn() versus CharIn()

Use the function CHARIN to read a file as often as possible (instead of using LINEIN). For example:

                       /* read a file into a variable                */
 fileContents = CharIn( myFile, 1, chars( myFile ) )
                       /* close the file                       v2.60 */
 call stream myFile, 'c', 'CLOSE'

See Read a file using CharIn() for an example routine to read a file using CHARIN and split it into lines manually.

Error handling & debugging

Catching debug output

The REXX Interpreter writes the debug output (switched on with the TRACE function or keyword) to STDERR. To redirect this output into a file use

 myProgram 2>>trace.out

To get the trace output on the screen and into a file at the same time (while your REXX program is running) you may use a TEE program:

 myProgram 2>>&1 | tee trace.out

"Note that VX-REXX programs 'hang' when RXTRACE=ON is set.

If RXTRACE=ON is set for REXX programs and STDERR is redirected (i.e 2>file_name), the effect of RXTRACE=ON is the same as TRACE R having been issued at the beginning of the REXX program (i.e., the trace is NOT interactive)."

(Source: Richard K. Goran

The same is true for using the function TRACE in a REXX program and redirecting STDERR of the REXX program into a file.

(see also Special environment variables for REXX programs)

CALL ON SYNTAX

Unfortunately there's no CALL ON SYNTAX possible in REXX, despite thte fact that SIGNAL ON SYNTAX is possible. But there's a workaround for it: Use a separate routine to interpret the statement. In this routine you can install a local error handler for syntax errors (and others). Example:

 
/*                                                                    */
/* sample program to test the "CALL ON SYNTAX" workaround             */
/*                                                                    */

  do forever
    say "Enter a REXX statement (exit to end): "
    curStmt = lineIn()
    if translate( curStmt ) = "EXIT" then
      leave

    say ""
    say "Executing your input ..."
    thisRC = InterpretCommand( curStmt )
    say " ... the result is " || thisRC
  end /* do forever */

exit

/* ------------------------------------------------------------------ */
/* function: interpret a command simulating "CALL ON SYNTAX"          */
/*                                                                    */
/* call:     interpretCommand cmd                                     */
/*                                                                    */
/* where:    cmd - command to interpret                               */
/*                                                                    */
/* returns:  0 - execution okay                                       */
/*           else error executing the command                         */
/*                                                                    */
/*                                                                    */
/* note:     do not use the keyword PROCEDURE!                        */
/*                                                                    */
InterpretCommand:
  parse arg IC.__CurLine

                        /* init return code with failure code         */
  IC.__ThisRC = 1

                        /* install a local error handler              */
  SIGNAL ON SYNTAX    NAME InterpretCommand1
  SIGNAL OFF NOVALUE
  SIGNAL OFF FAILURE
  SIGNAL OFF ERROR
  SIGNAL OFF NOTREADY

                        /* execute the line                           */
  interpret IC.__CurLine

                        /* set return code to OK                      */
                        /* this statement is not executed if an error */
                        /* occurs while executing the previous        */
                        /* interpret command                          */
  IC.__ThisRC = 0

InterpretCommand1:
  if IC.__ThisRC = 1 then
    IC.__ThisRC = rc

                        /* drop unnecessary local variables         */
  drop IC.__CurLine

RETURN IC.__ThisRC

Turn on NOVALUE

You should always turn the NOVALUE condition on. This simplifies the search of an error enormously. You can use the statement SIGNAL to turn the NOVALUE condition on.

Aborting a program in an external routine

Normally you can't abort a REXX program in an external routine using the REXX statement exit. You have to use the OS/2 command exit to abort the main program. But this will also close the current OS/2 session...

To avoid this misbehaviour, you either must call your REXX program with

 cmd /c main

or you use the following code at the start of your main REXX program:

/* ------------------------------------------------------------------ */
/* main.cmd                                                           */

  parse arg thisArgs 'FF'x pass

                    /* check the parameter to detect the current pass */
  if pass <> '$$PASS2$$' then
  do
                    /* cmd was called via "main"                      */
                    /* -> call it again using "cmd /c main"           */
    '@cmd /c %0 ' || thisArgs || 'FF'x  || '$$PASS2$$'
    exit
  end /* if */
                    /* cmd was called via "cmd /c main"               */

                    /* here goes the code of main .. */
  say 'Now MAIN really starts ...'
  say 'Arguments are "' || thisArgs || '"'

  say 'Now calling TEST2.CMD ...'
  call Test2.cmd

  say 'MAIN is still running ...'
exit 0
/* ------------------------------------------------------------------ */

/* ------------------------------------------------------------------ */
/* test2.cmd - sample external REXX routine to show how it works      */

  say "TEST2.CMD starts .."
  say "TEST2.CMD now aborts the MAIN program ..."
  '@exit'
/* ------------------------------------------------------------------ */

Debugging a "compiled" REXX program

Sometimes it's necessary to debug a REXX program from which the source code is not available. In this case issue

set rxtrace=on

in an OS/2 window and call the program from this window.

(see also Catching debug output, Get the source code of a "compiled" REXX program, and Special environment variables for REXX programs)

Get the source code of a "compiled" REXX program

Sometimes it's necessary to get the source code from a "compiled" REXX program. To do this, do the following:

Issue

 set rxtrace=on

in an OS/2 window and call the program from this window. Normally you should now have an interactive REXX interpreter session where you can execute the following code:

 do i = 1 to sourceline(); call lineOut "d:\rexxsource.cmd", sourceLine( i ); end;
 exit

Now the file d:\rexxsource.cmd contains the source code of the REXX program (normally without any comments, of course).

(see also Catching debug output, Special environment variables for REXX programs, and especially Protect the source code of a REXX program if you want to prevent this for your REXX programs)

Protect the source code of a REXX program

To protect the source code for a REXX program you must "compile" it in one way or another (see "Compiling" REXX programs) and you must use the following statement as first statement in your program

 dummy = trace( 'OFF' )

This is necessary to prevent the single-step execution even if the environment variable RXTRACE is set to ON (see Special environment variables for REXX programs).

Programming techniques

Recursive calls

Recursive calls are possible in OS/2 REXX. But be aware that the maximum number of nested control structs (DO, CALL, etc.) in OS/2 REXX is 100.

Source: IBM: OS/2 2.0 Procedures Language/2 Reference Manual

DO loops

To make nested DO loops more readable you should add the name of the repetitor after the END statement.

Example:

 do i = 1 to 4
   do j = 1 to 5
     do k = 1 to 3
       say i j k
     end k
   end j
 end i
Note
As a side effect of this technique the REXX Interpreter can check the nesting of the loops.

In OS/2 WARP 3 the REXX interpreter generates a TRAP D if there are two END statements on the same line without the delimiter ';' between them. This bug is fixed in the WARP 3 Fixpack (source: APAR PJ19346)

IF instruction

Be aware that the REXX interpreter always evaluates the complete IF condition!

Example:

 i = 0
 if i = 1 & myFunc() then
   ...

In this example the REXX interpreter executes the function myFunc() even if the first part of the condition is already false!

Information saved during subroutine & function execution

During internal subroutine (and function) execution the following information is saved and restored upon return from the routine:

  • the status of DO loops and other structures
  • the TRACE action
  • the NUMERIC settings
  • the ADDRESS settings
  • the CONDITION traps (CALL ON and SIGNAL ON)
  • the CONDITION information
  • the elapsed-time clocks (function TIME)
  • the OPTIONS settings

Source: IBM: OS/2 2.0 Procedures Language/2 Reference Manual

Returning more than one value from a function

To write functions returning more than one value you can use the following technique.

/* simple program to show a technique using a function which returns  */
/* more than one value                                                */
/*                                                                    */
/* This example uses a variable character for the separator in the    */
/* returned value.                                                    */
/*                                                                    */

  say "Values returned by the function MyFunc:"
  parse value myFunc() with separator 2 rc1 (separator) rc2 (separator) rc3

  if separator <= "A" then
    say " The separator for the return codes is: ASCII(" || ,
        C2D( separator ) || ")"
  else
    say " The separator for the return codes is: " || ,
        separator

  say " rc1 = """ || rc1 || """"
  say " rc2 = """ || rc2 || """"
  say " rc3 = """ || rc3 || """"
exit 0

/* simple function - does nothing than returning three values         */
myFunc: PROCEDURE

  separator = "00"x     /* character to separate the values in the    */
                        /* returned string                            */
                        /* Note that this can be any value from "00"x */
                        /* to "FF"x not used in one of the return     */
                        /* codes. The calling procedure need not      */
                        /* know the value before calling this         */
                        /* function!                                  */


                        /* sample return codes                        */
  rc1 = 4
  rc2 = "TestRC"
  rc3 = 0

RETURN separator || rc1 || separator || rc2 || separator || rc3

Catching CTRL-C

In REXX programs that don't contain an error handler for CTRL-C, CTRL-C and CTRL-BREAK are processed by the REXX interpreter using the default error handler for CTRL-C. The default error handler for CTRL-C simply prints an error message and ends the program. Here are some ways to make the handling of CTRL-C und CTRL-BREAK more useful.

To install your own error handler you can use either

 signal on halt name myCTRLCHandler

or

 call on halt name myCTRLCHandler

The adavantage of the call statement is that you can use the RETURN instruction in your CTRL-C handler to continue the execution of your program. Thus, using this method, it is possible, for example, to suppress CTRL-C depending on user input, to display specific messages, and so on (see the examples below).

Note: Because of a bug in the CTRL-C handling of the CMD.EXE you should always use

 cmd /c os2_command

to execute internal or external OS/2 commands from within a REXX program (see CTRL-Break & OS/2 commands, see also Information saved during subroutine & function execution).

Examples:

/* sample for a CTRL-C error handler using SIGNAL ON HALT ...       */

                    /* install the error handler for CTRL-C         */
  signal on halt name MyCtrlCHandler

                    /* this is an endless loop                      */
  do forever
     say 'Press CTRL-C to end this program ...'
  end /* do forever */

exit

/* error handler for CTRL-C                                         */
/*                                                                  */
/* This error handler simply ends the program.                      */
/*                                                                  */
MyCtrlCHandler:

                    /* reinstall the error handler for CTRL-C       */
  signal on halt name MyCtrlCHandler

  say 'CTRL-C pressed!'
exit

 
/* sample for a CTRL-C error handler using CALL ON HALT ...         */

                    /* install the error handler for CTRL-C         */
  call on halt name MyCtrlCHandler

  ctrlCPressed = 0

                    /* this is an endless loop                      */
  do until CtrlCPressed = 1
     say 'Press CTRL-C to end this program ...'
  end /* do until CtrlCPressed */

  say 'Program ends now'

                    /* end the program                              */
exit

/* error handler for CTRL-C                                         */
MyCtrlCHandler:

                    /* reinstall the error handler for CTRL-C       */
  signal on halt name MyCtrlCHandler

  say 'CTRL-C pressed (Execution was interrupted in line ' || sigl || ')!'
  say 'Press Y <RETURN> to end the program. Any other key to continue ...'
  if translate( strip( lineIn() ) ) = 'Y' then
  do
                    /* signal "end now" to the main program         */
    ctrlCPressed = 1
  end /* if */
                    /* continue the program at the interupted line  */
return

Self-recreating program

This is a program that recreates itself if called; save it as mother.cmd and try

 mother.cmd>daughter.cmd

Here's the code (author: Ian Collier (see EMail Addresses); ensure that there are only the three lines in your cmd and delete all leading and trailing blanks if you cut and paste the code!):

/* REXX */
p="/* REXX */";r="say p;say %p=%%%p%%%;r=%%%r%%%%;say translate(r,%22%x,%25%x)"
say p;say "p="""p""";r="""r"""";say translate(r,"22"x,"25"x)

Redefinition of internal functions

REXX allows it to redefine any built-in function in a REXX program. If, for example, you define a function named DATE in your program the call

 curDate = date()

calls your newly defined function and NOT the built-in function date()

To call the built-in function - for example in your new defined function - call it like this:

 call "DATE"

or

 curDate = "DATE"()

Important: You must use the name of the built-in function in uppercase!

see also Redefinition of functions from a DLL)

Note
See Expand the function FILESPEC, Check if a queue exists, or Call by value for working examples.

Redefinition of functions from a DLL

You can also redefine a function loaded from a DLL, for example REXXUTIL.DLL (see RxFuncDrop and Call by value for an example for using this technique):

/* example to redefine a function from the DLL REXXUTIL               */

  say "Now calling SysCurPos with (4,5) ..."
  rc = SysCurPos( 4 , 5 )
  say " rc = " || rc

  say "Now calling SysCurPos with (x,y) ..."
  rc = SysCurPos( "x", "y" )
  say " rc = " || rc

exit

                        /* new SysCurPos function                     */
SysCurPos: PROCEDURE
  parse arg p1,p2

                        /* check the type of the parameter            */
  if datatype( p1 ) <> "NUM" | datatype( p2 ) <> "NUM" then
    thisRC = "Invalid parameter!"
  else
  do
                        /* load the original function if not already  */
                        /* loaded                                     */
    if RxFuncQuery( "SysCurPos" ) then
      call RxFuncAdd "SysCurPos", "REXXUTIL", "SysCurPos"

                        /* call the original function                 */
    thisRC = "SYSCURPOS"( p1,p2 )
  end /* else */
RETURN thisRC

(see also Redefinition of internal functions)

Loading more than one DLL with the same function names

To load more than one DLL with the same function names, you can temporarily load the functions from the DLL with other REXX names. This can be done using RxFuncAdd. The REXX name is the first parameter of RxFuncAdd.

Example:

                   /* load the function */
 call rxFuncAdd "MyCls",,
                "REXXUtil",,
                "SysCls"

                   /* use the function  */
 call MyCls

(see RxFuncDrop for another example of using this technique; see RxFuncAdd for a restriction)

Hints for some keyword instructions

This section contains hints for using some of the keyword instructions.

The keyword instruction CALL

There is a bug in the REXX Interpreter of OS/2 WARP allowing external sub routines to have no or any extension (see REXX programs without an extension).

The keyword instruction EXPOSE

To give a subroutine using the keyword PROCEDURE access to more than one global variable, you can use the keyword EXPOSE with a variable list in parenthesis. Be aware, however, that in this case the variable containing the names of the exposed variables is also exposed! This is WAD, but poorly documented in the online help. (see also The keyword instruction PROCEDURE and EXPOSE does not work as expected)

Example:

/* sample program to show a side effect of the EXPOSE keyword         */

  varA = 1
  varB = 2

  exposeVars = "VarA VarB"

  say "Variable values before calling test:"

  say "  varA is " || varA
  say "  varB is " || varB
  say "  exposeVars is " || exposeVars

  say
  say "Now calling the sub routine test ..."

  call test

  say "Variable values after calling test:"

  say "  varA is " || varA
  say "  varB is " || varB
  say "  exposeVars is " || exposeVars

return

test: PROCEDURE expose (exposeVars)

  say " -----------------------------------------------------------"
  say " Now the sub routine test is activ ..."
  say ""
  say "  test is declared as "
  say "    test: PROCEDURE expose (exposeVars)"
  say ""
  say " The values of the known global variables are:"
  say "  varA is " || varA
  say "  varB is " || varB
  say "  exposeVars is " || exposeVars

  say " Now changing exposeVars to ""VarC VarD"" inside of ""test"" ..."
  exposeVars = "VarC VarD"
  say ""
  say " ... the routine ""test"" ends here"
  say " -----------------------------------------------------------"

return

The keyword instruction INTERPRET

If you're using an INTERPRET instruction in a loop, you should execute the complete loop using INTERPRET. This latter method results in greater efficiency.

Example:

In the routine ReadTextFile I use the following code sequence to read the file:

                       /* create the code to read the file           */
 rtf.__iLine = ,
  "do i = 1 until lines( """ || rtf.__fileName || """ ) = 0; "  ,
     rtf.__StemName || "i = lineIn( '" || rtf.__fileName || "');" ,
  "end;"
                       /* and now execute the code to read the file  */
 interpret rtf.__iLine

If I replace this code with the sequence

                       /* read the file                              */
 do i = 1 until lines( rtf.__fileName ) = 0
   rtf.__iLine = rtf.__StemName || i " = lineIn( rtf.__fileName )"
   interpret rtf.__iLine
 end /* do i = 1 ... */

the routine needs about three times longer to read a file!

The keyword instruction PROCEDURE

Be aware that any routine declared with PROCEDURE only allows access to the global variables it knows to all called routines (see also The keyword instruction EXPOSE).

Example:

  i = 4
  k = 3
  j = 2
  say "Value of i at program start is " || i
  say "Now calling MyProc1 from the main function ..."
  call MyProc1
  say " Value of i after call of MyProc1 is " || i

  say "Now calling MyProc2 from the main function ..."
  call MyProc2
  say "Value of i after call of MyProc2 is " || i
exit 0

MyProc1: PROCEDURE expose k j
  say " Now calling MyProc2 from within the function myProc1 ..."
  i = 0
  call MyProc2
RETURN

MyProc2: PROCEDURE expose i
  say "  MyProg2 starts ..."
  say "   Value of i in MyProc 2 is " || i

                        /* This is the global variable i if MyProc2   */
                        /* is called from the main function -- but:   */
                        /* This is a local variable if MyProc2 is     */
                        /* called from MyProc1!                       */
  i = i + 2

  say "   Value of i in MyProc 2 changed to " || i
  say "  ... MyProg2 ends."
RETURN
Note
If you want to use a label with CALL and SIGNAL you cannot use the keyword PROCEDURE!

The keyword instruction SAY

The keyword instruction SAY always appends a CR/LF to the strings it write. To avoid this, use the function CHAROUT instead.

The keyword instruction SIGNAL

There's a bug (or a feature) in the keyword instruction SIGNAL. Just try the following code:

/* show a bug in the SIGNAL instruction */
/*                                      */
  signal value "xyz"
  return

XYZ:
  say "Hello from XYZ"

Source: CompuServe-Message by Charles Daney [Quercus Systems, (see Internet - Web Pages)]

Hints for some REXX functions

This section contains hints for some REXX functions.

The function CHARS()

The function CHARS always opens a file.

You should close the file after calling CHARS or your OS/2 session might run out of free file handles.

Use the function STREAM to get the size of a file. STREAM works also for open files.

The behaviour is the same in Object-Oriented REXX; but because there is no file handle limit in Object REXX your program won't run out of free file handles.

(see also The function CHARS() in Object REXX)

The function DIRECTORY()

If you use the function DIRECTORY on a drive which isn't ready, OS/2 always pops up this nice little error box. There's no way to avoid this (No, even an active trap for NOTREADY conditions will do that).

For a workaround for this behavior see Check if a directory exists

Note: The DLL RXU contains a function to enable and disable popups for hard errors and exceptions.

In OS/2 WARP the function DIRECTORY fails if the name of the directory is too long (more than 132 chars). This bug is fixed in the WARP 3 FixPack (Source: APAR PJ19343).

The function FILESPEC()

The function FILESPEC called with the parameter D (for Drive) returns all chars up to the first colon in the filename. Example:

/*                            */
  say filespec( "D", "__D__:\test" )

/* -> returns:                */
/*      "__D__:"              */

  say filespec( "D", "  D:\test" )

/* -> returns:                */
/*      "  D:"                */

In general, the function FILESPEC only checks the syntax of the parameter string; it does not check if the parameter is a valid file or directory name. (see also Expand the function FILESPEC)

The functions LINEIN() and PULL()

If you've activated the trap of the NOTREADY condition, you got a NOTREADY condition in the following case:

Use the function LINEIN to read from STDIN

Call the program with a redirection for STDIN

LINEIN tries to read the end of the file

You cannot prevent this error by using LINES.

If you're using PULL to read from STDIN you won't get an error. Instead PULL won't detect the end of the file and will read empty lines forever.

This behavior is independent of the last byte in the input file (CTRL-Z or other).

There's a bug in the CTRL-C handling of the LINEIN function:

/* ------------------------------------------------------------------ */
/* sample code to show a bug in the CTRL-C handling of Classic REXX   */
/*                                                                    */

  parse version interpreterType .
  if interpreterType = 'OBJREXX' then
  do
                    /* Object REXX running                            */
    say 'This bug will only occur in Classic REXX'
    exit 0
  end /* if */

                    /* install an error handler for CTRL-C/BREAK      */
  call on halt


  call LineOut , "Enter a string (press CTRl-C to see the bug):"
  UserResponse = lineIn();

  say 'UserResponse is "' || userResponse || '"'

/* -------- uncomment the next statement for a workaround  ---------- */
/*                                                                    */
/*                     close STDIN to avoid a bug in the CTRL-C       */
/*                     handling                                       */
/*                     Note that the next LineIn call reopens STDIN.  */
/*  call stream 'STDIN', 'c', 'close'                                 */
/* ------------------------------------------------------------------ */

  say 'Now executing the statement: userResponse1 = lineIn()'
  UserResponse1 = lineIn()

  say 'UserResponse1 is "' || userResponse1 || '"'

  say 'Now executing the statement: userResponse2 = lineIn()'
  UserResponse2 = lineIn()

  say 'UserResponse2 is "' || userResponse2 || '"'

EndProgram:

exit

/* ------------------------------------------------------------------ */
/* error handler                                                      */

Halt:

  say
  say '*** CTRL-C pressed ***'
  say '    Note that the next calls of LineIn() will return immediately'
  say '    without reading any data!'
  say '*** -------------- ***'
  say
return

In Classic REXXX, pressing CTRL-C while the function LINEIN is active immediately activates the handler for the HALT signal; pressing CTRL-C while the function PULL is active, activates the handler for the HALT signal after entering ENTER.

In Object REXX the behaviour for PULL is similar to the behaviour for LINEIN

The function LINEOUT()

The function LINEOUT never deletes the output file. To rewrite a file, you must delete it with DEL (or SysFileDelte, if using REXXUTIL) and than recreate it.

To empty the file without deleting it you can use the OS/2 command

type nul >filename

In Object-Oriented REXX you can use a new option for the STREAM to open a file in "overwrite" mode (see also The function LINEOUT() in Object REXX).

The function LINES()

The function LINES interprets an EOF ('1A'x) char as end of file, thus returning 0 if the next character is an EOF. Because the function CHARIN does not interpret EOF chars, you should not mix these functions (use CHARS to get the remaining characters in a stream if using CHARIN to read from the stream and use LINES if using LINEIN to read from the stream). (Source: APAR PJ19194)

--- The function LINES always opens a file.

You should close the file after calling LINES or your OS/2 session will run out of free file handles. (see also The function CHARS())

--- see also Checking a queue and The function LINES() in Object REXX

The REXX API functions

If the function RxFuncAdd returns 0 this only means that the new function is registered. It does not mean that you can use this function! The same is true for the function RxFuncQuery.

Example:

 say rxFuncAdd( "TestFunc1", "REXXUTIL", "TestFunc1" )
                       /* result is 0 (= function registered!)       */
 say rxFuncQuery( "TestFunc1" )
                       /* result is 0 (= function registered!)       */
 call TestFunc1
                       /* error: Function not found!                 */

To use a DLL which does not exist in one of the directoies in the LIBPATH you can use a fully qualified path for the name of the DLL containing the functions. But be aware that functions like SysLoadFuncs will not work in this case because they expect the DLL to be in the LIBPATH. You have to register all needed functions from the DLL manually using RxFuncAdd (see RxFuncAdd for a restriction). This is true for REXXUTIL and mostly all DLLs exporting a function to register the other DLL functions. (Source: Documentation for the REXX DosStartSessionTool)

A name for an external function (the first parameter for the function RxFuncAdd) can only be registered once. So, if you want to reRegister a name you must first deRegister it using RxFuncDrop before registering it (or use another REXX name, see Loading more than one DLL with the same function names)

The conclusion from the statements above: To register a function you should use a routine like the following:

LoadMyDll:
                        /* install a temporary error handler          */
                        /* note: the previous error handler is auto-  */
                        /*       maticly restored a the end of the    */
                        /*       routine                              */
  SIGNAL ON SYNTAX NAME InitDllError

                        /* set a marker                               */
  dllInitOK = 0

                        /* first deregister the function              */
  call rxFuncDrop "InitMyDll"

                        /* load the function                          */
  dummy = rxFuncAdd( "InitMyDll", "MYDLL", "InitMyDLL" )

                        /* call the init function                     */
  call InitMyDll
  dllInitOK = 1         /* set the marker, this statement is not      */
                        /* executed if the previous statement fails!  */

InitDllError:
                        /* deRegister the name if the init call       */
                        /* failed                                     */
  if dllInitOK = 0 then
    call rxFuncDrop "InitMyDll"
                        /* returns: 1 - dll init ok                   */
                        /*          0 - dll init error                */
RETURN dllInitOK
Note
see also DLL loading failed, Loading more than one DLL with the same function names and RxFuncDrop

RxFuncAdd

Another thing to remember is that some DLLs (for example RxFtp.DLL, RxSock.DLL) only export a general routine to register all other functions from the DLL (e.g. FtpLoadFuncs, SockLoadFuncs). In this case you can not register functions from the DLL manually.

You may use a tool like ExeHdr to determine what functions are exported by a DLL.

The function RxFuncDrop()

Loading and dropping DLL functions is global for all REXX programs! Therefore you should avoid using RxFuncDrop or any DLL specific unload routine for general REXX DLLs like REXXUTIL.

To make your REXX programs resident against other programs dropping DLL functions you can use one of three methods. Each method has its advantages and disadvantages. You may use the one you like best.

Method 1. Load the necessary functions with REXX names other than their normal REXX names. In this case you should unload the functions at program end. (see Loading more than one DLL with the same function names for an example)

Method 2. Redefine the necessary functions in your program. Add code to load the DLL routine again if it was dropped to the new function in your program (see Redefinition of functions from a DLL and method 3 below for an example)

Method 3. Use a general wrapper function to call the functions from the DLL. Example:

/* ------------------------------------------------------------------ */
/* sample code to check the REXXUITL wrapper                          */
/*                                                                    */
/* Note: start REXXTRY in another session and do a SysDropFuncs in    */
/*       that session while running this program.                     */
/*       To use the wrapper routine for more than one DLL you can add */
/*       another parameter for the name of the DLL.                   */
/*                                                                    */

                    /* install a handler for CTRL-BREAK               */
  SIGNAL ON HALT NAME ProgramEnd

  do forever
    say
    say "Example for using the REXXUTIL wrapper"
    say
    say "Start another session and drop the REXXUTIL functions to"
    say "see how it works  ..."
    say
    say "Press CTRL-BREAK to halt this program ..."
    say
    say "Now calling some REXXUTIL functions using the wrapper ..."
    say
    say "  Calling SysOs2Ver         -> OS/2 Version is " || ,
             REXXUTIL( "SysOS2Ver" )

    say "  Calling SysTextScreenSize -> TextscreenSize is " || ,
             REXXUTIL( "SysTextScreenSize" )

    say "  Calling SysFileTree       -> result is " || ,
             REXXUTIL( "SysFileTree", "*.CMD", "fileStem" )

                    /* show the results of the SysFileTree function   */
    say "    SysFileTree founds " || fileStem.0 || " files:"
    do i = 1 to fileStem.0
      say "      "  || i || ". entry is " || ,
          Word( fileStem.i, words( fileStem.i ) )
    end /* do i = 1 to fileStem.0 */

    say "Result of calling the invalid function SysFoo is " || ,
           REXXUTIL( "SysFoo" )

    say "Now sleeping for 2 seconds"
    call REXXUTIL "SysSleep", 2

  end /* do forever */

ProgramEnd:
exit

/* ------------------------------------------------------------------ */
/* function: call a function from the DLL REXXUTIL. Reload the        */
/*           REXXUTIL function if it is not loaded or dropped by      */
/*           another process.                                         */
/*                                                                    */
/* call:     REXXUTIL funcName {, funcParameter} {...}                */
/*                                                                    */
/* where:    funcName - name of one of the REXXUTIL functions         */
/*           funcParameter - parameters for the function (if any)     */
/*                                                                    */
/* returns:  SYNTAX ERROR #                                           */
/*             if a syntax error occurred, # is the error number      */
/*             (999 = parameter funcName missing)                     */
/*           else the return code of the REXXUTIL function            */
/*                                                                    */
/*                                                                    */
/* notes:    Do not use PROCEDURE for this routine!                   */
/*                                                                    */
REXXUTIL:
                    /* get the function name                          */
  rx.__funcName = strip( arg(1) )

                    /* check the parameter                            */
  rc = "SYNTAX ERROR 999"
  if rx.__funcName = "" then
    SIGNAL rx.__REXXUtilEnd

                    /* install a local error handler                  */
  SIGNAL ON SYNTAX Name rx.__ReloadREXXUtil

                    /* create the statement to execute                */
  rx.__iLine = "rx.__fRC = " rx.__funcName

  rx.__iLine = rx.__iLine || "("

  do i = 2 to arg()
    if i <> 2 then
      rx.__iLine = rx.__iLine || ", "

    rx.__iLine = rx.__iLine || '"' || arg(i) || '"'
  end /* do i = 2 to arg() */

  rx.__iLine = rx.__iLine || ")"

  rc = 0            /* if rc is not equal zero after the interpret    */
                    /* instruction there was an error                 */

                    /* call the REXXUTIL function                     */
  interpret rx.__iLine

rx.__ReloadREXXUtil:

                   /* check the result                                */
  if rc = 0 then
  do
                   /* execution okay                                  */
    rc = rx.__fRC
    SIGNAL rx.__REXXUtilEnd
  end /* if rc = 0 then */

  if rc <> 43 then
  do
                   /* rc <> 0 and rc <> 43 -> an error occurred       */
                   /* (error number 43 means "function not found"     */
                   /*  and is handled in the code below)              */
    rc = "SYNTAX ERROR " || rc
    SIGNAL rx.__REXXUtilEnd
  end /* if rc <> 43 then */

/* ------- The next statement is for debugging purpose only --------- */
  say "  REXXUTIL: Reloading the function <" || ,
      rx.__funcName || "> ..." || "07"x
/*  ----------------------------------------------------------------- */

                   /* rc = 43 -> either the function is not yet       */
                   /* loaded or dropped by another process            */
                   /* -> reload the function                          */
  call rxFuncAdd rx.__funcName, "REXXUTIL", rx.__funcName

                    /* install another local error handler to avoid   */
                    /* endless loops                                  */
  SIGNAL ON SYNTAX Name rx.__REXXUtilError
  rc = 0

                   /* and try it again                                */
  interpret rx.__iLine

rx.__REXXUTILError:
                   /* check the result                                */
  if rc = 0 then
    rc = rx.__fRC  /* execution okay                                  */
  else
    rc = "SYNTAX ERROR " || rc  /* error                              */

rx.__REXXUtilEnd:
                   /* drop local variables                            */
  drop rx.
RETURN rc

The function RxMessageBox()

The function RxMessageBox is part of the standard OS/2 REXX - it is not necessary to load REXXUTIL to use it. RxMessageBox is only available for REXX programs running in a PM environment (see Start a REXX program in PM mode).

The function STREAM()

In "Classic" REXX a call of the function STREAM in the format

name=stream( fileName, "c", "QUERY EXISTS" )

never activates the NOTREADY condition. In Object-Oriented REXX this call leads to the OS popup message box saying "Drive x: not ready" if the statement AUTOFAIL=NO is missing in the CONFIG.SYS.

  • Tested with OBJREXX 6.00 12 Jul 1996
  • Fixed in OBJREXX 6.00 26 Feb 1997 and newer versions (included in WARP 4 Fixpack #6)

--- You can use the statement

curSize = stream( testFile, "c", "QUERY SIZE" )

to get the current size of open files on HPFS-formatted drives. Note that this is not possible for files on FAT-formatted drives (see also Copying open files). --- See REXX and Y2K for information about Stream and Y2K.

The function TIME()

The function TIME generates a syntax error if called with the parameter E or R if the elapsed time is negative (the stop time is before the start time). Note that this error is fixed in Object-Oriented REXX - but not in Classic REXX from WARP 4.

Example:

/* simple code to show the error in the function time()               */
                    /* install an error handler                       */
  signal on Syntax
                    /* get the current time                           */
  thisTime = time( "N" )
                    /* calculate the time 1 hour _before_ now         */
  testTime = substr( thisTime,1,2 ) - 1 || substr( thisTime,3 )

  say "Current time is:" thisTime
  say "Now reseting the timer ..."
  call time "R"
                    /* now setting the stop time _before_ the start   */
                    /* time                                           */
  say "Now setting the time to" testTime "..."
  "@time" testTime
  say "The time now is:" time()

  say "Now retreiving the elapsed time via time(E) ..."
  say " (--> This will generate a syntax error!)"

  say "Elapsed time is" time("E")

ProgramEnd:
  say "Now resetting the time to the correct value ..."
                    /* get the current time                           */
  thisTime = time( "N" )
                    /* set the correct time                           */
  "@time" substr( thisTime,1,2 ) + 1 || substr( thisTime,3 )
  say "Current time is:" time()
exit

/* error handler                                                      */
Syntax:
  say "*** Syntax error occurred in line:" sigl
  say "    Condition(D) is" condition(D)
  say "    Condition(C) is" condition(C)
  say "    rc is" rc
  say "    The error message for" rc "is:"
  say "   " errortext(rc)
                    /* jump to ProgramEnd to correct the time         */
signal ProgramEnd

The function TRANSLATE()

The function TRANSLATE called without the tab_in and tab_out parameter to translate the input string to uppercase only handles the characters from A to Z. National characters (like German "Umlaute") are not converted. Note also that there is no function to convert a string to lowercase.

(see Uppercase & Lowercase including German "Umlaute" for a possible solution of this problem. Note that the REXXUTIL.DLL from Object-Oriented REXX contains a function for country-dependent uppercase translations)

Hints for using QUEUEs

This section contains hints for using queues.

QUEUE names

The name of a queue is always in uppercase. You can call the function RXQUEUE with a queue name in lowercase or mixedcase - but be aware of the translation to uppercase if you're doing a compare with the result of RXQUEUE.

In OS/2 WARP 3 long queue names (with about 20 characters) are not handled correctly by the REXX interpreter. This bug is fixed in the WARP 3 FixPack (source: APAR PJ19351).

Umlaute können in den Namen für Queues nicht verwendet werden. Der Aufruf von RXQUEUE mit einem Umlaut im Namen für die Queue führt zu einem Syntax-Fehler.

Using queues for IPC

The default queue SESSION is a unique queue for each OS/2 session. So, if you want to use a queue to communicate between two or more sessions you must use a private queue.

Example:

/* use a private queue for IPC                                        */

                        /* name of the private queue                  */
  privatQueue = "MYQUEUE"                                    /* v1.70 */

                        /* get the name of the current active queue   */
  oldQueue = rxQueue( "GET" )

  if oldQueue <> privatQueue then
  do
                        /* try to create the private queue            */
    newQueue = rxQueue( "CREATE", privatQueue )
    if newQueue <> privatQueue then
    do
                        /* the queue already exist -> delete the just */
                        /* created queue!                             */
      call rxQueue "DELETE", newQueue              /* corrected v3.00 */
    end /* if rxQueue( "Create", ... ) */

                        /* make our private queue the active queue    */
    call rxQueue "SET", privatQueue                /* corrected v3.00 */
  end /* if oldQueue <> privatQueue then */
  else
  do
                    /* the private queue is already the active queue  */
                    /* do not reset the active queue at program end   */
    oldQueue = ""
  end /* else */

  /* ... do something */


ProgramEnd:
  if oldQueue <> "" then
  do
                        /* flush the private queue                    */
    do while queued() <> 0
      call lineIN "QUEUE:"
    end /* do until queued() <> 0 */

                        /* reset the original queue                   */
    call rxQueue "SET", oldQueue

                        /* delete the private queue                   */
    call rxQueue "DELETE", privatQueue
  end /* if oldQueue <> "" then */

exit 0

Using a queue

The function RXQUEUE("SET", queuename) does not check, if the queue queuename exist and writing or reading from a non-existent queue raises the condition FAILURE.

Therefor you should test a queue before using it. (see #Check if a queue exists for an example).

Reading from a Queue

The statement PULL reads from STDIN if there's no data in the REXX queue. Use the function LINEIN("QUEUE:") to read from a REXX queue and wait for the data if the queue is empty.

(see also Using a queue) Reading from an invalid queue raises the condition FAILURE Therefor you should test a queue before using it.

Checking a queue

The function LINES always returns 1 if used for a queue. Use the function QUEUED to get the number of lines currently in the queue.

Check if a queue exists

There is no function to check if a queue exists. To do this, you can use either the procedure shown below or the function RxProcQueueExists from the RXPROC.DLL from the SRE2003 Internet Interface package.

/* sample code to extend the RXQUEUE function with code               */
/*                                                                    */
/*   - to check if a queue exists                                     */
/*   - to test if a queue is working                            v3.60 */
/*                                                                    */

                        /* stem with the names of the test queues     */
  testQueue.0 = 2
  testQueue.1 = "QUEUEA"
  testQueue.2 = "QUEUEB"

                        /* install some error handlers to ensure the  */
                        /* deletion of the test queues at program     */
                        /* end if the program is aborted due to an    */
                        /* error!                                     */
  SIGNAL ON SYNTAX  Name ProgramEnd
  SIGNAL ON ERROR   Name ProgramEnd
  SIGNAL ON HALT    Name ProgramEnd

                        /* create the test queues                     */
  say "Creating some queues for testing ..."

  do i = 1 to TestQueue.0
    call CharOut, " Creating the queue """ || testQueue.i || """ ..."
    if rxQueue( "CREATE", testQueue.i ) = testQueue.i then
      call LineOut, " done."
    else
      call LineOut, " failed."
  end /* do i = 1 to TestQueue.0 */
  say ''

  do until myInput = ""
    say "Enter the parameter for rxqueue (RETURN to exit): "
    say "Examples: ""QUERY queuea"" ""TEST queuec"" ""CREATE"""
    myInput = strip( lineIn() )

    if myInput <> "" then
    do
      parse var myInput queuefunction queueName
      if queueName <> "" then
      do
        say " -->> calling rxqueue( " || queueFunction || "," || queueName || " )"
        testResult = rxqueue( queueFunction, queueName )
      end /* if */
      else
      do
        say " -->> calling rxqueue( " || queueFunction || " )"
        testResult = rxqueue( queueFunction )
      end /* else */

      say "The result is """ || testResult || """"
      if testResult <> 0 then
      do
        say "The error message for " || testResult || " is "
        say "    " || GetQueueErrorMessage( testResult )
      end /* if */
    end /* if myInput <> "" then */
  end /* do until myInput = "" */

ProgramEnd:
                        /* delete the test queues                     */
  say "Deleting the queues for testing ..."

  do i = 1 to TestQueue.0
    say " Deleting the queue """ || testQueue.i || """ ..."

                        /* use the _original_ function to avoid       */
                        /* endless loops if the new RXQUEUE function  */
                        /* is buggy!                                  */
    call "RXQUEUE"  "DELETE", testQueue.i
  end /* do i = 1 to TestQueue.0 */

exit 0

/* ------------------------------------------------------------------ */
/* function: Extended RXQUEUE function                                */
/*                                                                    */
/* usage:    RXQUEUE action {,queue_name}                             */
/*                                                                    */
/* where:    action                                                   */
/*             - QUERY - check if the queue "queue_name" exists       */
/*                                                                    */
/*               syntax: RXQUEUE query , queuename                    */
/*                                                                    */
/*             - TEST - check if the queue "queue_name" is usable     */
/*                                                                    */
/*               syntax: RXQUEUE test {, queuename}                   */
/*                       default for queuename is the current queue   */
/*                                                                    */
/*             All other values for action are processed by the       */
/*             original RXQUEUE function.                             */
/*                                                                    */
/* returns:  if action = "QUERY":                                     */
/*             1 - the queue exists                                   */
/*             0 - the queue does not exist                           */
/*            40 - syntax error                                       */
/*             else                                                   */
/*               error code of the original RXQUEUE function          */
/*                                                                    */
/*           if action = "TEST":                                v3.60 */
/*             0 - the queue is working                               */
/*             1 - the queue does not work                            */
/*            40 - syntax error                                       */
/*             else                                                   */
/*               error description (e.g SYNTAX ERROR)                 */
/*                                                                    */
/*           if action <> "QUERY" and <> "TEST":                      */
/*             return code of the original RXQUEUE function           */
/*                                                                    */
/* history                                                            */
/*   RXTT v3.60 - added the function TEST                             */
/*                                                                    */
RXQUEUE: PROCEDURE
  parse arg action, queue_name

                        /* init the return code (40 = incorrect call) */
  rc = 40

  currentQueue = ""

                        /* install local error handler                */
  SIGNAL ON SYNTAX NAME RxQueueError
  SIGNAL ON FAILURE NAME RxQueueError
  SIGNAL ON ERROR NAME RxQueueError

  curAction = translate( action )
  curQueue = translate( strip( queue_name ) )

  select

    when curAction = "QUERY" then
    do

      if curQueue <> "" then
      do

                        /* try to create the queue ...                */
        tempQueue = "RXQUEUE"( "CREATE", curQueue )

                        /* ... and delete the just created queue      */
        call "RXQUEUE" "DELETE", tempQueue

                        /* set the return code                        */
        rc = ( tempQueue <> curQueue )
      end /* if curQueue <> "" then */
    end /* when curAction = "QUERY" then */

    when curAction = "TEST" then
    do
       rc = 1

                        /* save the current queue name                */
      if queue_name <> "" then
        currentQueue = "RXQUEUE"( "SET", queue_name )

                        /* The current queue is restored a the end    */
                        /* of the routine                             */

      queue_teststring = "rxqueue test function"
      queue queue_teststring
      if queued() <> 0 then
      do
        curString = lineIn( 'QUEUE:' )
        if curString <> queue_testString then
          queue curString
        else
          rc = 0
      end /* if */

    end /* when curAction = "TEST" then */

    otherwise
    do
                        /* call the original RXQUEUE function         */
      if queue_name <> "" then
        rc = "RXQUEUE"( action, queue_name )
      else
        rc = "RXQUEUE"( action )
    end /* otherwise */

 end /* select */


RxQueueError:
                        /* restore the current queue if necessary     */
  if currentQueue <> "" then
    call "RXQUEUE" 'set', currentQueue

RETURN rc


/* ------------------------------------------------------------------ */
/* function: Get the error message for an error code                  */
/*                                                                    */
/* usage:    GetQueueErrorMessage( errorNumber )                      */
/*                                                                    */
/* where:    errorNumber - error number                               */
/*                                                                    */
/* returns:  the error message                                        */
/*                                                                    */
GetQueueErrorMessage: PROCEDURE
  parse arg errorCode

  errorMessages. = "Unknown error code"

  errorMessages.0 = "Operation successfull."
  errorMessages.5 = "Not a valid queue name or tried to delete queue named 'SESSION'."
  errorMessages.9 = "Queue named does not exist."
  errorMessages.10 = "Queue is busy; wait is active."
  errorMessages.12 = "A memory failure has occurred."
  errorMessages.40 = "Incorrect call (invalid or missing queue name)."
  errorMessages.48 = "Failure in system service (the queue does not exist)."

  errorMessages.1000 = "Initialization error; check file OS2.INI."

  if errorCode = "" then
    return "Parameter for GetQueueErrorMessage missing"
  else
    return errorMessages.errorCode

Flushing a queue

Some user (including myself in one of the last versions of RXT&T) say that using rxqueue /CLEAR to flush the REXX queue is significantly faster than flushing the queue with LINEIN or PULL if there are a lot of entries in the queue.

Well, I've done some tests and in my tests, the PULL statement was always faster. But you may use the code below to test this behaviour on your PC.

/* program to test the speed of flushing the REXX queue               */

                        /* init the variables                         */
  queueThreshold = 1000
  queueLines = 1000
  queueLineLength = 100

  do forever

    call LineOut , "Enter EXIT on any question to exit the program"

    call CharOut , "  Enter the queue threshold ("""" for " || ,
                   queueThreshold || "): "
    userInput = translate( lineIn() )
    if userInput = "EXIT" then
      exit
    if userInput <> "" then
      queueThreshold = userInput

    call CharOut , "  Enter the no. of lines for the queue ("""" for " || ,
                   queueLines || "): "
    userInput = translate( lineIn() )
    if userInput = "EXIT" then
      exit
    if userInput <> "" then
      queueLines = userInput

    call CharOut , "  Enter the length for the lines in the queue " || ,
                   "("""" for " || queueLineLength || "): "
    userInput = translate( lineIn() )
    if userInput = "EXIT" then
      exit
    if userInput <> "" then
      queueLineLength = userInput

                        /* fill the queue                             */
    call CharOut , "  Now filling the queue with " || queueLines || ,
                   " lines with " || queueLineLength || " characters  ..."

    do i = 1 to queueLines
      push copies( "X", queueLineLength )
    end /* do i = 1 to queueLines */
    call LineOut , " done."
    call LineOut , "  There are now " || queued() || " lines in the queue."

    call CharOut , "  Now flushing the queue ..."
    call time "R"
    call FlushQueue
    call LineOut , " done. Time used: " || time("E")
  end /* do forever */

exit

/* ------------------------------------------------------------------ */
/* function: flush the default REXX queue                             */
/*                                                                    */
/* call:     FlushQueue                                               */
/*                                                                    */
/* returns:  nothing                                                  */
/*                                                                    */
/*                                                                    */
FlushQueue:
  queueCount = queued()
  if queueCount >= queueThreshold then
  do
    call LineOut , "  Using rxqueue ..."
    ADDRESS "CMD" "RXQUEUE /CLEAR"
  end /* if */
  else
  do
    call LineOut , "  Using parse pull ..."
    do i = 1 to queueCount
      parse pull
    end /* do i = 1 to queueCount */
  end /* else */
return

Queues versus files

Because the queue processing of OS/2 REXX is very slow, you should test if using files instead of queues speeds up your program.

The PARSE instruction

This section contains a more detailed description of the PARSE instruction.

General

The format of the PARSE instruction is PARSE {UPPER|LOWER|CASELESS} source {template}

where

UPPER
Translate the source to uppercase before parsing. This parameter is optional.
LOWER
Translate the source to lowercase before parsing. This parameter is optional (Object REXX only!).
CASELESS
Parse the source ignoring the case (e.g. A-Z is equal to a-z). This parameter is optional (Object REXX only!).
source
The source to parse (see below)
template
This parameter specifies how to parse the source (see below). This parameter is optional.

source may be one of the following:

ARG
Use the arguments for the program or procedure as source
LINEIN
Use the next input line from the keyboard as source. PARSE LINEIN is a short form of PARSE VALUE LINEIN() WITH
PULL
Use the next line from the default REXX queue or the next line from the keyboard if the queue is empty as source
SOURCE
Use the program's source information as source (see PARSE SOURCE)
VALUE expression WITH
Use the result of the expression expression as source. expression may be any legal REXX expression.
VAR name
Use the contents of the variable name as source
VERSION
Use the REXX interpreter version information as source (see PARSE VERSION)

template can be any combination of the following patterns:

variable name
a variable to be assigned a value
literal string
a literal string used as string pattern to split the source
(variable name)
a literal string saved in a variable used as a string pattern to split the source
. (a single period)
a placeholder for unused parts of the source. You must use at least one space to separate periods from other patterns.
(# is an integer value, e.g. 4)
an absolute character position within the source
=# (# is an integer value, e.g. =4)
an absolute character position within the source
+# (# is an integer value, e.g. +4)
a relative character position within the source (move right)
-# (# is an integer value, e.g. -4)
a relative character position within the source (move left)
=(variable name)
a variable containing an absolute character position within the source
+(variable name)
a variable containing a relative character position within the source (move right)
-(variable name)
a variable containing a relative character position within the source (move left)

General notes

Templates are processed from left to right. The PARSE instruction does not change the source string (i.e., if you don't force it to do this; see below).


Hints for selecting the proper template

Use only variables if you want to split the string into words. Example:

/* parse into words                                                   */

 testString = "This    is       a       Test "

 parse var TestString var1 var2 var3 var4

       /* result:                                                    */
       /* -> var1 = "This", var2 = "is", var3 = "a" and              */
       /*    var4 = "       Test "                                   */

In this example PARSE splits the source string into words (separated with one or more blanks) and copies these words into the variables used in the template. The blanks between the words are not saved in a variable -- with one exception: The blanks around the word(s) for the last variable are saved in the last variable. To avoid this, you may add a period at the end of the template.

Example:

 /* parse into words                                                 */
 /* use a period at the end of the template to suppress the spaces   */
 /* surrounding the string in the last variable                      */

 testString = "This    is       a       Test "

 parse var TestString var1 var2 var3 var4 .

       /* result:                                                    */
       /* -> var1 = "This", var2 = "is", var3 = "a" and              */
       /*    var4 = "Test"                                           */

Note that if there's only one variable in the template, that variable is also the last variable!

Further note that PARSE only uses spaces as the word delimiter. If, for example, the source string uses tabulators ("09"x) to separate the words, you can either use parsing with string patterns (see below) or you can use the function translate before parsing the string.

Example:

 /* translate tabulator to spaces and parse into words               */

 testString = "This    is" || "09"x || "another    Test"

                   /* translate tabulators to spaces                 */
 TestString = translate( testString, " ", "09"x )
 parse var TestString var1 var2 var3 var4 .

       /* result:                                                    */
       /* -> var1 = "This", var2 = "is", var3 = "another" and        */
       /*    var4 = "Test"                                           */

Use string patterns if you want to split the string after one or more special substrings. Example:

/* parse with string patterns                                         */

  testString = "datafields=data1//data2//data3"

  parse var testString keyName "=" value1 "//" value2 "//" value3

       /* result:                                                    */
       /* -> keyName = "datafields", value1 = "data1",               */
       /*    value2 = "data2" and value3 = "data3"                   */

In this example PARSE splits the string into substrings before and after the string patterns. The string patterns are not saved in a variable.


Use positional parsing if the source is built out of fields with fixed length.

Example:

 /* absolute positional parsing                                        */
 
 /*              0        1         2         3         4         5    */
 /*    position: 12345678901234567890123456789012345678901234567890123 */
   testString = "Doe       John M.   03/03/65  New York            USA";
 
   parse var testString name1 11 name2 21 birthday 31 town 51 country
 
         /* result:                                                    */
         /*                0        1         2         3              */
         /*                123456789012345678901234567890              */
         /* -> name1    = "Doe       "                                 */
         /*    name2    = "John M.   "                                 */
         /*    birthday = "03/03/65  "                                 */
         /*    town     = "New York            "                       */
         /*    country  = "USA"                                        */

In this example PARSE splits the source string at the given positions into substrings and copies these substrings into the variables. All characters from the source string (including the spaces) are saved in a variable.

You can also use relative positional parsing in this case.

Example:

 /* relative positional parsing                                        */ 
 
 /*              0        1         2         3         4         5    */
 /*    position: 12345678901234567890123456789012345678901234567890123 */
   testString = "Doe       John M.   03/03/65  New York            USA";
 
   parse var testString name1 +10 name2 +10 birthday +10 town +20 country
 
         /* result:                                                    */
         /*                0        1         2         3              */
         /*                123456789012345678901234567890              */
         /* -> name1    = "Doe       "                                 */
         /*    name2    = "John M.   "                                 */
         /*    birthday = "03/03/65  "                                 */
         /*    town     = "New York            "                       */
         /*    country  = "USA"                                        */

Positional patterns that specify a position beyond the end of the parse data are considered to match the end of the data. Positional patterns that specify a position to the left of the beginning of the parse data are considered to match the beginning of the parse data.


You can also mix string patterns, positional parsing and variables in one PARSE instruction. Example:

 /* mixed parsing                                                      */ 
 
 /*              0        1         2         3         4         5    */
 /*    position: 1234567890123456789012345678901234567890123456789012  */
   testString = "This a remark field (up to 39 chars)   44 55 //66    77";
 
   parse var testString 40 data1 data2 . "//" data3 data4 .
 
        /* result:                                                    */
        /* -> data1 = "44", data2 = "55", data3 = "66" and            */
        /*    data4 = "77"                                            */

Further considerations

Use the placeholder . (period) to ignore parts of the source. Example:

 /* parsing into word using periods in templates                       */
 
   testString = "data1 garbage data2 garbage garbage data3 garbage";
 
   parse var testString resultStr1 . resultStr2 . . resultStr3 .
 
        /* result:                                                    */
        /* -> resultStr1 = "data1", resultStr2 = "data2" and          */
        /*    resultStr3 = "data3"                                    */

Note that the placeholder is not necessary in all cases. Example:

 /* examples for using the placeholder                                 */
 
   testString = "data1-data2-data3"
   parse var testString . '-' resultStr '-' .
         /* -> resultStr = "data2"                                     */
 
   /* is equal to */
   parse var testString '-' resultStr '-'
         /* -> resultStr = "data2"                                     */
 
   parse var testString . 7 resultStr +5 .
         /* -> resultStr = "data2"                                     */
 
   /* is equal to */
   parse var testString 7 resultStr +5
         /* -> resultStr = "data2"                                     */

All variables in a template receive new values. If there are no more words for one or more variables in the source, the remaining variables receive an empty string. Example:

/* further parsing example                                            */

  parse value "word1 word2" WITH resultStr1 resultStr2 resultStr3

        /* result:                                                    */
        /* -> resultStr1 = "word1", resultStr2 = "word2" and          */
        /*    resultStr3 = ""                                         */

In this example the variable resultStr3 contains an empty string after the PARSE instruction because there are two words in the source but the template contains three variables. Another example

/* further parsing example                                            */

  parse value "s1 // s2 s3" WITH resStr1 "//" resStr2 "--" resStr3

        /* result:                                                    */
        /* -> resStr1 = "s1 ", resStr2 = " s2 s3", resStr3 = ""       */

In this example the variable resStr3 contains an empty string after the PARSE instruction because there is no match for the string pattern "--" in the source string.


If there are more words in the source than variables in the template, the last variable receives all words remaining in the source. Example:

 
/* further parsing example                                            */

  parse value "word1 word2 word3" WITH resultStr1 resultStr2

        /* result:                                                    */
        /* -> resultStr1 = "word1", resultStr2 = "word2 word3"        */

In this example the variable resultStr2 contains "word2 word3" because there are three words in the source but only two variables in the template.


An empty string (e.g. "") in the template is never found. It always matches the end of the string (see Get the first and the last char of a string for an example). Example:

/* parse a string from the end                                        */

  testString = "anything not needed      44 55 66 77"

  parse var testString "" -2 var1 +2 -5 var2 +2 -5 var3 +2 -5 var4 +2

        /* result:                                                    */
        /* -> var1 = "77", var2 = "66", var3 = "55" and var4 = "44"   */

In PARSE VAR instructions you can use the source in the template. You can use this method to process a variable word by word. Example:

/* process a string word by word                                      */

  testString = "Otto Karl Heinrich Klaus Peter"

  do until testString = ""

                    /* copy the 1. word of testString into curName    */
                    /* remove it from testString                      */
    parse var testString curName testString

    say "Current word is " || curName
    say "Remaining words in testString are " || testString
  end /* do until testString = "" */

--- You can set a variable in a template and use it in the remaining template. Example:

/* further parsing example                                            */

  testString = "*data1*data2*data3*"

  parse var TestString sep 2 resStr1 (sep) resStr2 (sep) resStr3 (sep)
        /*             \ /             |             |             |  */
        /*              |              \-------------+-------------/  */
        /*             set the            use the variable "sep"      */
        /*             variable "sep"                                 */
        /*                                                            */
        /* result:                                                    */
        /* -> resStr1 = "data1", resStr2 = "data2", resStr3 = "data3" */
        /*    sep = "*"                                               */

--- Only PARSE ARG can have more than one source string (separate the source strings with commas). Example:

/* further parsing example                                            */

  call TestFunc "data11,data12" , "data21data22"

RETURN

TestFunc:
    /* Note the difference meaning of the commas in the template!     */

  parse arg arg1_part1 ',' arg1_part2 , arg2_part1 7 arg2_part2

        /* result:                                                    */
        /* -> arg1_part1 = "data11", arg1_part2 = "data12"            */
        /*    arg2_part1 = "data21", arg2_part2 = "data22"            */
RETURN

Note that a REXX program called from the command line always retrieves only one parameter (see also Parameters for a REXX program).

--- Using positional parsing you can parse the source string with different templates in one PARSE instruction. Example:

/* sample for parsing a string with different templates in one PARSE  */
/* instruction                                                        */

  testString = "a--b c++d r//g"

  parse value testString with   word1 word2 word3 ,
                          1 part1 "--" part2 "++" part3 "//" part4 ,
                          1 char1 2 4 char2 5 9 char3 10 char4 11 14 char5

        /* result:                                                    */
        /* -> word1 = "a--b", word2 = "c++d", word3 = "r//g"          */
        /*    part1 = "a", part2 = "b c", part3 = "d r"               */
        /*    char1 = "a", char2 = "b", char3 = "d", char4 = "r",     */
        /*    char5 = "g"                                             */

PARSE instruction examples

Now some further examples for using the PARSE instruction. By the way: The best method to learn about PARSE is using REXXTRY (better yet, EREXXTRY) to test it. Double-Click on to call REXXTRY right now.

Further examples of using PARSE:

/* ----- PARSE the contents of a variable --------------------------- */

                            /* use PARSE VAR                          */
  testVariable = "1part 2part 3part"

  parse var testVariable resultStr1 resultStr2 resultStr3

        /* result:                                                    */
        /* -> resultStr1 = "1part", resultStr2 = "2part" and          */
        /*    resultStr3 = "3part"                                    */

                            /* use PARSE VALUE                        */
  testVariable = "1part 2part 3part"

  parse value testVariable WITH resultStr1 resultStr2 resultStr3

        /* result:                                                    */
        /* -> resultStr1 = "1part", resultStr2 = "2part" and          */
        /*    resultStr3 = "3part"                                    */

/* ----- PARSE the result of a function ----------------------------- */

                            /* use PARSE VALUE                        */
  parse value SysTextScreenSize() WITH cols rows

                            /* use PARSE VAR                          */
  textScreenSize = SysTextScreenSize()

  parse var textScreenSize WITH cols rows

/* ------ PARSE the date into day, month and year ------------------- */
/*   All examples assume a date in the American format (mm/dd/yy)!    */

                            /* parse with string pattern              */
  parse value date( "U" ) WITH month "/" day "/" year

        /* result:                                                    */
        /* -> day = "08", month = "01" and year = "96" if the current */
        /*    date is 08 Jan 1996                                     */

                            /* parse with string pattern, ignore the  */
                            /* month and year                         */
  parse value date( "U" ) WITH  . "/" day "/" .
                            /* or */
  parse value date( "U" ) WITH  "/" day "/"

                            /* parse with variable string pattern     */
  separator = "/"
  parse value date( "U" ) WITH month (separator) day (separator) year

                            /* parse with variable string pattern     */
                            /* The separator character is part of the */
                            /* source. It is saved in the variable    */
                            /* "separator" and used with              */
                            /* "(separator)".                         */
  parse value date( "U" ) WITH month 3 separator 4 day (separator) year

                            /* absolute positional parsing            */
  parse value date( "U" ) WITH month 3 4 day 6 7 year

                            /* absolute positional parsing with       */
                            /* changed parse order                    */
  parse value date( "U" ) WITH 7 year 1 month 3 4 day 6 .

                            /* absolute positional parsing, ignore    */
                            /* the month and year                     */
  parse value date( "U" ) WITH  4 day 6

                            /* relative positional parsing            */
  parse value date( "U" ) WITH month +2 +1 day +2 +1 year

                            /* relative positional parsing            */
                            /* with changed parse order               */
  parse value date( "U" ) WITH 4 day +2 +1 year +2 -8 month +2

                            /* relative positional parsing            */
                            /* ignore the month and year              */
  parse value date( "U" ) WITH  +3 day +2

/* ------ PARSE the date into day, month and year (all formats) ----- */
    /* dateVar may contain something like "01.04.95" (European) or    */
    /* "04/01/95" (US)                                                */

  if dateFormat = "US" then
  do
                    /* US date format is mm/dd/yy                     */
    monthPos = 1;  dayPos = 4;    yearPos = 7
  end /* if */
  else
  do
                    /* European date format is dd/mm/yy               */
    monthPos = 4;  dayPos = 1;    yearPos = 7
  end /* else */

  parse var dateVar =(dayPos) day +2 ,
                    =(monthPos) month +2 ,
                    =(yearPos) year +2

See the section Hints for PARSEing for further hints for the PARSE instruction.

Note
For a complete description of the PARSE instruction see IBM: OS/2 2.0 Procedures Language/2 Reference Manual.

PARSE SOURCE

PARSE SOURCE returns 3 tokens with the following information:

1.- token: Operating system

This can be OS/2, TSO, CMS or any other operating system with a REXX interpreter. (see Writing OS independent programs for an example for using this value)

2.- token: call type

This is COMMAND if the REXX program was called from the command line, or SUBROUTINE if the REXX program was called from another REXX program. (see Parameter for a REXX program for an example for using this value)

3.- token: fully qualified name of the source file

Note for OS/2 REXX: If this token only contains the name of the REXX program without the path, the REXX program is in the macro space.

Example REXX program:

/* sample for using PARSE SOURCE */

parse source operatingSystem commandType sourceFileName

say "The Operating system is <" || operatingSystem || ">."
say "The call type is <" || commandType || ">."
say "The name of the source file is <" || sourceFileName || ">."

(see also Get the invocation syntax)

PARSE VERSION

PARSE VERSION returns 5 tokens describing the current REXX interpreter:

1.- token: REXX interpreter name

This is REXXSAA for the default OS/2 REXX interpreter or OBJREXX for Object-Oriented REXX. Another possible value is REXX/Personal (used by the REXX interpreter from Quercus Systems, (see Internet - Web Pages)).

2. token: REXX interpreter level

The format is majorVersion.minorVersion, e.g. the default OS/2 REXX interpreter returns 4.00. The new Object-Oriented REXX returns 5.00 or 6.00 (depending on the version).

3., 4. and 5. token: REXX interpreter date

These tokens are the date of the REXX interpreter (day, month and year). Day is a 2-digit numeric value (e.g. "08"), month is a 3-character abbreviation of the month name (e.g. "Feb") and year is a 4-digit numeric value (e.g. "1994").

Example REXX program:

/* sample for using PARSE VERSION */

parse version interpreterName interpreterLevel interpreterDate

say "The name of the interpreter is <" || interpreterName || ">."
say "The level of the interpreter is <" || interpreterLevel || ">."
say "The date of the interpreter is <" || interpreterDate || ">."

Hints for PARSEing

This section contains some further hints for using the keyword instruction PARSE.

Initialization of variables

You can use the PARSE instruction to init more than one variable in one statement. Example:

 
/* init more than one variable in one statement                       */

                    /* init the variables varA, varB, varC and VarD   */
                    /* with 0                                         */
  parse value 0 0 0 0  with varA varB varC varD

                    /* init the variables varE and varG with 1 and    */
                    /* the variables varF and VarH with 0             */
  parse value 1 0 1 0  with varE varF varG varH


                    /* init all global variables with ""        v2.30 */
  exposeList = "Global. Menu. ansi. ascii. help. msgStr."    ,
               "!curMenu !ListMenu !HistoryMenu !MacroMenu"  ,
               "!MainMenu !curMenuEntry !curMenuAction"      ,
               "!thisMenu !NextMenu !BackLevel"

                    /* use interpret to init the variables      v2.30 */
  interpret "parse value '' with " exposeList
Multiple assignments

You can use the PARSE instruction to assign one value to two or more variables in one statement.

Example:

/* assign a value to more than one variable in one statement          */

  parse value directory() with 1 dirString1 1 dirString2
                        /* now dirString1 and dirString2 both contain */
                        /* the name of the current directory          */

  parse var sourceVar 1 targetVar1 1 targetVar2 1 targetVar3
                        /* now the variables targetVar1, targetVar2   */
                        /* and targetVar3 all contain the value of    */
                        /* sourceVar                                  */

(see also Get the current screen size for a working example)

Using parse to split a string

See Returning More than One value from a function for an example of using the PARSE instruction to split a string into substrings using a separator which is part of the string.

Swapping two variables without using a temp. variable

Use the PARSE instruction to swap the values of two variables without using a temporary variable:

/* sample code to swap the values of two variables using PARSE        */

  var1=44
  var2=55
  parse value var1 var2 with var2 var1;

/* use a separator character if the values contain blanks             */
  var1=44 66
  var2=55 77
  parse value var1 || "00"x || var2 with var2 "00"x var1;
Get the first and the last char of a string

You can use PARSE also to split a string into first char, last char and all chars between the first and the last char.

Example:

/* split a string into first char, last char, and the chars between   */
/* (assuming the last char is unique in the whole string)             */

  nextLine = "[TestSection]"
  parse var nextline firstChar +1 . "" -1 lastChar,
                     1 (firstChar) SectionName (lastChar)

  say "The first char is <" || firstChar || ">"
  say "The last char is <" || lastChar || ">"
  say "The chars beteween are <" || sectionName || ">"
How to use parse arg

(see also Parameters for a REXX program)

To many new REXX users it's not clear when to use parse arg1, arg2, arg3 and when to use parse arg1 arg2 arg3

The simplest answer is:

parse arg1 arg2 arg3 is only used for parsing the parameter passed to a REXX program.

In every other case (internal and external REXX routines routines from DLLs, routines from within the macro space) you should use parse arg1, arg2, arg3

/* sample program to show the difference between parse arg            */
/* with and without commas                                            */

                    /* get our name                                   */
  parse source . . thisProgram

  if arg(1) = "" then
  do
    say "Now calling this program as an external program"
    say "(This is the same as if you call it in an OS/2 window)"

    cmdLine = thisProgram 'test1, test2, test3, test4,test5'
    say "==>> " cmdline
    'cmd /c ' cmdLIne
    say ""
    say "Now calling this program as an external procedure"
    rexxlIne =  'call ' fileSpec( 'N', thisProgram ) "test1, test2, test3, test4,test5"
    say "==>> " rexxline
    interpret rexxline

    exit
  end /* if */

  say '  The result of "parse arg1, arg2, arg3, arg4, arg5" is'
  parse arg arg1, arg2, arg3, arg4, arg5
  say '    arg1 is "' || arg1 || '"'
  say '    arg2 is "' || arg2 || '"'
  say '    arg3 is "' || arg3 || '"'
  say '    arg4 is "' || arg4 || '"'
  say '    arg5 is "' || arg5 || '"'

  say '  The result of "parse arg1 arg2 arg3 arg4 arg5" is'
  parse arg arg1 arg2 arg3 arg4 arg5
  say '    arg1 is "' || arg1 || '"'
  say '    arg2 is "' || arg2 || '"'
  say '    arg3 is "' || arg3 || '"'
  say '    arg4 is "' || arg4 || '"'
  say '    arg5 is "' || arg5 || '"'
exit 0

Optimizing REXX programs for speed

This section contains a few performance hints for REXX programs.

Creating a token image

The OS/2 REXX interpreter stores a tokenized copy of each executed REXX program in the EAs of the program. Before executing a REXX program, the interpreter first checks if the tokenized copy in the EAs is still valid and, if so, executes the tokenized copy and ignores the source code. In this case no new tokenizing is necessary.

Because the length of the EAs is limited to 64 KB, it is useful to avoid token images greater than 64 KB for often-used REXX programs. To get the length of the token image of your REXX program use the following OS/2 commands:

 REM --- delete all existing EAs
 eautil REXXProgram nul /s
 REM --- create the token image
 REXXProgram //t
 REM --- write the length of the EAs
 REM     (=token image) to STDOUT
 dir REXXProgram /n

The length of the token image is the second size shown by the dir command. If this value is 0, either the token image of your REXX program is greater than 64 KB or the program has not token image (practically the only time the latter would be true would be if the REXX program has never been run before). In this case you should split your REXX program into two or more smaller programs.

If you're using Object-Oriented REXX ignore the above. Instead, use the program REXXC from Object REXX to create a token image of your REXX programs. REXXC saves the token image in a file and not in the EAs. Therefore there is no 64 K limit using this method. (see also "Compiling" REXX programs)

Note
The CMD replacement 4OS2/32 (v2.0 ... v2.5b) shows an invalid size for the EAs of a file if they are using more than 32,767 bytes.

Using the REXX Macro Space

You should load often-used REXX programs into the REXX Macro Space. Functions to do this are, for example, in the DLL RXU or in the new REXXUTIL DLL. (for an example usage see LoadMac.cmd, see also The functions to work on the macro space and REXXCC - a REXX "compiler")

Advantages of the macro space are:

  • routines in the macro space are useable by all REXX programs running on the machine (like routines loaded from a DLL)
  • routines in the macro space are already tokenized, thus the REXX interpreter can execute them more quickly than external REXX routines
  • You don't need to waste an entry in the PATH variable to implement global routines
  • The REXX interpreter can find routines in the macro space quicker than other external routines because it's not necessary to search through the directories listed in the PATH variable.

Be aware that the routines in the macro space have their own variable scope (like other external REXX routines). You cannot use variables from within the calling program in routines that reside in the macro space.

Investigate the overheads of function calls

Investigate the overheads of function calls

Author: Arthur Pool (see EMail Addresses)

(Statements in green color [like this] are comments from Bernd Schemmer)

(Use the included REXX program to run the test described in this text on your PC)

In the OS/2 operating system, there are several different ways a REXX program can call a function coded in REXX, each of which has implications for both performance and maintainability.

These options are discussed (briefly) in the WARP REXX on-line information (at least, in WARP Connect V3), which says (inter alia): "... internal labels take first precedence, then built-in functions, and finally external functions. External functions and subroutines have a system-defined search order. REXX searches for external functions in the following order:

  • Functions that have been loaded into the macrospace for pre-order execution
  • Functions that are part of a function package
  • REXX functions in the current directory, with the current extension
  • REXX functions along environment PATH, with the current extension
  • REXX functions in the current directory, with the default extension
  • REXX functions along environment PATH, with the default extension
  • Functions that have been loaded into the macrospace for post-order execution."

In practice then, the options, and their implications, are:

[1] Include the source code for the function in the primary source file. This should provide the best performance. However, if the function is to be called by more than one primary REXX program, this approach is undesirable in that it requires duplication of code, with consequent maintenance problems.

[2] Load the macro function into a REXX MacroSpace. This can be achieved using the RexxAddMacro call from C, or using the RxAddMacro function provided in the RXU utility package (I understand that WARP 4 (Merlin) provides a similar function in the REXXUTIL package but I have no experience with that; see New REXXUTIL functions in Object REXX, The functions to work on the macro space, and LoadMac.cmd for information about the REXXUTIL DLL included in Object REXX). This approach is more or less equivalent to the EXECLOAD facility in VM/CMS. This approach should provide performance somewhere between the preceding and following approaches. It does however have some management costs:

  • you have to explicitly load the function into the macrospace - if you don't, you'll simply execute the copy from disk, which will be much slower;
  • you have to also ensure that the macrospace copy is unloaded and reloaded whenever the function's source file is modified - if you don't you'll be executing an out-of-date copy.

As noted above, functions can be loaded in either of two ways:

  • Loaded into the MacroSpace for pre-order execution (executed before disk-based files) - this should produce better performance than using disk-based functions.
  • Loaded into the MacroSpace for post-order execution (executed after disk-based files) - this should produce worse performance than using disk-based functions (below).

However, it is worthwhile to consider the meaning of current extension and default extension. When a function is loaded into the MacroSpace, it can be loaded with an extension (eg, .CMD) or without an extension. In these tests, as the primary REXX file had an extension of .CMD, it appears that the current extension is .CMD. We therefore measured the performance of functions loaded into the MacroSpace (and called) with an extension of .CMD and also without an extension.

Note however that in general one would prefer to load functons without extension so that they are equally accessible to REXX programs with any extension - whether called from REXX command files (.CMD), THE macros (.THE), or from other environments. It's also cumbersome to have to specify the extension when invoking the macro.

We measured 4 sub-cases:

  • 2a] MacroSpace function, pre-order, .CMD extension;
  • 2b] MacroSpace function, pre-order, no extension;
  • 2c] MacroSpace function, post-order, .CMD extension;
  • 2d] MacroSpace function, post-order, no extension;

Leave the source code as a separate file, which is invoked anew for each call to the function. Although ideal in terms of maintenance, this approach will not produce good performance. In practice, disk caching will reduce the impact of all function calls after the first. Also, the performance will be affected by whether the source file is in the current directory, or how far the system has to search along the PATH string before it finds the source file. We therefore test these sub-categories:

  • 3a] source file in the current directory;
  • 3b] source file in a directory at the start of the PATH string.
  • 3c] source file in a directory at the end of the PATH string.
  • 3d] source file in a directory at the end of the PATH string, without EAs. (See below for the rationale for test [3d].)

Following are some measurements of elapsed time (in seconds) for 255 function calls using these various approaches (use the included test program to run this tests on your PC).

[C:\Usr\AFP\SW\Testing]REXX_Function_Call_Performance 255

[1]                             function in the source program:      0.88
[2a]            MacroSpace function, pre-order, .CMD extension:      2.06
[2b]              MacroSpace function, pre-order, no extension:      2.13
[2c]           MacroSpace function, post-order, .CMD extension:     91.09
[2d]             MacroSpace function, post-order, no extension:    180.87
[3a]   function in an external source file - CURRENT directory:     10.28
[3b]       function in an external source file - START of PATH:     12.66
[3c]         function in an external source file - END of PATH:     55.25
[3d] function in an external source file - END of PATH, no EAs:     42.12

[C:\Usr\AFP\SW\Testing]

Notes:

1) The function used was:

     REXX_Function_Call_Performance_1:
       return arg(1)**arg(1)

2) These measurements were on a 80486-DX4 with OS/2 Warp Connect (Blue Box) with no service applied, using (obviously) HPFS.

(Results on a P133 with 32 MB RAM and OS/2 WARP 4 with Fixpack #7 and Object REXX with HPFS:

D:\...\DEVELOP\REXX\FWTOOLS\REXXTT\Test>REXX_Function_Call_Performance 255

[1]                             function in the source program:      0.15
[2a]            MacroSpace function, pre-order, .CMD extension:      0.48
[2b]              MacroSpace function, pre-order, no extension:      0.49
[2c]           MacroSpace function, post-order, .CMD extension:      4.70
[2d]             MacroSpace function, post-order, no extension:     11.77
[3a]   function in an external source file - CURRENT directory:      2.12
[3b]       function in an external source file - START of PATH:      2.53
[3c]         function in an external source file - END of PATH:      5.82
[3d] function in an external source file - END of PATH, no EAs:      7.75

)

(Results on a P266 with 160 MB RAM and OS/2 WARP 4 with Fixpack #12 and Object REXX with HPFS:

D:\...\DEVELOP\REXX\FWTOOLS\REXXTT\Test>REXX_Function_Call_Performance.CMD 255

[1]                             function in the source program:      0.04
[2a]            MacroSpace function, pre-order, .CMD extension:      0.17
[2b]              MacroSpace function, pre-order, no extension:      0.17
[2c]           MacroSpace function, post-order, .CMD extension:      2.46
[2d]             MacroSpace function, post-order, no extension:      6.61
[3a]   function in an external source file - CURRENT directory:      0.81
[3b]       function in an external source file - START of PATH:      1.08
[3c]         function in an external source file - END of PATH:      2.98
[3d] function in an external source file - END of PATH, no EAs:      3.46

)

Conclusions
  • As expected, including the function in the primary source file was fastest.
  • Loading the function in the MacroSpace for pre-order execution is slower than including the function in the primary program, but much faster than invoking from a disk file.
  • Loading the function in the MacroSpace with an extension of .CMD is faster than loading without any extension, most notably when loaded for post-order execution, presumably because the first search is for macros with a .CMD extension. Note however that to achieve this performance, the extension must be specified both when the file is loaded and when the function is invoked - somewhat cumbersome, and probably of marginal benefit in the case of pre-order execution except in the most extreme cases.
  • When loading automatically from a disk file, the position of the function's source file in the PATH string can have a significant effect on performance - and the presence of network drives in the PATH string is likely to exacerbate the effect.
  • Loading in the MacroSpace for post-order execution is very slow. This slow performance can be partly explained because the system searches every directory in the search path (twice if the function is loaded without any extension), fails to find the function as an separate external file, and then finally looks in the MacroSpace (where it finds the function).
  • Even allowing for the preceding item, note that the performance of functions loaded in the MacroSpace for post-order execution is much slower in all cases than those loaded from disk (even when the source file's directory is at the end of the PATH string). Why is this?

I thought that perhaps it was because the version loaded into the MacroSpace does not include the semi-compiled version which REXX normally stores in the Extended Attributes (EAs), but test [3d] shows that this alone does not explain the poor performance of tests [2c] and [2d] (The macro space contains only the tokenized code.)

  • Test [3d] (see above) strips the EAs from the function's source file and makes it read-only (which prevents REXX from attaching the semi-compiled form to the source file) to compare the performance of an external source file without the benefit of the semi-compiled form. Surprisingly, this version is faster than test [3c]! Unlikely though it seems, the semi-compiled form appears to be of no benefit in this test! Perhaps because the external function is quite small and relatively simple, the additional overhead of accessing and loading the EAs outweighs the benefit of the semi-compiled form? (I think the answer to this question is Yes. In addition, this is also dependent from the processor and harddisk used; see results for the P133 above.) In any case, the poor performance of tests [2c] and [2d] remains a puzzle.
  • Loading in the MacroSpace for pre-order execution therefore appears to be generally a good compromise - fairly good performance, easy to maintain, but has some management overheads.
History
  • Sent to THElist 1998-03-16
  • Updated copy sent to THElist 1998-08-03

Test program to test the overheads of function calls

/* ------------------------------------------------------------------ */
/*                                                                    */
/* REXX_Function_Call_Performance - measure the costs of functions    */
/*                                  calls in OS/2 REXX                */
/*                                                                    */
/* Author: Arthur Pool                                                */
/*         (see EMail Addresses)                                      */
/*                                                                    */
/*                                                                    */
/* Syntax: REXX_Function_Call_Performance                             */
/*                                                                    */
/* `REXX_Function_Call_Performance' measures the `cost' (time) of     */
/* calling a REXX function when the function is:                      */
/*                                                                    */
/*  [1] included in the primary REXX source file (this file);         */
/*                                                                    */
/*  [2a] MacroSpace function, pre-order, .CMD extension;              */
/*                                                                    */
/*  [2b] MacroSpace function, pre-order, no extension;                */
/*                                                                    */
/*  [2c] MacroSpace function, post-order, .CMD extension;             */
/*                                                                    */
/*  [2d] MacroSpace function, post-order, no extension;               */
/*                                                                    */
/*  [3a] a separate source file located in the CURRENT directory;     */
/*                                                                    */
/*  [3b] a separate source file located in a directory at the START   */
/*       of the PATH string;                                          */
/*                                                                    */
/*  [3c] a separate source file located in a directory at the END     */
/*       of the PATH string;                                          */
/*                                                                    */
/*  [3d] a separate source file located in a directory at the END     */
/*       of the PATH string, without EAs.                             */
/*                                                                    */
/* `REXX_Function_Call_Performance' is specific to OS/2, though the   */
/* concepts are applicable to other environments.                     */
/*                                                                    */
/* `REXX_Function_Call_Performance' has been tested on Warp Connect   */
/* (Blue Box), and requires the REXXUTIL.DLL and RXU.DLL function     */
/* packages. RXU can be obtained from the common OS/2 FTP sites       */
/* (Hobbes, CDROM, Leo) - search for `RXU' and get the most recent    */
/* version.                                                           */
/*                                                                    */
/* Note:                                                              */
/* This is the test program for the section                           */
/* Investigate the overheads of function calls                        */
/*                                                                    */
/* Last Update                                                        */
/*   01/08/12 /bs Added some error checking for missing DLLs          */
/*   01/08/22 /bs Added more error checking                           */
/* ------------------------------------------------------------------ */
                    /* check the parameter - use default if omitted  */
  parse arg limit;
  select

   when limit = '' then
      limit = 255

    when datatype( limit ) = 'NUM' then
      nop

    otherwise
      limit = 255
      trace a

  end /* select */
                    /* Initialisation - restore original environment  */
                    /* on exit                                        */
  call setlocal

                    /* Ensure we have no undefined variables          */
                    /* (eg, typos)                                    */
  signal on novalue name CrashIt

  tempCMDName = 'REXX_Function_Call_Performance_2.cmd'

                    /* create REXX_Function_Call_Performance_2.cmd in */
                    /* the directory with this file if it don't exist */
  if CreateExternalRoutine( tempCMDName) <> 0 then           /* v3.30 */
  do
    say 'Error: Error creating the temporary file ' || tempCMDName || '!'
    say ''
    say 'Hint:  This program needs the current drive to be formatted with'
    say '       HPFS (or another filesystem supporting long filenames).'
    say '       The current directory must be writeable.'
    say ''
    say 'Program aborted.'
    exit 2
  end /* if */
                    /* Ensure required function packages are          */
                    /* available                                      */
  call LoadUtil 'RexxUtil'                                   /* v3.30 */

  call LoadUtil 'RXU'                                        /* v3.30 */

                    /* Initialise descriptive text                    */
  desc.1  = '[1]                             function in the source program:'
  desc.2a = '[2a]            MacroSpace function, pre-order, .CMD extension:'
  desc.2b = '[2b]              MacroSpace function, pre-order, no extension:'
  desc.2c = '[2c]           MacroSpace function, post-order, .CMD extension:'
  desc.2d = '[2d]             MacroSpace function, post-order, no extension:'
  desc.3a = '[3a]   function in an external source file - CURRENT directory:'
  desc.3b = '[3b]       function in an external source file - START of PATH:'
  desc.3c = '[3c]         function in an external source file - END of PATH:'
  desc.3d = '[3d] function in an external source file - END of PATH, no EAs:'


/* ------------------------------------------------------------------ */
  say ''
  call charout , ' 'desc.1
  t = time('R')

  do i = 0 to limit
    res = REXX_Function_Call_Performance_1(i)
  end /* do i = 0 to limit */

  say format(time('E'),7,2)

/* ------------------------------------------------------------------ */
                    /* Find if the function is already loaded - with  */
                    /* either possible name                           */
  parse value RxQueryMacro('REXX_Function_Call_Performance_3') with rc position
  if rc = 0 then
  do
                    /* Drop the function if it is already loaded      */
    rc = RxDropMacro('REXX_Function_Call_Performance_3')
  end /* if */

  parse value RxQueryMacro('REXX_Function_Call_Performance_3.cmd') with rc position
  if rc = 0 then
  do
                    /* Drop the function if it is already loaded      */
    rc = RxDropMacro('REXX_Function_Call_Performance_3.cmd')
  end /* if */

                    /* Ensure that it is R/W - so that REXX can store */
                    /* the semi-compiled form in the Extended         */
                    /* Attributes (EAs)                               */
  '@attrib -r -h -s' macsrc

                    /* Now add the function to the beginning of the   */
                    /* macrospace with .CMD extension                 */
  rc = RxAddMacro('REXX_Function_Call_Performance_3.cmd', MacSrc, 'B')
  if rc = 0 then                                             /* v3.30 */
  do
    call charout , ' 'desc.2a
    t = time('R')
    do i = 0 to limit
      res = REXX_Function_Call_Performance_3.cmd(i)
    end /* do */

    say format(time('E'),7,2)

                    /* Finally, drop it to cleanup                    */
    rc = RxDropMacro('REXX_Function_Call_Performance_3.cmd')
  end /* if */
  else                                                       /* v3.30 */
    say " -- Error loading the Macro"                        /* v3.30 */

/* ------------------------------------------------------------------ */
                    /* Now add the function to the beginning of the   */
                    /* macrospace without extension                   */
  rc = RxAddMacro('REXX_Function_Call_Performance_3', MacSrc, 'B')
  if rc = 0 then                                             /* v3.30 */
  do
    call charout , ' 'desc.2b
    t = time('R')
    do i = 0 to limit
      res = REXX_Function_Call_Performance_3(i)
    end /* do */

    say format(time('E'),7,2)

                    /* Finally, drop it to cleanup                    */
    rc = RxDropMacro('REXX_Function_Call_Performance_3')
  end /* if */
  else                                                       /* v3.30 */
    say " -- Error loading the Macro"                        /* v3.30 */

/* ------------------------------------------------------------------ */
                    /* Now add the function to the end of the         */
                    /* macrospace with .CMD extension                 */
  rc = RxAddMacro('REXX_Function_Call_Performance_3.cmd', MacSrc, 'A')
  if rc = 0 then                                             /* v3.30 */
  do
    call charout , ' 'desc.2c
    t = time('R')
    do i = 0 to limit
      res = REXX_Function_Call_Performance_3.cmd(i)
    end /* do */

    say format(time('E'),7,2)

                    /* Finally, drop it to cleanup                    */
    rc = RxDropMacro('REXX_Function_Call_Performance_3.cmd')
  end /* if */
  else                                                       /* v3.30 */
    say " -- Error loading the Macro"                        /* v3.30 */

/* ------------------------------------------------------------------ */
                    /* Now add the function to the end of the         */
                    /* macrospace with no extension                   */
  rc = RxAddMacro('REXX_Function_Call_Performance_3', MacSrc, 'A')
  if rc = 0 then                                             /* v3.30 */
  do
    call charout , ' 'desc.2d
    t = time('R')
    do i = 0 to limit
      res = REXX_Function_Call_Performance_3(i)
    end /* do */
    say format(time('E'),7,2)

                    /* Finally, drop it to cleanup                    */
    rc = RxDropMacro('REXX_Function_Call_Performance_3')
  end /* if */
  else                                                       /* v3.30 */
    say " -- Error loading the Macro"                        /* v3.30 */

/* ------------------------------------------------------------------ */
                    /* Now we do the tests with the function loaded   */
                    /* from disk. NOTE that calls after the first     */
                    /* will often benefit from disk caching.          */
                    /* Performance will vary depending upon the       */
                    /* location of the function's source file in the  */
                    /* search path, so we will do 3 tests, one with   */
                    /* the source file located in the CURRENT         */
                    /* directory, one with the source directory at    */
                    /* the START of the PATH string, another with the */
                    /* source path at the END of the PATH string.     */

                    /* To prepare for this, first take a copy of the  */
                    /* PATH string and remove all references (either  */
                    /* explicit or implicit) to the function's source */
                    /* file directory. This is a bit messy, as that   */
                    /* directory (whatever it is) could appear        */
                    /* explicitly (even multiple times) in the        */
                    /* current PATH statement, or it may be present   */
                    /* (de facto) if it is the current directory and  */
                    /* '.' appears in the PATH string. We need to be  */
                    /* careful to cope with both these possibilities! */

                    /* We already found the source file id location   */
                    /* above - extract just the drive and directory   */
                    /* parts of it                                    */
  MacSrcDsk = FileSpec('D', MacSrc)
  MacSrcDir1 = FileSpec('P', MacSrc)
  MacSrcDir2 = substr(MacSrcDir1, 1, length(MacSrcDir1)-1)
  MacSrcDskDir1 = MacSrcDsk || MacSrcDir1
  MacSrcDskDir2 = MacSrcDsk || MacSrcDir2

                    /* Retrieve the current PATH string               */
  os2 = 'OS2ENVIRONMENT'
  path = value('PATH', , os2)

                    /* For simplicity, we'll ensure that the PATH     */
                    /* does NOT have a leading ';' and DOES have a    */
                    /* trailing ';'                                   */
  path = strip(path, 'B', ';') || ';'

                    /* These are the strings we need to remove from   */
                    /* the PATH string                                */
  try.1 = '.'
  try.2 = MacSrcDskDir1
  try.3 = MacSrcDskDir2
  try.4 = MacSrcDir1
  try.5 = MacSrcDir2

                    /* Make a working copy of the previous PATH, with */
                    /* a leading ';' added (recall we've already      */
                    /* ensured that it has a trailing ';') to ensure  */
                    /* that matches for the first and last components */
                    /* will succeed - we will strip this additional   */
                    /* character after we've deleted any necessary    */
                    /* strings                                        */
  newpath = ';'path

                    /* Now strip each of these strings from the PATH  */
  do i = 1 to 5
    ThisOne = ';'translate(try.i)';'
    Lth = length(ThisOne) - 1
    ctr = 0
    do j = 1 by 1
      t = pos(ThisOne, translate(newpath))
      if t = 0 then
        leave j
      newpath = delstr(newpath, t, lth)
      ctr = ctr + 1
    end j
    ctr.i = ctr
  end i

                    /* Strip the added leading ';'                    */
  newpath = substr(newpath, 2)

                    /* Right - now change into the source file's      */
                    /* directory for the next test                    */
  t = directory(MacSrcDskDir2)

                    /* Set the new PATH value                         */
  t = value('PATH', newpath, os2)

                    /* Now do the test!                               */
  call charout , ' 'desc.3a
  t = time('R')
  do i = 0 to limit
    res = REXX_Function_Call_Performance_2(i)
  end /* do */

  say format(time('E'),7,2)

/* ------------------------------------------------------------------ */
                    /* Now - under OS/2, REXX looks for external      */
                    /* functions in the CURRENT directory before      */
                    /* looking on the PATH, we'll have to change to a */
                    /* different (any different) directory, but we'll */
                    /* have to make sure that there is not a copy of  */
                    /* the external function in that directory. The   */
                    /* simplest way to do this is probably to make a  */
                    /* temporary directory and change into that       */
                    /* directory                                      */
  OriginalDir = directory()
  tempdir = SysTempFileName('TMP?????.DIR')
  call SysMkDir TempDir /* Must work! */
  t = directory(TempDir)

                    /* Prepend the source directory to the `cleaned'  */
                    /* path string                                    */
  temppath = MacSrcDskDir2 || ';' || newpath

                    /* Set the new value                              */
  t = value('PATH', temppath, os2)

                    /* Now do the test!                               */
  call charout , ' 'desc.3b
  t = time('R')
  do i = 0 to limit
    res = REXX_Function_Call_Performance_2(i)
  end /* do */

  say format(time('E'),7,2)

/* ------------------------------------------------------------------ */
                    /* Now we want to have the external source file   */
                    /* directory at the END of the PATH string.       */
  temppath = newpath || MacSrcDskDir2 || ';'

                    /* Set the new value                              */
  t = value('PATH', temppath, os2)

                    /* Now do the test!                               */
  call charout , ' 'desc.3c
  t = time('R')
  do i = 0 to limit
    res = REXX_Function_Call_Performance_2(i)
  end /* do */

  say format(time('E'),7,2)

/* ------------------------------------------------------------------ */
                    /* Can this really be the last test? Strip the    */
                    /* EAs from the external source file, make it R/O */
                    /* to prevent REXX storing the semi-compiled      */
                    /* form, then test again.                         */

                    /* Find a temporary file id to store the stripped */
                    /* EAs                                            */
  EAfile = SysTempFileName('TMP?????.eas')

  '@attrib -r -h -s' macsrc
  '@eautil /S' macsrc EAfile
  '@erase' EAfile

                    /* Make the file R/O                              */
  '@attrib +r' macsrc

                    /* Now repeat the previous test                   */

                    /* Now do the test!                               */
  call charout , ' 'desc.3d
  t = time('R')
  do i = 0 to limit
    res = REXX_Function_Call_Performance_2(i)
  end /* do */

  say format(time('E'),7,2)

/* ------------------------------------------------------------------ */
                    /* Make the file R/W again so that EAs can be     */
                    /* stored on next invocation                      */
  '@attrib -r' MacSrc

                    /* Finally, change back to the original directory */
                    /* and delete the temporary directory             */
  t = directory(OriginalDir)
  call SysRmDir TempDir

exit

/* ------------------------------------------------------------------ */
                    /* internal sub routine used for testing          */
REXX_Function_Call_Performance_1:
  return arg(1)**arg(1)

/* ------------------------------------------------------------------ */
/* code and routines added by Bernd Schemmer for REXX Tips & Tricks   */

/* ------------------------------------------------------------------ */
                    /* error handler for NOVALUE errors               */
CrashIt:
  say 'Error: NOVALUE condition raised in line ' || sigl || '!'
  say '       The undefined variable is "' || condition('D') || '".'
exit 255

/* ------------------------------------------------------------------ */
                    /* load REXXUTIL or RXU                           */
LoadUtil:
  parse upper arg dllName

                    /* install a local error handler            v3.30 */
  signal on syntax name LoadUtilError

  select

    when dllName = 'REXXUTIL' then
    do
                    /* load REXXUTIL                                  */
      call rxFuncAdd 'SysLoadFuncs', 'REXXUTIL', 'SysLoadFuncs'
      call SysLoadFuncs
    end /* when */

    when dllName = 'RXU' then
    do
                    /* load RXU                                       */
       call RxFuncAdd 'RxuInit', 'RXU', 'RxuInit'
       call RxUInit
    end /* when */

    otherwise
      nop

  end /* select */
return 0

/* local error handler for syntax errors in the procedure LoadUtil    */
/*                                                              v3.30 */
loadUtilError:
  say 'Error: Can NOT find the DLL "' || dllName || .DLL'"!'
  say '       This DLL is necessary for this program.'
  if dllName = 'RXU' then
  do
    say 'Hint:  RXU.DLL is a free extension DLL for REXX. You can find it'
    say '       on hobbes for example.'
  end
exit 1

/* ------------------------------------------------------------------ */
                    /* create the necessary external routine          */
                    /*   REXX_Function_Call_Performance_2.cmd         */
                    /* in the directory with this file if it doesn't  */
                    /* exist                                          */
CreateExternalRoutine: PROCEDURE expose macSrc
  parse arg cmdName

  parse source . . thisProg
  progPath = fileSpec( 'D', thisProg ) || fileSpec( 'P', thisProg )
  fullCmdName = progPath || cmdName

  if stream( fullCmdName, 'c', 'QUERY EXIST' ) = '' then
  do
    call stream fullCmdName, 'c', 'OPEN WRITE'
    call LineOut fullCmdName ,,
      "/" || "* " || cmdName || " - investigate the overheads of function calls *" || "/"
    call LineOut fullCmdName ,,
      "/" || "* Arthur Pool .. pool@commerce.uq.edu.au *" || "/"
    call LineOut fullCmdName ,,
      "/" || "* $Id: " || cmdName || ",v 1.2 1998-08-03 15:29:04+10 pool Exp pool $ *" || "/"
    call LineOut fullCmdName ,,
      "  return arg(1)**arg(1)"
    call stream fullCmdName, 'c', 'CLOSE'
  end /* if */
                                                             /* v3.30 */
  if stream( fullCmdName, 'c', 'QUERY EXIST' ) <> '' then
  do
    macSrc = fullCmdName
    return 0
  end /* if */
  else
  do
    macSrc = ''
    return 1
  end /* else */