REXX Techniques and Practices
This article explores techniques and practices applicable to writing small to intermediate sized REXX programs. It does this by taking a working sample and showing how it was designed, written and tested.
REXX is one of the very useful features of OS/2. It has great power if used properly but, like any tool, can be a problem if it isn't. I have been using REXX now for many years (more than I care to remember). I first used it on IBM's Virtual Machine (VM) operating system. Then I used a version on PC DOS before I ran out of memory and had to stop. Now, of course, I use the very excellent version available with OS/2.
I have a number of REXX programs that I have developed over the years and that I have become very dependent on. It struck me the other day that I could use one of these as a sample for showing off some of the techniques and, hopefully, best practices I developed for myself over the years. And so here we are.
The program I will describe handles all the backup requirements for my stand-alone system. Those of you using networked systems hopefully have access to centralised, policy driven backup mechanisms that would make such a program unnecessary. If not, you may find this tool useful as well.
As part of this article, I will be providing the complete source code for the program, explaining how it works, looking at coding techniques and showing how to integrate it into the Workplace Shell environment. These details can all be found by viewing the following sections:
- 1 Getting Started
- 2 External Design
- 3 Initial Environment
- 4 Checking Parameters
- 5 Query Modified Files
- 6 Backup Files
- 7 Testing the Program
- 8 Desktop Integration
Prerequisite Skills and Tools
In this article I will be assuming a general introductory knowledge of REXX. For example, it is probably a good idea to have written a couple of short REXX programs and you should be familiar with the general syntax. If not, have a browse through the online book REXX Information which comes with OS/2. You will find it in the Information folder.
For more in-depth skills, there are a number of books on the topic available from your local technical book shop. Try these:
- The REXX Language by M.F. Cowlishaw
- ISBN 0-13-780651-5 (2nd Ed) Prentice Hall
- Writing OS/2 REXX Programs by Ronny Richardson
- ISBN 0-07052-373-X McGraw-Hill
Finally, there is the [REXX Home Page] with lots of useful information about what's happening in the world of REXX. Another good web site is REXX Information.
The only tools we will be using are those available to you in the standard OS/2 package. I am using Warp but the same tools have been available since OS/2 Version 2.0 days and some of them before even that.
Before we write anything we must at least decide on the problem we want to solve. Those of you with programming experience should hopefully be familiar with this step. Take some time to figure out what you want from the program before you think about how you are going to write it. Here are my requirements for my backup management tool:
- Firstly, I would like to be able to get answers to questions like "Have I backed up all the files in this directory tree?" and "Have I findd any files in this directory tree since I last backed it up?"
- Secondly, I would like to know how many bytes need to be backed up.
- Thirdly, I would like to be able to backup any directory trees identified as containing findd files to diskettes. I would like to be able to select a full backup of all files or a partial backup of findd files which gets added to the back of an existing backup diskette sequence.
- Lastly, I would like all this to happen in my native environment, the Workplace Shell. For example, it is no good developing a solution that runs in a DOS or OS/2 full screen session. I just wouldn't be interested. I should be able to select the directory tree to be operated on by dragging it to an icon of some sort. By the same token, any results should be shown in a PM window not a text window.
- Oh yes, and I don't want to spend any money. We must use what comes out of the OS/2 box.
Hmmm! Well now we know what we want let's continue to the next section and turn those requirements into a design.
We have taken a bit of time to think through our requirements and get what we really want straight in our minds. Now we should do the same when considering the external design of the solution we are trying to put together.
Having decided on a REXX solution, the external things we should consider are as follows:
- Parameter Driven or Interactive
- Output Format
These points are examined in the following sections.
Parameter Driven or Interactive
There are two styles of REXX program possible. One is driven entirely by parameters supplied on the command line. If something is wrong with these, the program shows a message and stops. You correct the problem and reissue the command. You are probably familiar with commands of this type. An example is the OS/2 DIR command.
The other style is totally interactive. The command is started with no parameters at all and the user is prompted for any input along the way. This type of command is rarer for text based programs these days but can be seen in action with most PM programs.
So which are we to choose? Interactive would seem to be the most attractive. However, there is a requirement that at least one of the parameters, the target directory name, must be supplied by dragging and dropping. If you missed that, go back and check it in Requirements Analysis.
Now, drag and drop interactively is a bit beyond REXX unless we spend money on some tools. However, as we shall see in the Desktop Integration section, we can invoke a program by dropping a file or directory on to it. The program is started with the file or directory name as its parameter. This sounds ideal and dictates our choice of execution mode in this case.
The only thing that remains is the choice and format of parameters. From the requirements, we have three things we need to be able to do:
- We need to be able to query a directory tree to see whether it has any modified (not backed up) files in it. There two parameters applicable to this operation, a flag to distinguish the operation from the others and the name of the directory at the head of the directory tree to be checked. Let's use /LIST as the flag and allow any abbreviation down to /L.
- We need to be able to backup all files in a given directory tree whether or not they have been modified. Again, there are two parameters applicable, a flag, as before, and the name of the top level directory. Let's use /FULL as the flag to indicate a full backup and allow any abbreviation down to /F.
- Finally, we need to be able to backup just the modified files in a given directory tree. As before, two parameters are applicable, a flag and the top level directory. Let's use /MODIFIED as the flag and allow abbreviations down to /M.
Specifying long flag names and allowing abbreviations is good practice. When the command is issued manually from the command line, the abbreviations can be used. When they are encoded in a program object or batch or REXX file, the full form can be used for documentation's sake.
Another parameter set is required to display help. All programs should display a bit of help even if it is only a memory jogger. A lot of people write commands solely for themselves and which they use infrequently enough to forget what they had set up. No help in this situation means digging through the code to find out what you originally meant.
There are a number of conventions used to get help from a program. The most popular are specifying no parameters at all or a parameter of ?, /? or /HELP which can be abbreviated down to /H. We will support all of these.
Finally, we need a name for the command file itself. I have called mine BACKUPMANAGEMENT.CMD which is fine as I don't expect to be typing it much. I will be using a program object to invoke the command. This gives us a command line syntax as follows:
The first piece of output from the program should be an identifying banner naming the program, identifying its version and giving ownership or copyright details. All programs that have any form of displayed output should do this. For example, PM programs usually have a Program Information menu item on their Help menu. We will just display the following banner at the top of our displayed output:
Manage Directory Backups Version 2.00 (C) Copyright N.S.McGuigan 1994-1995
It is a good thing to codify the help information right at the start as this assists us to finalise our external design decisions. I normally display a title such as:
Title : Manage Directory Backups
This is followed by the syntax diagram as shown above and an explanation of the syntax such as:
Performs a query, full backup or partial backup function against a specified directory and all its subdirectories. /List Displays the number of files and bytes needed to be backed up within the specified directory followed by a list of subdirectory names showing the number of files and bytes for each. /Full Performs a full backup of all files and subdirectories within the specifed directory to the A: drive. /Modified Performs a partial backup of all modified files within the specified directory and all contained subdirectories to the A: drive. dirpath The name and path to the directory to be queried or backed up. /Help, /? or ? Displays this help information. Issuing the command with no parameters has the same effect.
Nailing down the output from the query (/LIST) function, we need to show the number of files requiring backup and the number of bytes in them. I have decided to list the total number of affected files and the total number of bytes that potentially need to be backed up in the target directory and all subdirectories and then to show a break down by top level subdirectory. Something like this in fact:
Directory D:\Workarea Total Files: 654 Total Bytes: 20039765 Files Bytes Directory Name ----- ----- -------------- 5 104826 D:\Workarea\ASSOED03 9 1317363 D:\Workarea\EXCAL 101 1812912 D:\Workarea\Fonts1 155 1424041 D:\Workarea\Fonts2 368 11951542 D:\Workarea\Fonts3 11 121828 D:\Workarea\PMVIEW 2 3219710 D:\Workarea\WKSCSD1 Query Directories For Modified Files Complete
The initial message shows the target directory name. The final message indicates a successful conclusion. We must allow for situations where no subdirectories exist:
Directory D:\Workarea\EXCAL Total Files: 9 Total Bytes: 1317363 No Subdirectories Query Directories For Modified Files Complete
For the backups, we should announce the type of backup, full or modified, being performed and the name of the top level directory. The rest of the output will come from the BACKUP command itself. For the full backup we will see something like:
Backing up all of D:\Workarea\EXCAL Insert backup diskette 01 in drive A: Warning! The files in the root directory of target drive A: will be erased. Press Enter to continue or Ctrl+Break to cancel. The files are being backed up to drive A. Diskette number 01 \WORKAREA\EXCAL\excal.ann \WORKAREA\EXCAL\EXCAL.CMD \WORKAREA\EXCAL\EXCAL.DLL \WORKAREA\EXCAL\EXCAL.HLP \WORKAREA\EXCAL\excal.new \WORKAREA\EXCAL\excal.zip \WORKAREA\EXCAL\EXCINST.EXE \WORKAREA\EXCAL\README.TXT \WORKAREA\EXCAL\UPDATEOV.CMD Backup All Files in Directories Complete
Similarly, for the modified backup:
Backing up modifications to D:\Workarea\ObjUtil Insert the last backup diskette in drive A: Press Enter to continue or Ctrl+Break to cancel. The files are being backed up to drive A. Diskette number 01 \WORKAREA\OBJUTIL\ObjUtReg.cmd Backup Modified Files in Directories Complete
This looks like it should cover all our requirements. With our external design complete, we can now start writing the REXX program that implements the solution. The initial stages of this process are covered in the next section.
This section deals with all the housekeeping and setup minutae that is so essential to a good maintainable piece of code. Hopefully, that is what you want for your investment for that is what it is, an investment of your time and effort.
So much REXX code appears to be "write only" code, quite unintelligable to all except the person who wrote it. After the passage of time, even he or she may find the code growing less comprehensible by the moment. This is a pity really as the application of a bit of discipline and a few rules can make code quite readable.
All REXX programs must start with a comment. This can be anything you want including an empty comment such as:
However, it is far more useful to take some time and enter up some useful really information such as the program title, a short description of what it does, the current version and who owns it. This is my effort from the actual code:
/********************************************************************/ /* */ /* MANAGE DIRECTORY BACKUPS */ /* */ /* Performs a query, full backup or partial backup */ /* function against a specified directory and all its */ /* subdirectories. */ /* */ /* Version 2.00 (C) Copyright N.S.McGuigan 1994-1995 */ /* All Rights Reserved */ /* */ /********************************************************************/
Immediately after the initial comment block, I usually put the following two statements:
trace o; '@ECHO OFF'
The first, the trace statement, sets the debug trace off. This looks a bit peculiar as the initial state of the trace is off by default. However, it serves as a place where I can flip the trace on or off by just changing the o parameter. There is more about tracing in the Testing the Program section.
The Echo statement is a stock standard OS/2 statement as used in old-style batch files. It suppresses the echo of commands being executed onto the display. The command output is still displayed but the command itself is not seen.
A few comments are necessary about how REXX communicates with its command environments. The command environment we are working with is effectively the OS/2 command line processor, CMD.EXE. The purpose of the REXX program is to construct as strings any command for its host environment and pass these to the command processor for execution. When the host environment is the OS/2 Command Line, these may be any valid OS/2 command.
The way this is done is to build a string containing the command and to return it without assigning it to another variable. For example:
MyVar = 'DIR /W'
will assign the string 'DIR /W' to the variable MyVar. Contrast this with the following:
In this case, the string is not assigned to anything and REXX interprets this as meaning it is to be returned to the command processor, CMD.EXE in our case, for execution. When execution has finished, control returns to the program at the next statement and the ERRORLEVEL return code is found in the RC variable.
This answers the often asked REXX newbie question, Why are commands placed in quotes? The answer is that REXX doesn't see them as commands at all but as strings. REXX has no knowledge of the valid commands for any of its host environments. However, sometimes one can see the following in a program:
What has happened here is the programmer is taking advantage of a default substitution rule. That is if a variable has no value, it is replaced by its own name in uppercase. DIR is seen as a variable by REXX as it is not a keyword and it is not enclosed in quotes. Hopefully, it does not have a value and will be replaced by the value DIR.
Now there is the source of error. The programmer is relying on the fact that the variable DIR will not have a value. Very dodgy! My rule is to write all non-substitutable strings as strings, that is with quotes. Hence, '@ECHO OFF' not @ECHO OFF. I strongly advise you to do the same.
The first thing we need to do during start up is to display the program banner. We do this no matter what else we plan to do. I prefer to keep code in the top level of the program, usually called the mainline, to a minimum. This tends to make the overall logic of the program more visible at a glance. So we take the code that displays the banner and place it in a separate subroutine. The statement that invokes the subroutine is:
The actual routine may be found lower down in the REXX code and looks like this:
/**************************************/ /* Display Banner */ /**************************************/ DisplayBanner: procedure; say 'Manage Directory Backups'; say 'Version 2.00 (C) Copyright N.S.McGuigan 1994-1995'; say; return 0;
The format is taken from the External Design section. I like to place a short one line comment block before my subroutines and functions to make them standout. This is followed by the header DisplayBanner: procedure starting in the same column. Then follows the body which I normally indent two spaces.
It doesn't really matter what standards and conventions you use to arrange your code as long as you remember the following points:
- You should use the same arrangements throughout the code. Chopping and changing will just make your program look messy.
- The purpose of a standard covering the way you write and arrange your code should be to make your code clear and readable. This not only benefits subsequent readers but will undoubtably benefit you when you return to your code after the passage of time.
- Any such coding standard should be self consistent. The more rules and exceptions, the more suspicious I am of a standard.
- The standard should take into account and follow the syntax of the language. The standard you use for COBOL or C++ ain't going to cut it with REXX and vice vera.
- The standard should emphasise natural breaks, such as individual routines, and guide the eye through the structure of the code within.
Following the banner, we call a routine to do various initialisation tasks around the place. This is called by:
The actual routine has a number of interesting features and looks like this:
/**************************************/ /* Initialise Program */ /**************************************/ InitialiseProgram: procedure; /* Load Utility Functions */ call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'; call SysLoadFuncs; /* Clear the Queue */ do while queued() > 0; pull .; end; /* Return */ return;
The first thing to note is the use of the procedure statement just after the routine name. Subroutines and functions in REXX are identified by a label, a name coded with a colon (:) following it. The procedure statement is optional and tells REXX that all variables outside the routine are invisible to the routine and that all variables defined within it are to be dropped when the routine returns. In other words, it gives us our own little "variable sandpit" to play in. We don't have to worry about messing up variables defined outside by assigning values to them by accident.
Routines rarely exist in a vacuum. If we need to get values from outside the routine, they can be passed in as parameters. The routine can return a value on its return statement to get values out. Finally, we can expose variables external to the routine by adding the expose variable [variable...] clause to the procedure statement. This is commonly used to make genuinely global variables visible to a routine or to pass stem variables into one. Stem variables cannot be passed by parameters. Another common use is where routines need to return multiple values which isn't possible with a single return statement.
The InitialiseProgram routine doesn't work with any global stuff and so there is no expose on the procedure statement. The first thing it does is register the extended REXX utility functions. These functions are supplied as part of the standard OS/2 REXX kit and can perform a number of OS/2 related chores for us. We will be using some of these later in the program. The functions are documented in the REXX Information online book under the heading REXX Utility Functions (RexxUtil).
Before the utility functions can be used, they must be registered. This is standard with all function packages that are not part of the base language. Registration is done using the RxFuncAdd function. Now using this function, we could individually register each of the functions we are going to use. However, the utility function package provides another way. We register just the SysLoadFuncs function and then we call this function which registers the rest for us. The RxFuncAdd function is documented in REXX Information in Functions under the API Functions section.
The second piece of work we need to do is clear the input queue. This is to get rid of any left overs from previous executions. Remember, the REXX queue is global and could have been messed up by another program altogether. To clear the queue, we execute a loop that pulls lines off the queue until it is empty. The lines are pulled using:
The full stop (.) is a placeholder which tells REXX to toss whatever was pulled into that particular position. The loop is implemented by a repetitive do. The control is a while which tests to see if the number of lines in the queue, returned by queued(), is 0. This condition is checked at the start of each turn around the loop so if the queue is empty, we go straight to the return statement.
The last thing we do in the mainline as part of the initialisation is save the current directory as follows:
CurrentDirectory = directory();
We will use this to restore the current directory at the end of the program. It is good manners to restore as much of the initial environment as possible when completing a program. This particular program finds the current directory during execution so our last statement before terminating restores it as follows:
call directory CurrentDirectory;
Now we have displayed our program banner and have set up the environment for action, our next step is to work out what we have been passed as parameters. This is covered in the next section.
Any form of user input, and this includes parameters, needs to be checked fairly thoroughly. The intent is, on the one hand, to avoid getting the program confused when wrong or inconsistent data is passed to it and, on the other, to avoid having it do the wrong thing. An example of the first might be trying to add up a list of names. An example of the second might be deleting a file when we meant to copy it.
The sample code shows a fairly thorough piece of checking without getting laborious about it. The mainline code looks like this:
/* Check for Command Line Help Request */ parse arg Parms; if abbrev('/HELP',translate(Parms),2) = 1 |, Parms = '?' | Parms = '/?' | Parms = then do; call CommandHelp; exit 1; end; else call ValidateParameters Parms;
Acting on the rule Never Assume Anything About User Input, we get the entire parameter string into the Parms variable with the parse arg statement. Now we can poke it with a stick to see what's in there.
The if statement conditional is just testing for all the forms of help requests we have agreed to support as documented in the External Design section. Apart from checking to see if Parms contains the strings ?, /? or is empty, we use the abbrev function to check to see if the value is some abbreviation of /HELP. As we can't assume anything about case, we use the translate function without any parameters to give us an uppercase version of Parms. The final parameter of 2 says we must have a minimum of two matching characters. In other words, we will accept /H, /HE, /HEL and /HELP but not / or nothing.
If we come up true on these tests, the CommandHelp routine is called to display the help. This is another subroutine which uses say statements to display useful help information. Normally, you should try to keep this under 24 lines, the standard window size but as we will be using a scrollable PM window (see #Desktop Integration), we can let ourselves go a bit.
On return from displaying the help information, the exit statement is used to terminate the program with a return code of 1. The return code is the ERRORLEVEL code returned by programs when they finish executing and indicates how things went. As with all things, it is better to have a simple scheme for assigning these codes. Mine is as follows:
0 - Good Result 1 - Help Displayed 2 - Error in Parameters 3 - Problem During Execution 4 - Disaster (Possible Environment Corruption) 10+ - Other Program Defined Returns
the program defined returns (10+) can be good or bad. They are program defined after all. Sticking to this scheme, help only was displayed and hence the return of 1.
Having disposed of the help possibility, we know we have a business request and so the else clause calls another routine, ValidateParameters, to check the supplied parameters and to extract the information the program needs from them. The Parms variable is passed to it as the single parameter. The routine header looks like this:
/**************************************/ /* Validate Parameters */ /**************************************/ ValidateParameters: procedure expose Operation DirPath;
Here we are using the expose clause to make the Operation and DirPath variables visible to the routine. These have not been defined yet but when they are, within the routine, they will carry a value indicating the required operation and the name of the top level directory (target directory) back to the rest of the program. Effectively, these two variables are the output from the routine.
The next section breaks the Parms parameter string up into pieces and checks we have the right number:
/* Get Individual Parameters */ parse arg Operation DirPath OtherStuff; /* Check For Too Much Stuff */ if OtherStuff <> then do; say 'Invalid or Too Many Parameters -' OtherStuff; say; call CommandHelp; exit 2; end;
The parse statement grabs the parameter string passed to the routine, Parms, and breaks it up according to the supplied template. In this case, the template is just a list of variables into which it places the extracted words. It will place the first word into the Operation variable, the second word into the DirPath variable and anything else into OtherStuff. Remembering our External Design, Operation should then contain the operation flag parameter, DirPath should contain the target directory name parameter and OtherStuff should be empty.
And that is what we have to check. The first test checks to see if there is anything in OtherStuff. If it is not empty, then we know we have a problem. We issue a message and terminate with a return code of 2 meaning a parameter failure. Before we terminate however, we display the parameter help information. This might assist our user with the parameters and avoid another failed attempt.
As a thought, the help information shown is quite lengthy. It might be more appropriate to show a slimmed down memory jogger such as the syntax diagram with a suggestion to try /? for more detailed help. Your users, your choice.
Next we concentrate on the flag parameter. We need to determine if it contains any of the valid flag values, in any case, or any valid abbreviation of the same. Here is the code:
/* Edit Operation Flag */ select; /* Check for List Operation */ when abbrev('/LIST',translate(Operation),2) then Operation = '/LIST'; /* Check for Full Operation */ when abbrev('/FULL',translate(Operation),2) then Operation = '/FULL'; /* Check for Modified Operation */ when abbrev('/MODIFIED',translate(Operation),2) then Operation = '/MODIFIED'; /* Failed Validation */ otherwise say 'Invalid Operation Flag -' Operation; say 'Should Be /List, /Full or /Modified' say; call CommandHelp; exit 2; end;
To check for the three possible flag settings, we use a select statement. Each component when clause checks for one of the valid flag values in Operation. The format of the conditional in each clause is similar in form to the check for /HELP that appeared in the mainline section.
If the value matches any of the valid flag formats, then we have passed this part of the edit. In this case, we assign a known value to the Operation variable. This enables us to perform a simple compare of a known value later when we need to know what function we are performing instead of repeating the edit tests.
If the contents of Operation match none of the when clauses, then the contents are invalid and the edit fails. This is caught in the otherwise clause. As before, the response is to issue a message, call CommandHelp and terminate with a return code of 2.
At this point, we know we have no more than two parameters and that the first one is the operation flag as expected. All that remains is to validate the target directory name that is expected to be in the DirPath variable. Our requirements are that it be present and be a valid, existing directory name. Here is the code:
/* Edit Target Directory */ if DirPath = '' | CheckDirectory() = 0 then do; say 'Directory Path Omitted or Invalid' say; call CommandHelp; exit 2; end;
The first clause in the conditional checks to see if there is a value in the DirPath variable. If not, we fail the first requirement and the edit. The second clause invokes the CheckDirectory function, found lower down in the REXX source, to perform the check on the variable contents. If this returns a value of 0, the value is invalid and the edit fails. Failing the edit results, as before, in the display of a message and the help information and the termination of the program with a return code of 2.
Checking the validity of the directory name is simplified by the condition that it must exist. Instead of examining the contents of DirPath, we can try changing to the directory and see what happens. Simple but effective.
There are two techniques I could have used. With one, I could have used the directory function. Using this, I could have got the current directory, findd to the new one and then tested for any find. If there was find, the proposed name was valid. This works fine as long as you are not currently working in the target directory. In that case, there would be no find and we could not determine if the directory was valid or not.
In my opinion, a better solution is shown in the sample code. In this, I use the OS/2 CHDIR command. This has the advantage of giving a message if the directory is not valid and none if it is valid. Let's try it on a command line:
[OS2] CHDIR D:\Workarea [OS2] CHDIR Rumplestiltskin SYS0003: The system cannot find the path specified.
Here is the code for the CheckDirectory routine that uses this command:
/**************************************/ /* Validate Directory */ /**************************************/ CheckDirectory: procedure expose DirPath; /* Check It Exists */ 'CHDIR' DirPath '2>&1 | RXQUEUE' call GetQueueData; if QueueList.0 > 0 then return 0; /* Return Result */ return 1;
First, the routine uses the DirPath variable globally. No parameters are passed. This limits the reuse potential of the routine but has no ill effects in our case. It would perhaps have been better to pass the directory name as a parameter which would have increased the reuse potential of the routine in different situations.
Next, we execute the CHDIR utility and pipe its output to the RXQUEUE utility which makes it available to us through the parse pull statement. The only fly in the ointment is piping works with standard input and output but the SYS0003 error message is sent to standard error. We have to fix that by redirecting standard error to standard output using the 2>&1 notation.
To pull the queued output off the queue, we have created a little utility routine that reads the queue and places the contents in a stem variable. The code for this is:
/**************************************/ /* GET DATA FROM QUEUE */ /**************************************/ GETQUEUEDATA: PROCEDURE EXPOSE QUEUELIST.; /* LOAD DATA INTO QUEUE */ INDEX = 0; DO WHILE QUEUED() > 0; Index = Index + 1; parse pull QueueList.Index; end; /* Set Size of Queue List */ QueueList.0 = Index; /* Return */ return;
The procedure statement exposes the QueueList. stem variable. It is in this variable that the routine will return the contents of the queue as follows:
QueueList.0 - Count of lines returned QueueList.1 - First line from queue QueueList.2 - Second line from queue ... QueueList.n - nth line from queue
The function loops around while there are lines in the queue keeping a count as it goes. When the queue is empty, it assigns this count to QueueList.0 and returns to the calling routine.
In our case, if the directory is valid, QueueList.0 should be 0. If the directory name is invalid, QueueList.0 should contain 1 and QueueList.1 should contain the error message. Thus for our purposes it is sufficient to test for a non-zero QueueList.0 value. If non-zero, the directory name is invalid and the value fails the edit. In that case, CheckDirectory returns a zero value. Otherwise, the value is good and it returns 1.
That ties up the parameter editing. If we got through to this point, we have values we can trust in Operation and DirPath. If not, we communicated this clearly to the user and terminated the program. On achieving a successful edit, we return to the mainline and carry out the specified operation using a select statement as follows:
/* Process Selected Operation */ select; /* Query Directories For Modified Files */ when (Operation = '/LIST') then do; say 'Directory' DirPath; call QueryMainDirectory DirPath; call QuerySubDirectories DirPath; say 'Query Directories For Modified Files Complete'; end; /* Backup All Files in Directories */ when (Operation = '/FULL') then do; say 'Backing up all of' DirPath; call BackupDirectory DirPath; say 'Backup All Files in Directories Complete'; end; /* Backup Modified Files in Directories */ when (Operation = '/MODIFIED') then do; say 'Backing up modifications to' DirPath; call BackupDirectory DirPath, '/A /M'; say 'Backup Modified Files in Directories Complete'; end; end;
As the Operation variable must contain one of the value /LIST, /FULL or /MODIFIED as set in the edit function, we can key our invocation of the various functions based on that value. This is implemented by the select statement with the various when clauses doing the testing.
In the next section, we will go on to the implementation of the query function.
Query Modified Files
This is the more complicated operation of the set because there is no convenient little utility we can invoke to do it all for us. We get a bit of help from the RexxUtil functions but the rest is up to us. Let's see how we go about it.
The operation is coded in two parts that correspond to the two pieces of output we want. The first is some overall information giving the total number of files that need to be backed up and the number of bytes they represent. From the example in External Design:
Total Files: 654 Total Bytes: 20039765
Our first part calculates these figures. The second produces the breakdown by directory as in the following example:
Files Bytes Directory Name ----- ----- -------------- 5 104826 D:\Workarea\ASSOED03 9 1317363 D:\Workarea\EXCAL 101 1812912 D:\Workarea\Fonts1 155 1424041 D:\Workarea\Fonts2 368 11951542 D:\Workarea\Fonts3 11 121828 D:\Workarea\PMVIEW 2 3219710 D:\Workarea\WKSCSD1
You will see when we go through the code that the algorithm chosen does two passes of the directory structure. It is perfectly possible to do the whole thing in one pass. The reason this method was selected was, quite simply, it was easier to code. I was in a hurry when I first put the program together and so I chose the easy way. I was intending to find it later but the performance has always been so good that I haven't had the incentive to do it.
As usual, the mainline code under the select statement's when clause controls how the whole thing is put together:
/* Query Directories For Modified Files */ when (Operation = '/LIST') then do; say 'Directory' DirPath; call QueryMainDirectory DirPath; call QuerySubDirectories DirPath; say 'Query Directories For Modified Files Complete'; end;
The first say displays the target directory at the head of the tree being scanned. If you remember, the editing functions, covered in Checking Parameters, finish up with the target directory name stored in DirPath. Then we call QueryMainDirectory to calculate and display the first section of output followed by QuerySubDirectories to handle the subdirectory breakdown. The target directory name is passed to both these as the sole parameter. The displayed output is ended with the operation completion message.
Let's look at QueryMainDirectory first. Using procedure in the header confirms we are isolating the routine from any variables defined outside it. There are no exposed names:
/**************************************/ /* Query Main Directory */ /**************************************/ QueryMainDirectory: procedure;
The first piece of code retrieves the target directory name parameter as a single string using a parse arg statement:
/* Get Directory Name Parameters */ parse arg DirPath; say;
The target directory name is then fixed up so a file name template can be appended to it without any fiddling about. This is done to facilitate the subsequent searching operations.
/* Check Terminating Character */ if right(DirPath,1) <> ':' & right(DirPath,1) <> '\' then DirPath = DirPath'\';
If the target directory name ends in a semicolon (:) or a backslash (\), it doesn't need changing. For example, D: effectively means the current directory on drive D and D:\ means the root directory on that drive. Adding a backslash to either would find the meaning of the first and the validity of the second. All others need a backslash.
We use the right function to test the character at the end of the directory name, the rightmost character. Concatenation of the directory name and the backslah is handled by abuttal. This means they are stuck up against each other. There are a number of ways of doing concatenation you should be handy with. Suppose Var1 contains AAAA and Var2 contains BBBB. Then:
Var1'\' gives 'AAAA\' Var1 '\' gives 'AAAA \' Var1 || Var2 gives 'AAAABBBB' Var1' 'Var2 gives 'AAAA BBBB'
Concatenating two variables requires the || operator. This concatenates without spaces. Most other concatenation can be done using abuttal. Putting two things up against each other concatenates with no space. Leaving one or more spaces inserts just one space. If you want more spaces, insert them as a string literal as follows:
Var1 Var2 gives 'AAAA BBBB' Var1' 'Var2 gives 'AAAA BBBB'
We will look at fancy formatting for numbers later in this section. Next we call a routine called GetDirectoryTotals to calculate the totals:
/* Get Main Directory Totals */ call GetDirectoryTotals DirPath;
As we will see, the GetDirectoryTotals routine is called both from here and from the QuerySubDirectories routine. It takes a directory name and calculates the total number of modified files requiring backup and the total number bytes in them. It returns these totals in two variables, TotalFiles and TotalBytes.
In our case, the directory name passed is the target directory name which will result in the routine calculating its totals for the entire directory structure. This matches our expectations for this part of the program. We will look at this routine in a moment but let's assume for the moment it completes and returns its counts as predicted. The next thing to be done is check to see if anything has been found to backup. We do this by checking the total number of modified files found:
/* Check for No Files */ if TotalFiles = 0 then do; say 'No Files Requiring Backup'; say; exit 0; end;
If there are no files needing backup, we don't need to go any further. We have all the information we need so we display a message and exit with a return code of 0 for successful completion. If there are files requiring backup, we display the totals obtained and return to the mainline so the next routine can be invoked:
/* Display Total Files and Bytes */ say ' Total Files:' TotalFiles; say ' Total Bytes:' TotalBytes; say;
/* Return */ return 0;
Now let's have a look at the GetDirectoryTotals routine. This is the heart of both the QueryMainDirectory and QuerySubDirectories routines. The header looks like this:
/**************************************/ /* Total Files/Bytes for Directory */ /**************************************/ GetDirectoryTotals: procedure expose TotalFiles TotalBytes;
As expected, the total variables TotalFiles and TotalBytes are named in the expose clause on the procedure statement. Just in passing, it is a common error to put commas between the names on the expose clause.
Next, we get the directory name parameter and set the two totals counters to 0:
/* Get Parameters */ parse arg DirName; TotalFiles = 0; TotalBytes = 0;
Our strategy for calculating the figures required involves the use of a RexxUtil function called SysFileTree to list all the files in the directory and its subdirectories. This function returns these names in a REXX stem variable which we can then cycle through totting up our counts. Here is the code:
/* Search for Files */ call SysFileTree DirName'*', 'FileList', 'FS', '+****'; if result <> 0 then do; say; say 'Error Searching Directory' DirPath; exit 3; end;
The first parameter for SysFileTree is the directory name with a global file name template attached. Simplifying the construction of this template is the reason we needed to adjust the directory name in the calling routine. The next parameter is the name of the stem variable into which the function will place the file details.
The third parameter modifies what the function searches for and where it searches for it. The F means search for files only and the S means search all subdirectories as well as the specified directory.
The last parameter ensures we list only modified files, the objects of our interest in this program. Each position in the given five character string represents one of the file attributes as follows:
A - Modified D - Directory H - Hidden R - Read Only S - System
These are arranged in the order 'ADHRS'. An asterisk (*) in any position means we don't care what that attribute's value is. A plus (+) means we want only files with that attribute on listed. A minus (-) means we want only files with that attribute off listed. Thus '+****' means we want only files with the modified attribute on listed and we don't care about any of the others.
The result of calling this function with these parameters is a list of every file requiring backup. The list is stored in the FileList stem variable with FileList.0 being set to the number of items in the list.
We take the time to check the return code from the function to see if there have been problems. Even though we used a call statement to call it, SysFileTree is a function and returns a result. We could have coded the call as follows:
retval = SysFileTree(DirName'*', 'FileList', 'FS', '+****');
If we use the call statement instead, the return value from the function is placed in the special variable result. In our code, this is the variable we test. A non-zero value indicates a problem. If this is the case, we issue a message and collapse gracefully with a return code of 3 for a serious execution error.
If we had omitted the assignment to retval in the last example, the returned value would have been passed to the command environment as a host command. This can be a confusing source of errors. Remember, REXX knows zippo about host commands and will pass anything you tell it to the command processor.
Having got all the file details, we now need to scan them in a loop to get the required totals. The format of each stem variable entry is date, time, size, attributes and fully qualified file name. For example:
8/15/95 1:25p 65996 A---- D:\Workarea\04500050.gif 8/15/95 1:55p 13502 A---- D:\Workarea\04e00001.gif 8/22/95 6:07p 739 A---- D:\Workarea\Capture.TXT 8/17/95 11:19p 8045 A---- D:\Workarea\DilbertZone.gif
The date and time formats takes no account of the formats specified by your country code. However, the only thing we are interested in is the file size. We know that each file is a modified file in or under the target directory so we just need to add its size to our total. Here is the code:
/* Total Files and Bytes */ do Index = 1 to FileList.0; parse var FileList.Index . . FileSize .; TotalFiles = TotalFiles + 1; TotalBytes = TotalBytes + FileSize; end;
We use a variable Index to index the stem variable. It starts at one. Each time we go through the loop, one is added to it until it is greater than FileList.0, the number of entries in the stem at which time we drop out of the loop.
For each pass, we parse the current stem entry, FileList.Index, into four pieces. The pieces corresponding to the fullstops (.), placeholders, are thrown away and only the piece corresponding to the FileSize variable is retained as follows:
With the file size safely in FileSize, all we need to do is add it to the TotalSize counter and add one to the TotalFiles counter. When all the files have been treated in this way, we have our totals and so we drop the FileList stem variable and return to our caller:
/* Return */ drop FileList; return 0;
That is the code to put the overall totals up. The individual total lines are handled by the QuerySubDirectories routine which is called next. Let's have a look at it starting with the header:
/**************************************/ /* Query Dependent Directories */ /**************************************/ QuerySubDirectories: procedure;
No surprises here. The only thing the routine needs, the target directory name, is passed in as a parameter so there is no need to expose anything. The parameter is retrieved using a parse arg as usual. This reads the name as a single string into DirPath. Next we adjust that name as we did in the last routine, that is we find it so we can append a global file template by adding a backslash (\) if necessary. Then it is used in this way with the SysFileTree function to obtain a list of all the subdirectories within the target directory:
parse arg DirPath; if right(DirPath,1) <> ':' & right(DirPath,1) <> '\' then DirPath = DirPath'\'; call SysFileTree DirPath'*', 'DirList', 'DO'; if result <> 0 then do; say; say 'Error Searching Directory' DirPath; exit 3; end;
This is another use of SysFileTree. Instead of modified files in the top level directory and all subdirectories, this time we want just the subdirectories in the top level directory. This is controlled by the flags DO. D tells the routine to just list directories. As the S flag is not specified, only the target directory is scanned, not its subdirectories. O tells the routine to list just the fully qualified directory names and leave out the date, time and so on.
As before, the first parameter is the search template and the second is the name of the stem variable to use. When the routine completes, the number of lines returned will be in DirList.0 and each DirList.n will contain exactly one fully qualified directory name. We start the output by printing the headings as follows:
/* Display Headings */ if DirList.0 = 0 then say ' No Subdirectories'; else do; say 'Files Bytes Directory Name'; say '----- ----- --------------'; end;
Then we loop through the directories stored in DirList and call the GetDirectoryTotals routine for each one. Using the file and byte totals returned, a line is printed for the directory under the given headings. If no modified files are found, the directory line is skipped:
/* TOTAL FILES AND BYTES */ DO INDEX = 1 TO DIRLIST.0; CALL GETDIRECTORYTOTALS DIRLIST.INDEX'\'; IF TOTALFILES > 0 then say format(TotalFiles,5,0)' 'format(TotalBytes,8,0)' 'DirList.Index; end;
The GetDirectoryTotals routine has been covered previously in this section. Note the use of the format function, one of the standard REXX functions, to right align the numeric output. As used here, the numbers, the first parameter, are formatted into a field of the width given in the second parameter. The third parameter, a zero in our case, specifies the number of decimals. The numbers are right aligned in the given field and padded on the left with blanks.
Note that if subdirectories are present but no modified files are found in them, we could be left with a set of headings and no directory lines under them. We probably should fix this so subdirectory headings are only printed if a subdirectory with modified files is found. I'll leave this as an exercise for you.
When all the directory names have been processed, the loop exits, the stem variable is dropped and the routine returns to the mainline:
/* Return */ say; drop DirList; return 0;
The mainline finishes by displaying the operation completion message and terminating the program.
The backup functions, both the full and the modified, are much simpler to implement than the query function. This is because most of the work is done by the OS/2 BACKUP command. The bulk of the REXX code is involved in determining the correct parameters to use on that command. Let's look at this issue first.
There are two types of backup we are interested in. One backs up every file on to new diskettes using the A: drive. The other backs up modified files on to an existing set of backup disks. In both cases, all subdirectories of the target directory need to be processed as well.
If you are not familiar with the syntax and operations of the OS/2 BACKUP command, this would be a good time to review it. Check in your OS/2 Command Reference under OS/2 Commands by Name. To do the first type of backup, we would need to issue the following OS/2 BACKUP command:
BACKUP targetdirectory A: /S
To do the second, we need to add an additional two flags as follows:
BACKUP targetdirectory A: /S /A /M
Apart from this, the actual business end of the procedure is identical. The only other difference is we need to differentiate the operations in the information messages as described in External Design. This is a no-brainer as we display those messages in the mainline before invoking the actual backup routine as follows:
/* Backup All Files in Directories */ when (Operation = '/FULL') then do; say 'Backing up all of' DirPath; call BackupDirectory DirPath; say 'Backup All Files in Directories Complete'; end; /* Backup Modified Files in Directories */ when (Operation = '/MODIFIED') then do; say 'Backing up modifications to' DirPath; call BackupDirectory DirPath, '/A /M'; say 'Backup Modified Files in Directories Complete'; end;
As can be seen, this enables us to have the same routine, BackupDirectory, do both the types of backup. The additional BACKUP utility flags are passed in as parameters. First the appropriate type of message is issued and then the routine is called. For a full backup (/FULL), the target directory name, contained in the DirPath variable, is the only parameter. For the backup of modified files (/MODIFIED), the additional BACKUP utility flags, the string literal '/A /M', are passed as the second parameter. The routine code is:
/**************************************/ /* Backup Directory */ /**************************************/ BackupDirectory: procedure; /* Get Parameters */ parse arg DirPath, BkupOpts; /* Perform Backup */ say; 'BACKUP' DirPath 'A: /S' BkupOpts say; /* Return */ return;
The first thing we do on entry to the routine is get the parameters and parse them into two variables. Note the comma (,) between the DirPath and BkupOpts variables. This indicates there are two parameters to be dealt with. If the first comma was omitted, the first parameter string would be parsed into the two variables and the second parameter would be ignored.
As per the calling sequence, the first parameter was the target directory and which is now in the DirPath variable. The second is either omitted (blank) in the case of a full backup or the string '/A /M' for a backup of modified files. Either way, we construct the BACKUP command and execute it as shown.
We haven't bothered checking the return code in RC. We haven't supressed the normal BACKUP utility output and so our assumption can justifiably be that users can see for themselves. There are no subsequent operations that depend on this one.
Just as an aside, the way to suppress output from a command executed in this way is to redirect the standard output to the special device NUL. Note, only one L. For example:
ECHO This Message Won't Display >NUL
Try it, it works. Standard error output can be dealt with in the same way as follows:
[OS2] CHDIR Rumplestiltskin 2>NUL
After the completion of the backup routine, control passes back to the mainline. Here the appropriate completion message is issued and control drops through to the end.
Testing the Program
Testing is the set of actions we go through to ensure what we have written is correct. That may seem simple enough but there are usually problems agreeing on what we mean by correct. That is one argument I certainly don't want to get into here so I will propose a simplified definition which is usually quite sufficient for developing the odd REXX program.
Let me then define correct as that happy state where our program is producing all the stuff we said were requirements in the format we laid down during our external design and doing it without undesirable side effects or crashes. This includes handling unforeseeable situations gracefully.
I add one qualification to this and that is the words within reason. It would be nice to have a program that handles all situations gracefully including the outbreak of World War III. However, the effort to produce it will probably outweigh the benefit derived.
So know where to draw the line. Your program should be able to handle all daily situations and provide a catchall for any others. With these thoughts in mind, let's look at some of the tools at our disposal. These include the following:
- OS/2 Command Line
- REXXTRY Utility
- REXX Trace
Plus I have included a few tips and hints on how to proceed.
OS/2 Command Line
Most of the REXX programs I write are for the OS/2 Command Line (CMD.EXE) environment. Thus the command line itself is an essential development and debugging tool. It is good practice to check out all but the most trival commands on the command line before coding them into a REXX program. Doing this enables you:
- To check the syntax you are going to use. This is cheaper to do on a command line than when buried in a program.
- To see what output is returned in the various circumstances the command will be used. This is essential if you are going to process that output in any way. Remember, blank lines are still one line, that is one parse pull in your program. If you have any doubt about what is generated, redirect the output to a file and use your editor to check it.
- To debug any redirection, piping and other command line notation. The OS/2 command line is quite powerful with a number of different operations available on it. Some arrangements can be rather complex and are best debugged before they appear in a program.
The OS/2 Command Line is often overlooked as a development and debugging aid which is a mistake, particularly if you are developing for the command line environment.
Another useful tool, particularly if you are a casual REXX user, is REXXTRY. As the name implies, it enables REXX statements to be tried out before committing them to a program. One way of using it is to invoke it with the statements you want to test as parameters:
[OS2] REXXTRY Name = 'Nick'; say 'Hello' Name; Hello Nick ................................................ REXXTRY.CMD on OS/2 [OS2]
Another is to start it with no parameters and let it prompt you for statements. You can continue to enter statements until you terminate the utility by typing the REXX exit statement:
[OS2] REXXTRY REXXTRY.CMD lets you interactively try REXX statements. Each string is executed when you hit Enter. Enter 'call tell' for a description of the features. Go on - try a few... Enter 'exit' to end. Name = 'Nick'; ................................................ REXXTRY.CMD on OS/2 say 'Hello' Name; Hello Nick ................................................ REXXTRY.CMD on OS/2 exit; [OS2]
The advantage of this last approach is the environment is preserved so if you set a variable name in one line, you can use that variable in the next. With the single line invocation, everything has to be on the command line although you can enter multiple REXX statements separated by semicolons (;).
REXXTRY is very useful to check if a REXX statement or a group of statements is going to work as you intended. For example, I might have tested the concept of using the SysFileTree function to list all the directories within the target directory as follows:
[OS2] REXXTRY REXXTRY.CMD lets you interactively try REXX statements. Each string is executed when you hit Enter. Enter 'call tell' for a description of the features. Go on - try a few... Enter 'exit' to end. call RxFuncAdd 'SysLoadFuncs', 'RexxUtil', 'SysLoadFuncs'; ................................................ REXXTRY.CMD on OS/2 call SysLoadFuncs; ................................................ REXXTRY.CMD on OS/2 call SysFileTree 'D:\Workarea\*', 'DirList', 'DO'; ................................................ REXXTRY.CMD on OS/2 say DirList.0; 9 ................................................ REXXTRY.CMD on OS/2 do Index = 1 to DirList.0; say DirList.Index; end; D:\Workarea\ASSOED03 D:\Workarea\Fonts1 D:\Workarea\Fonts2 D:\Workarea\Fonts3 D:\Workarea\InstallJunk D:\Workarea\PMVIEW D:\Workarea\SendMail D:\Workarea\SendMailError D:\Workarea\WKSCSD1 ................................................ REXXTRY.CMD on OS/2 exit; [OS2]
Finally, REXXTRY is a REXX program itself. We haven't covered the PMREXX utility yet but we will in the Desktop Integration section later on. At this point, you should note that you can run a REXX procedure in a PM window using PMREXX and this includes REXXTRY. The advantage here is you're not limited to the size of the window for viewing the output as the PM window is scrollable.
The REXX trace facility is the major tool you will use to debug your programs. It is used to display the code being executed and the results of the execution.
The trace can be used interactively or in batch mode. In batch mode, the trace messages are written continuously to the standard error destination. The program does not halt except for normal execution stops, for example user input.
In interactive mode, the program halts after each line has been traced. This enables you to type in and execute REXX statements and host commands and examine variables, for example using the say statement. In addition, you can rectify errors or even set up errors you want to test for.
Normally, the trace output for batch mode is redirected to a file for examination after the program has finished executing. As the trace output is written to the standard error not the standard output, the normal program output is not suppressed. To start our sample program with standard output redirected, use something like this:
BackupManagement /LIST D:\Workarea 2>TraceFile.TXT
In the discussion that follows, I will show sample traces and debug sessions for our sample application and for the following test program:
/* Trace Test Code */ trace o; call SysFileTree 'D:\Workarea\*', 'DirList', 'DO'; do Index = 1 to DirList.0; say DirList.Index; end; exit;
I use this small sample rather than our main program for some of the traces as large programs can produce very large traces.
The REXX trace is started by the trace statement which has the following general syntax:
TRACE [?] keyword or TRACE number
This statement is placed in the program at the point at which the trace is to start. The keyword is used to specify the different types of trace. Only the first letter of the keyword is required. The question mark (?) is used before a particular keyword to set on the required trace in interactive mode. The following keywords or trace types are available:
This keyword causes all statements to be displayed as they are executed. The trace of each statement appears before the output that the statement generates. The following example shows our small test program with the trace statement set to trace a:
5 *-* Call SysFileTree 'D:\Workarea\*', 'DirList', 'DO'; 7 *-* Do Index = 1 To DirList.0; 8 *-* Say DirList.Index; D:\Workarea\ASSOED03 9 *-* End; 7 *-* Do Index = 1 To DirList.0; 8 *-* Say DirList.Index; D:\Workarea\Fonts1 9 *-* End; 7 *-* Do Index = 1 To DirList.0; 8 *-* Say DirList.Index; D:\Workarea\InstallJunk 9 *-* End; 7 *-* Do Index = 1 To DirList.0; 8 *-* Say DirList.Index; D:\Workarea\PMVIEW 9 *-* End; 7 *-* Do Index = 1 To DirList.0; 11 *-* Exit;
In summary, this trace gives us a simple list of each statement executed without displaying results or return codes. The interspersed directory names are the actual program output.
This keyword causes all host commands to be displayed as they are executed. The trace of the host command appears before the output that the command generates. Any non-zero return code, as set into the RC variable, is displayed. Our little test program doesn't issue any host environment commands but this is the result of running a query operation using the BackupManagement sample:
15 *-* '@ECHO OFF'; 15 >>> "@ECHO OFF" Manage Directory Backups Version 2.00 (C) Copyright N.S.McGuigan 1994-1995
293 *-* 'CHDIR' DirPath '2>&1 | RXQUEUE'; 293 >>> "CHDIR D:\Workarea 2>&1 | RXQUEUE" Directory D:\Workarea
Total Files: 122 Total Bytes: 2613167
Files Bytes Directory Name ----- ----- -------------- 5 104826 D:\Workarea\ASSOED03 101 1812912 D:\Workarea\Fonts1 11 121828 D:\Workarea\PMVIEW
Query Directories For Modified Files Complete
This type of trace is very useful for determining errors in your interactions which the host environment. As usual, the normal output is interspersed with the trace output on the display. The query operation only issues two OS/2 commands. These are the ECHO and CHDIR commands neither of which returned a non-zero return code.
The keyword does pretty much what the Command keyword does but lists only the host commands that return a non-zero return code. Unlike the Command keyword, the trace of the host command appears after the output that the command generates.
This keyword is similar to the Error keyword except it only traces host commands that raise the Failure condition. For a full explanation of conditions, see the OS/2 Procedures Language 2/REXX manual. In the case of a command line, this is most likely to happen if you try to execute an unknown program.
This keyword gives you the most detailed of all the trace information. It lists all statements being executed and all intermediate results obtained resolving the statement for execution. Here is an extract of the trace of our small sample program:
8 *-* Do Index = 1 To DirList.0; >V> "2" >>> "3" 9 *-* Say DirList.Index; >C> "DIRLIST.3" >V> "D:\Workarea\InstallJunk" >>> "D:\Workarea\InstallJunk" D:\Workarea\InstallJunk 10 *-* End;
This is just one iteration of the loop displaying the directory names from the stem variable, DirList. Under the do statement on line 8 are two numbers. The first represents the contents of variable Index before it is incremented, the other the result of the increment operation. Under statement 9, the first line shows the stem variable with the index resolved, underneath is the actual value of that stem variable entry and the last line is the value returned or result.
The Intermediate keyword is used for really in-depth debugging where, for example, you are getting strange strings being built and you can't see how. In these types of situation, bracket the problem area with trace statements setting the Intermediate trace on and then off again. You will still be focussed on the problem but will have a lot less guff to plough through.
The trace set by this keyword is very useful for getting an overview of what routines your program is running through as it performs its work. It traces the labels passed during execution. For example, here is a label trace of our sample program executing a query operation:
79 *-* DisplayBanner: Manage Directory Backups Version 2.00 (C) Copyright N.S.McGuigan 1994-1995 92 *-* InitialiseProgram: 114 *-* ValidateParameters: 288 *-* CheckDirectory: 307 *-* GetQueueData: Directory D:\Workarea 179 *-* QueryMainDirectory: 330 *-* GetDirectoryTotals: Total Files: 122 Total Bytes: 2613167 219 *-* QuerySubDirectories: Files Bytes Directory Name ----- ----- -------------- 330 *-* GetDirectoryTotals: 5 104826 D:\Workarea\ASSOED03 330 *-* GetDirectoryTotals: 101 1812912 D:\Workarea\Fonts1 330 *-* GetDirectoryTotals: 330 *-* GetDirectoryTotals: 11 121828 D:\Workarea\PMVIEW Query Directories For Modified Files Complete
This trace is particularly useful in interactive mode. As the trace halts at the start of each function, you can decide whether or not to do a more detailed trace of a particular function while still skipping over the others. When you get to the function concerned, issuing a trace i will get you the detailed trace of the function. When you exit from the function, the trace type will automatically revert to the original label trace.
Note that even though we are in interactive trace mode, we don't have to repeat the question mark (?) when we issue the trace i. Interactive mode remains on until we specifically set it off. We do this by using the question mark (?) again. The question mark is effectively a toggle. The first time it is used, it sets the interactive mode on. After that, it toggles it on and off each time it is used.
So if we had specified the question mark on the trace i statement, it would have set interactive mode off. However, when we exit the function, the trace type would have reverted to interactive again when it went back to a label trace.
This is quite a useful feature that allows us to mix interactive and batch mode tracing without losing control. The following example shows the start of the label trace for our sample program but this time, we execute it in interactive mode and issue a trace ?a at the entry to the DisplayBanner routine:
79 *-* DisplayBanner: +++ Interactive trace. "Trace Off" to end debug, ENTER to Continue. trace ?a 80 *-* Procedure; 82 *-* Say 'Manage Directory Backups'; Manage Directory Backups 83 *-* Say 'Version 2.00 (C) Copyright N.S.McGuigan 1994-1995'; Version 2.00 (C) Copyright N.S.McGuigan 1994-1995 84 *-* Say; 86 *-* Return 0; 92 *-* InitialiseProgram: +++ Interactive trace. "Trace Off" to end debug, ENTER to Continue.
As you can see, after we issue the trace ?a statement, the trace zips through batch style until it exits the DisplayBanner routine. At this point, the trace reverts to an interactive label trace which then halts at the next label, InitialiseProgram.
One final tip about interactive tracing. If you want to finish the program normally, use trace ?o which will set tracing off. If you want to terminate it right there, use the REXX exit statement.
This is the same as the Failure keyword.
This keyword switches the trace off. It can either be inserted in the program to turn tracing off once past a certain point or it can be issued during interactive tracing to run normally to the end of the routine. Note that setting the trace off while in a function only lasts until the end of the function. Then the trace reverts to whatever was active when the function was entered.
The trace enabled by this keyword is similar to the Intermediate keyword but only the final result of each statement execution is shown. I use this trace type for all my normal in-depth debugging. This is the same section shown under Intermediate above but using trace r instead:
8 *-* Do Index = 1 To DirList.0; >>> "3" 9 *-* Say DirList.Index; >>> "D:\Workarea\InstallJunk" D:\Workarea\InstallJunk 10 *-* End;
As described above, use of the question mark prefix toggles interactive trace mode on and off. Alternatively, interactive tracing can be turned off at any time by issuing trace o or just trace with no options at all.
The numeric options are only used during interactive mode. If a positive whole number or an expression that evaluates to one is specified, that number indicates the number of debug stops to be skipped. The trace output is produced but the program doesn't stop at a trace point until the number of trace points specified has been passed.
If the option is a negative whole number or an expression that evaluates to one, all tracing is temporarily switched off for the specified number of trace stops. This is similar to the case of positive numbers but no trace output is produced at all.
Tips and Hints
When testing, it pays to have some kind of plan or approach. With small programs, between 1 and twenty lines, this plan might be as simple as run until it crashes, fix the problem and try again.
With larger programs, this is not the best strategy. It pays to sit down and work up a set of test cases for a start. We should have test cases planned to test all combinations of parameters, the extent of ranges and so on.
In our sample program, we would want to test cases for each of the three operations and for when an invalid operation is specified. For each operation flag, we should test the shortest and longest abbreviations as well as some in the middle. We should also test for valid and invalid directories as well as special cases such a drive letters only (d:) or root directories (d:\ or \). I used the following test cases with the sample program:
BackupManagement /Help BackupManagement /H BackupManagement /He BackupManagement /Hlp BackupManagement /Helpa BackupManagement /? BackupManagement ? BackupManagement /List D:\Workarea BackupManagement /L D:\Workarea BackupManagement /Lis D:\Workarea BackupManagement /Lst D:\Workarea BackupManagement /Listi D:\Workarea BackupManagement /List BackupManagement /Full D:\Workarea BackupManagement /F D:\Workarea BackupManagement /Fu D:\Workarea BackupManagement /Fll D:\Workarea BackupManagement /Fully D:\Workarea BackupManagement /Full BackupManagement /Modified D:\Workarea BackupManagement /M D:\Workarea BackupManagement /Modif D:\Workarea BackupManagement /Modied D:\Workarea BackupManagement /Modifiedr D:\Workarea BackupManagement /Modified BackupManagement / D:\Workarea BackupManagement /Fred D:\Workarea BackupManagement /list D:\ BackupManagement /list D: BackupManagement /list \ BackupManagement /list D:\NotHere BackupManagement /list F: BackupManagement D:\Workarea BackupManagement /LIST D:\Workarea TooMuch
Within the program itself, list each of the functions together with the input from parameters and globals they are expecting and the output they return. During testing, verify that these inputs and results are achieved. In particular, check that the variables it expects to see are visible. A common problem when using procedure expose is to forget to expose a needed variable.
Don't use live data to test on unless it is used for reading only. Remember you are testing a program so by definition it doesn't work properly yet and can't be expected to treat your data nicely. Take a copy or an extract of the data you will eventually be using and test against that. Make sure your test data is complete enough to exercise all the test cases you have decided on.
It is okay to have a small REXX program to do the extract or copy to create test data as any such program will be using the real data as read only. Such a program is handy to have about in case your testing destroys the test data and it has to be recreated.
As already described above, check out your host commands on the OS/2 command line, validate your approachs to the solution as REXX code using REXXTRY and use traces to zero in on trouble spots. I often set the trace up for results during testing and redirect it to a file which I examine if I have a problem or discard otherwise. Having identified a trouble area, I might set the label trace on interactively. When I get to the function causing the program, I will switch to intermediates or results and have a detailed look at the function.
Finally, if you want to really get into your code, you can do an indepth validation of the finished product using the intermediate trace. Set the trace on in batch mode and run all your test cases while redirecting the trace output to different files, one per case. Then do a manual check to make sure everything happened exactly as you expected and that you can explain any differences and are happy with them. Use the trace to check off all lines of code you have executed during the tests. Whatever is left over, hasn't been tested and will need to be looked at.
This is way beyond what the average casual user needs to do to be confident in their work. However, if you are producing work for sale or deployment throughout an organisation, it is worth the extra time.
The final thing we need to consider is the required integration into the Workplace Shell environment. Now as the current REXX implementation doesn't enable us to write SOM objects let alone Workplace Shell objects, we shall have to make use of some other tools available to us. There are two of these that will help us here, the standard WPS program object and a utility supplied with OS/2 called PMREXX.EXE.
As we mentioned before, we can use drag and drop with program objects to specify parameters to the program object and invoke them. For example, if we setup a program object and drag a directory to it from any disk object view, the program object will be invoked with the dragged directory's name as a parameter (see the picture below).
This property will form the basis for our drag and drop integration. All we have to do is add the appropriate operation flag /LIST, /FULL or /MODIFIED to the parameters. We achieve this by creating a program object for each operation and entering the appropriate flag value in the parameter field for each. When we drag our directory to the program object, its name is added to the end of any parameters coded in this field. We can document this expectation by coding a %* value in the field as follows:
We don't really need the %* marker in this case. It tells the WPS where to put the dropped directory name. If omitted, it sticks it on the end of the parameters which is where we want it in this case. However, it is still useful to code the marker to document the fact that we expect the program object to be the target of a drop operation.
For more information about %* and other things you can specify in the parameter field of a program object, open the settings of any program object and press the Help pushbutton at the bottom of the first page. Follow the links referencing parameters.
Now we have sorted out how we can kick off the program using drag and drop, we need to work out how to use the PMREXX.EXE utility to display the program's output in a scrollable PM window. PMREXX.EXE is documented in the REXX Information online book in the information folder in your OS/2 system. Take a moment to review it now if you are not familiar with it.
To use PMREXX.EXE, we execute it instead of the REXX command and pass the name of the REXX command to it as a parameter. The REXX command then runs within the PMREXX environment with its output redirected to the PMREXX window and any input taken from a PMREXX entry field. For our program objects, this means the name in the Path and File Name field is now set to PMREXX.EXE and the Parameter field becomes:
BACKUPMANAGEMENT.CMD /LIST %*
for the List function with /FULL and /MODIFIED replacing /LIST for the other operations. The finishing touch is to design three icons for the program objects representing the three functions. These are my efforts:
To create an icon, open the settings for the program objects, go to the General tab (last page) and press the Create Another pushbutton to create a new icon or press the Edit pushbutton to modify the existing one. If you already have a suitable icon, perhaps one from the various collections of icons around the place, this can be dragged in from a folder and dropped on the current icon to replace it.
To test the program objects you have set up, open a disk object in either a tree, icon or detail view and drag a directory to each. PMREXX will start in each case and display the action in its window. Dismiss the procedure ended message when shown and then scroll through the results to check they are as expected. Press F3, File/Exit or double click the system menu icon to dispose of the evidence.
You can extend this technique to non-REXX utilites, for example PKUNZIP2, by writing a short REXX wrapper as follows:
/********************************************************************/ /* */ /* EXECUTE PROGRAM FOR PMREXX ENVIRONMENT */ /* */ /* Invokes the program and parameters specified in the */ /* parameter list. This wrapper command file enables such */ /* programs to be run in the PMREXX environment. */ /* */ /* Version 1.10 (C) Copyright N.S.McGuigan 1994-1995 */ /* All Rights Reserved */ /* */ /********************************************************************/ trace o; '@ECHO OFF' /* Get Program Name and Parameters */ parse arg Filename Parms; /* Set Current Directory to Program Directory */ Drive = filespec('Drive',Filename); Path = filespec('Path',Filename); Program = filespec('Name',Filename); call directory Drive || Path; /* Add Program Directory to Path */ ProgPath = value('PATH',,'OS2ENVIRONMENT'); call value 'PATH','D:\Packages\System\Data;'ProgPath,'OS2ENVIRONMENT'; /* Execute Program With Parameters */ Program Parms /* Exit Program */ exit rc;
I have removed a few lines of code for clarity, for example no help. To use this command file, set up a program object with the Path and File Name as PMREXX.EXE and the Parameters as follows:
d:\cmdpath\EXECPROG.CMD d:\prgpath\program.exe parameters
You can use the %* value to specify where the target file name is to appear. Don't forget to create an icon to distinguish your object. Using the PKUNZIP2 example, the parameter line would look something like this:
d:cmdpath\EXECPROG.CMD PKUNZIP2.EXE %*
Another way you can integrate programs like these into the desktop environment is to associate certain data files with them using Associations. For example, we can associate all files with ZIP extensions with our PKUNZIP2 program object. These files are then shown with the same icon as the program object and, when double clicked to open them, will automatically start that program object.
To add associations to a program object, open its settings and select the tab marked Associations. You can select from the predefined types and press the Add pushbutton or you can type a file mask and press the other Add pushbutton. Using our PKUNZIP2 example, this is what the Associations page looks like after adding an association with *.ZIP files:
For more information about associations, press the Help pushbutton on the Association page of any program object's settings notebook.
That just about wraps it up for the BackupManagement REXX utility. You can download the source code and play around with it or use it as it is.