SSG Tips & Techniques

Idioms & Tricks


How to tell what SSG release you're running

If you write skeletons that run on various 2200 machines, it may be necessary to do different things depending on what release of SSG is in use. Beginning with release 24R3, there is a built-in, read-only, numeric variable called FEATURE_LEVEL$ that Unisys will increment each time they release SSG with new features. It is set to 1 for release 24R3.

Unfortunately, for releases below 24R3 there is no systematic way to tell from within a skeleton what SSG features are available. However, because SSG has added system-defined SGSs (or new fields on previously existing SGSs) or system-defined variables over the years, you can often use these to determine what older release of SSG is in use:

*IF +FEATURE_LEVEL$  =  1                               . if SSG 24R3
*  DISPLAY '24R3'
*.
*ELSEIF [SYSTEM$,1,3,2]                                 . if SSG 24R2 or higher
*  DISPLAY '24R2 or higher'
*.
*ELSEIF NEW_BOOLEAN$ IS CLEAR  OR  NEW_BOOLEAN$ IS SET  . if SSG 24R1 or higher
*  DISPLAY '24R1 or higher'
*.
*ELSEIF [SYSTEM$,1,4,1]                                 . if SSG 23R8 or higher
*  DISPLAY '23R8 or higher'
*.
*ELSEIF [INFO$,1,4,2]                                   . if SSG 23R7 or higher
*  DISPLAY '23R7 or higher'
*.
*ELSEIF [SYSTEM$,1,2,2]                                 . if SSG 23R6 or higher
*  DISPLAY '23R6 or higher'
*.
*ELSEIF [INFO$,1,7,1]                                   . if SSG 23R5 or higher
*  DISPLAY '23R5 or higher'
*.
*ELSEIF [INFO$,1,3,3]                                   . if SSG 23R3 or higher
*  DISPLAY '23R3 or higher'
*.
*ELSEIF [SUPS$,1,7,1]                                   . if SSG 23R2 or higher
*  DISPLAY '23R2 or higher'
*.
*ELSEIF [DATE$]                                         . if SSG 23R1 or higher
*  DISPLAY '23R1 or higher'
*.
*ELSE
*  DISPLAY 'Lower than 23R1'
*.
*ENDIF


How to tell if you're on an XPA machine

If you write skeletons that run on various 2200 machines, it may be necessary to do different things depending on whether or not your skeleton is running on an XPA (eXtended Processing Architecture) machine. You could maintain a set of SGSs listing each machine type ('2200/400', '2200/500', etc.) and telling whether or not it's an XPA machine. Or you could take advantage of the fact that Core Block SUPS will also be zero on an XPA machine:
     *IF [SUPS$,1,6,1] = 000000000000    . If no Core Block SUPS (i.e., if XPA system)
     *.  Do exciting M-Series things
     *ELSE
     *.  Do boring C-series things
     *ENDIF


How to calculate day of the week for any date between 1964 and 2099

Unisys supplies a *COPY proc for calculating the day of the week for a given date (see Appendix C of the SSG Programmer Reference Manual. Unfortunately, this routine has three faults: First, it fails in the year 2000 (see PLE # 16918521). Second, it only accepts six-digit dates (format YYMMDD) as input. Third, it only works with NEW_PROCESS$ set.

To correct these three faults, I wrote my own version of this proc. It provides the day of the week for any eight-digit input date (format YYYYMMDD) between 1964 and 2099 or for any six-digit input date (format YYMMDD) between 1964 and 2027 (it uses the Unisys Standard of 2027 to convert YYMMDD to YYYYMMDD).

Unisys has corrected the year 2000 bug in their proc beginning with the SSG 23R2 release. But the other two problems remain, which is fair because it's only demonstration code.


How to fake an array -- first method

SSG does not supports arrays. But you'll often want to use an array to simplify your code. For example, suppose you must report on security violations by month. Without arrays you might code something like:

     *CLEAR JAN_CNT                          . counts security violations for January
     *CLEAR FEB_CNT                          . counts security violations for February
     ... (nine *CLEAR directives omitted here)
     *CLEAR DEC_CNT                          . counts security violations for December
     *.
     *INCREMENT V TO [VIOLATION]             . for each security violation
     *IF [VIOLATION,V,V_DATE,1,SUBSTR$,5,2]  =  01      . if violation occurred in January
     *SET JAN_CNT = JAN_CNT + 1
     *ELSEIF [VIOLATION,V,V_DATE,1,SUBSTR$,5,2]  =  02  . if violation occurred in February
     *SET FEB_CNT = FEB_CNT + 1
     ... (ten *ELSEIF and *SET directives omitted here)
     *ENDIF
     *LOOP

You'll wind up with one mother of a *IF statement. A more compact method is to fake an array by adjusting the variable name based on the month number

     *INCREMENT M TO 12                             . there are 12 months
     *CLEAR MONTH_CNT_[*M]     ...                  . counts security violations for month M
     *LOOP
     *.
     *INCREMENT V TO [VIOLATION]                    . for each security violation
     *SET MX = +[VIOLATION,V,V_DATE,1,SUBSTR$,5,2]  . convert month to integer (no leading zero)
     *SET MONTH_CNT_[*MX] = MONTH_CNT_[*MX] + 1
     *LOOP


How to fake an array -- second (better) method

The previous method for faking an array works fine when the number of 'bins' into which you must count items is fixed and known (e.g., there are only 12 months). But frequently the number of 'bins' is unknown or varies over time. For example, we might need to count the number of security violations by user but the population of users grows and shrinks without notice. This can be handled by using an SGS:

     *REMOVE SGS BADUSER,1,[BADUSER]         . just to be safe!
     *.
     *INCREMENT V TO [VIOLATION]             . for each security violation
     *IF COLUMN SEARCH FROM BADUSER,1,1,1 FOR [VIOLATION,V,V_USERID,1]  . if not first for user
     *SET TOT = [BADUSER,STMT$,2,1] + 1      . increment the user's violation count
     *REMOVE SGS BADUSER,STMT$,1             . remove the user's SGS
     *ELSE
     *SET TOT = 1                            . first violation for this user
     *ENDIF
     *CREATE SGS:  BADUSER  [VIOLATION,V,V_USERID,1]  [*TOT]
     *LOOP

This code will create one BADUSER SGS for each user who committed a security violation. The first field will contain the user-id; the second field will contain the number of security violations committed by that user.

I find this method to be more flexible and straighforward than the other method.


How to justify *DISPLAYed output

The *DISPLAY directive provides little facility for formatting printed output. But often you'll want to print a professional looking report. To report on the security violations by user from the previous example, you might code:

     *DISPLAY '  User-id       Count'             . report heading
     *.
     *INCREMENT B TO [BADUSER]                    . for each user who commited a violation
     *DISPLAY '[BADUSER,B,1,1]     [BADUSER,B,2,1]'
     *LOOP

This will get the job done but will look ugly, for two reasons. First, unless all user-ids are the same length, the second (security violation count) field will start in different columns on different lines, leading to a ragged look. We can fix this very simply:

     *DISPLAY '  User-id       Count'             . report heading
     *.
     *INCREMENT B TO [BADUSER]                    . for each user who commited a violation
     *DISPLAY '[BADUSER,B,1,1,LSTR$,12]     [BADUSER,B,2,1]'
     *LOOP

The maximum length of the user-id field is 12 characters. The LSTR$ (left-string) reference will force SSG to display all occurrences of this field as 12 characters (it will pad with trailing spaces where necessary). This removes the ragged look from the report.

But the second problem with this report remains: The count field will be left-justified and it's more traditional to right-justify numeric fields. We can do this by:

     *DISPLAY '  User-id       Count'             . report heading
     *SET BLANKS = '         '                    . constant
     *.
     *INCREMENT B TO [BADUSER]                    . for each user who commited a violation
     *SET OUT_CNT = '[*BLANKS][BADUSER,B,2,1]'    . pad the count with leading blanks
     *DISPLAY '[BADUSER,B,1,1,LSTR$,12]     [*OUT_CNT,RSTR$,8]'
     *LOOP

I think Arnold Cameron first showed me this trick. RSTR$ (right-string) does not pad with leading spaces if the field has fewer characters than are specified. So we must first pad the number with at least n leading spaces and then to use RSTR$ to select the right-most n characters (where n is the maximum number of digits necessary to show the variable's greatest possible value). In the above example, I was very paranoid and chose eight digits.


How to vary the device on *ACCEPT and *DISPLAY

Often you want to *ACCEPT and *DISPLAY on the console if the skeleton is run in batch but on the demand terminal if the skeleton is run in demand. Even if your skeleton is only used in batch with operator input, this saves you from going to the console to test it. The 'O' option on these directives specifies the SSG should communicate with the console. So you might be tempted to code, for example:

     *IF [INFO$,1,5,1] = DEMAND                   . if demand
     *ACCEPT USERID 'Please enter user-id'
     *ELSE                                        . batch or deadline batch
     *ACCEPT,O USERID 'Please enter user-id'
     *ENDIF

This will lead to overly long skeletons and to maintenance headaches (you'll need to change two messages rather than one). A better method is to set a variable with the *ACCEPT/*DISPLAY option at the beginning of skeleton execution

*IF [INFO$,1,5,1] = DEMAND *SET DEST = 'Z' . Dummy value *ELSE *SET DEST = 'O' . Console operator *ENDIF

In this case I've chosen the variable name DEST (short for destination) but any fairly short variable name will do. I use this so often that I wrote a *COPY proc for it. Then later in the skeleton you can code, for example:

     *ACCEPT,[*DEST] USERID 'Please enter user-id'

Strictly speaking, this method is probably syntactically illegal: SSG does not provide a *ACCEPT/*DISPLAY option letter to specify that the input and output uses CARD$ and PRINT$. Instead, the lack of an option letter indicates this. So '*ACCEPT,Z' is not strictly legal. But if a new release ever flagged this as an error it'd break a million skeletons and Unisys would have to back off. This technique is far too valuable to worry about strict syntactical legality. (Unisys could make this legal by choosing an option letter and reserving the letter for this purpose. No code change would be necessary, only a manual update.)


How to *REMOVE some SGSs from a set

Assume you've got a set of SGSs describing disk packs and that the equipment type is the fourth field. How do you remove all SGSs for packs with equipment type FMD? You might try:

     *INCREMENT P TO [PACK]                   . for each pack
     *IF [PACK,P,4,1]  =  FMD                 . if it's equipment type FMD
     *REMOVE SGS PACK,P,1                     .  get rid of it
     *ENDIF
     *LOOP

Alas, this fails because the value of loop index (variable P) is not adjusted when a *REMOVE is done. If, for example, you *REMOVE the 7th occurrence, the 8th occurrence would now become the 7th. But the next time thru the loop P would be 8, so you'd miss testing the SGS that follows one you *REMOVE. You could code the adjustment to the index but it'd be tricky.

A better approach is to process the SGSs in reverse order:

     *INCREMENT P FROM [PACK] TO 1 BY -1      . for each pack in reverse order
     *IF [PACK,P,4,1]  =  FMD                 . if it's equipment type FMD
     *REMOVE SGS PACK,P,1                     .  get rid of it
     *ENDIF
     *LOOP

This always works because the occurrences that are being adjusted in response to *REMOVE are in the SGSs that we've already tested.



Revised 2016-03-24