REXX Tips & Tricks:General hints for REXX
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
- 1 REXX and Y2K
- 2 Reserved Words
- 3 Using comments
- 4 Writing OS independent programs
- 5 Writing filter programs in REXX
- 6 Using the handles 3 to 9 in REXX programs
- 7 Calling REXX programs in the CONFIG.SYS
- 8 Using REXX if booted from diskette
- 9 Force a REXX program to run in a special way
- 10 Starting other programs in REXX programs
- 11 Parameters...
- 12 Special environment variables for REXX programs
- 13 Special filenames
- 14 String constants
- 15 Variables
- 15.1 Special variables
- 15.2 Read-only and Write-only environment variables
- 15.3 Compound variables
- 15.3.1 Defining compound variables
- 15.3.2 Using "open" compound variables
- 15.3.3 Initializing compound variables
- 15.3.4 Using compound variables
- 15.3.5 Using variables for the stem=
- 15.3.6 Using variables for the tail
- 15.3.7 Writing general routines for compound variables
- 15.3.8 Local variables
- 15.3.9 Global variables
- 15.3.10 Global variables across more than one REXX program
- 15.3.11 Using Persistent Variables
- 15.3.12 Using Persistent Variables
- 15.3.13 When to Quote Variables in Functions and/or Procedures
- 15.3.14 List all defined variables
- 16 File, directory and device handling
- 17 Error handling & debugging
- 18 Programming techniques
- 19 Redefinition of internal functions
- 20 Redefinition of functions from a DLL
- 21 Loading more than one DLL with the same function names
- 22 Hints for some keyword instructions
- 23 Hints for some REXX functions
- 23.1 The function CHARS()
- 23.2 The function DIRECTORY()
- 23.3 The function FILESPEC()
- 23.4 The functions LINEIN() and PULL()
- 23.5 The function LINEOUT()
- 23.6 The function LINES()
- 23.7 The REXX API functions
- 23.8 RxFuncAdd
- 23.9 The function RxFuncDrop()
- 23.10 The function RxMessageBox()
- 23.11 The function STREAM()
- 23.12 The function TIME()
- 23.13 The function TRANSLATE()
- 24 Hints for using QUEUEs
- 25 The PARSE instruction
- 26 Optimizing REXX programs for speed
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 */