MPE XL PROGRAMMING
by Eugene Volokh, VESOFT
Presented at 1988 INTEREX Conference, Orlando, FL, USA
Published by HP PROFESSIONAL Magazine, Aug-Oct 1988.
Published in "Thoughts & Discourses on HP3000 Software", 4th ed.
ABSTRACT.
In 1983, I wrote a paper called "MPE PROGRAMMING" (presented at the
INTEREX Montreal conference), which showed how you could do some
remarkable things with MPE alone, without the aid of a custom-written
program. MPE Programming was the art of writing system programs
entirely in the "language" of CI commands (possibly with some help
from standard, HP-supplied utilities).
The main advantages of MPE Programming were ease of writing and
ease of maintenance. The idea was that a couple of dozen MPE commands
in a job stream were easier to deal with than a custom-made SPL or
COBOL program, especially since when you write a program, you'll have
to always keep track not just of the job stream, but also the
program's source and object files. UNIX, incidentally, has a very
powerful "Command Interpreter Programming" facility (such programs are
called "shell scripts"); UNIX users often write very many shell
scripts to do things that would otherwise require some rather
cumbersome C or PASCAL system programs.
Unfortunately, MPE V (and earlier MPE versions) were not really
designed for any sort of sophisticated MPE programming. Many of the
tricks I showed in my original paper bordered, I must admit, on the
perverse. For instance, to find out if you're in job mode or session
mode (without writing a program that calls WHO), I suggested that you
execute the :RESUME command.
Why the :RESUME command, of all things? Well (almost by accident),
the :RESUME command returns one error condition if done in a job and
another if done in a session (but not in break). We could then
completely ignore the actual function of the :RESUME command, and look
only at its "side effect" -- the value of the CIERROR JCW, which told
us whether we were in a job or session.
Similarly, to see if a file existed, we'd do a :LISTF ;$NULL of it.
This was not because we wanted to see information about this file (if
we did, we wouldn't put on the ;$NULL) -- rather, we wanted to see if
the :LISTF succeeded or failed. If it failed with a CIERROR 907, this
meant that the file didn't exist -- if it succeeded, the file did
exist.
MPE XL was intended to make many of these things a lot simpler to
do -- instead of weird, indirect techniques, mechanisms would be
provided for easily getting environment information (your logon mode,
etc.), file information (does a file exist?), and so on. Seemingly
using UNIX as a prototype (in spirit if not always in detail), MPE XL
sought to make MPE Programming a straightforward proposition.
To a large extent, HP succeeded -- MPE XL has a number of new
commands and features that let you do much more powerful things from
the Command Interpreter. In some ways, though, some of the features
seem at first glance to be more powerful than they really are, and
quite a few things that you'd like to do remain tantalizingly out of
your reach.
In the process of converting my MPEX/3000 and SECURITY/3000
products to MPE XL -- and in the process of implementing most of the
MPE XL user interface features in the MPE V version of MPEX (and in
SECURITY/3000's STREAMX module), usable by "classic HP3000" users --
I learned a good deal about the new MPE XL features, their strengths
and their weaknesses. This paper will try to objectively discuss both;
to show you how to use the strengths to their utmost and how to work
around some of the weaknesses.
THE NEW FEATURES OF MPE XL
What exactly are the new MPE programming-related features of MPE
XL? There are several:
* First of all, MPE XL supports VARIABLES. Think of them as JCWs
that can have string values as well as integer values. (Actually,
they can have boolean and 32-bit integer values, too.) E.g.
:SETVAR FNAME "FOO.DATA.PROD"
* MPE XL PREDEFINES some variables to values such as your user
name, your account name, your capabilities, etc. For instance,
:SHOWVAR @
HPACCOUNT = VESOFT
HPDATEF = TUE, FEB 9, 1988
HPGROUP = DEV
HPINPRI = 8
HPINTERACTIVE = TRUE
HPJOBCOUNT = 2
HPJOBLIMIT = 2
HPJOBFENCE = 7
HPJOBNAME = EUGENE
HPJOBNUM = 268
HPJOBTYPE = S
HPLDEVIN = 20
...
(Don't you wish you'd had this all along???)
* MPE XL lets you SUBSTITUTE the values of variables (and even
EXPRESSIONS involving the variables) into MPE commands -- just as
you could always substitute the values of UDC parameters. For
example,
:SETVAR FNAME "FOO.DATA.PROD" :PURGE !FNAME
is equivalent to
:PURGE FOO.DATA.PROD
Then you could also say
:BUILD !FNAME;DISC=![100*NUMUSERS+25];REC=-64,,F,ASCII
it will build a new FOO.DATA.PROD file with room for
100*NUMUSERS+25 records (presumably NUMUSERS is an integer
variable previously set with a :SETVAR).
* As shown in the above example, MPE XL lets you use EXPRESSIONS in
variable substitution, in the :SETVAR command, in the :IF
command, and in the new :WHILE and :CALC commands. A few examples
are:
:SETVAR EXPECTEDFLIMIT 100*NUMUSERS+25
:SETVAR FNAME "S"+MODULENAME+".PUB.SYS"
:SETVAR MODULENAME STR(FNAME,2,POS(".",FNAME)-2)
:IF HPACCOUNT<>"SYS" THEN
:IF POS("SM",HPUSERCAPF)=0 THEN << user doesn't have SM >>
As you can see, the expressions can involve either numbers or
strings, and a number of useful string operators have been
defined, such as:
+ to concatenate strings;
STR to extract substrings;
POS to find the position of one string in another;
UPS to upshift a string;
and many others.
* Perhaps the most useful of the defined operators is FINFO, which
takes a filename and an option number and returns a piece of
information about that file:
FINFO(filename,0) = TRUE if file exists, FALSE if it doesn't
FINFO(filename,1) = string with fully-qualified filename
FINFO(filename,4) = string containing file's creator
FINFO(filename,8) = file's creation date, formatted string
FINFO(filename,-8) = file's creation date, integer format
FINFO(filename,9) = file's string filecode (e.g. "EDTCT")
FINFO(filename,-9) = file's integer filecode (e.g. 1052)
and much more.
For example, to check if a file exists, you can say
:IF FINFO('MYFILE',0) THEN
To check if a file is more than 90% full, you might enter
:IF FINFO('MYFILE',19)>=FINFO('MYFILE',12)*9/10 THEN
FINFO mode 19 gets you the EOF; FINFO mode 12 gets you the
FLIMIT. (The mode numbers are taken from the FLABELINFO intrinsic
-- one of the weaknesses of FINFO is that you have to remember
these silly item numbers.)
* Commands have been added to OUTPUT and INPUT data:
:ECHO NOW WE'LL ASK YOU FOR A FILENAME.
:INPUT FNAME; PROMPT="Please enter the filename: "
:ECHO FNAME = !FNAME, FLIMIT = ![FINFO(FNAME,12)]
The :INPUT command can even have a timeout (wait for no more than
X seconds) option.
* In addition to MPE V control structures like :IF, :ELSE, and
:ENDIF, MPE XL implements the :WHILE / :ENDWHILE construct, e.g.
:SETJCW I = 295
:WHILE I < 314
: ABORTJOB #J!I
: SETJCW I = I+1
:ENDWHILE
* Instead of setting up UDCs, you can set up COMMAND FILES. If you
want to define a command called S that does a :SHOWJOB, you can
build a file called S.PUB.SYS that contains the lines:
PARM WHAT=" "
SHOWJOB JOB=@!WHAT
Now, whenever you type
:S J
(for example), MPE XL will execute the file S.PUB.SYS passing "J"
to it as a parameter. Same as a UDC, but no need to :SETCATALOG.
* Actually, whenever you type a command (like S in the example
above) that isn't a normal MPE command, MPE XL doesn't just check
for it in PUB.SYS. It instead looks at the variable (remember
those?) called HPPATH, and tries to find the file in the groups
listed in the variable.
By default, HPPATH is set to
!HPGROUP,PUB,PUB.SYS
This means "first look in !HPGROUP (i.e. your group), then in the
PUB group (of your own account), and then in PUB.SYS". You can
change HPPATH to tell MPE XL to look in UTIL.SYS, PUB.VESOFT,
PUB.TELESUP, or what have you.
Note that UDCs and built-in MPE commands take precedence -- the
HPPATH groups are searched only if the command you typed isn't a
UDC or a built-in MPE command.
* In addition to letting you execute command files by just entering
their names, you can also run a program just by entering its name
(IMPLIED RUN). If you say
:SPOOK
MPE XL will search the groups specified in HPPATH -- if the first
file it finds is SPOOK.PUB.SYS (a program file), it'll run it
just as if you'd said
:RUN SPOOK.PUB.SYS
Similarly, to run a program in your own group, you can just say
:MYPROG
and MPE XL will automatically supply the :RUN (remember, MPE XL
will look in HPPATH to determine which groups it should search --
by default, your group is one of them). If you say
:MYPROG "BANANA",5
it'll run MYPROG with INFO="BANANA" and PARM=5 (other :RUN
command parameters are not available). The quotes around BANANA
can be omitted, but only if the INFO= string doesn't include
commas, semicolons, equal signs, or blanks!
* Finally, a few odds and ends:
- The :CALC command works as a general-purpose integer and string
calculator.
- The :RETURN command lets you easily exit a UDC or a command
file.
- Users can now redefine their own prompt by setting the HPPROMPT
variable.
- :SETCATALOG lets you add a new UDC file (or remove one) without
retyping the names of all the other UDC files (which is
cumbersome and risks accidentally unsetting an important file).
- You can :REDO not just the last command, but one of the last 20
commands (or even more than 20 if you so choose). This is
actually a very powerful tool -- I'm only including it in "odds
and ends" because it's not directly relevant to MPE XL
programming.
These are the features -- what are the benefits?
THINGS THAT ARE NOW EASY TO DO
#1. ENVIRONMENT VARIABLES
One example in my original "MPE Programming" paper involved a UDC
finding out whether it's being executed in a session or in a job. This
might, for instance, be a logon UDC that you use to set your function
keys -- it outputs a whole bunch of escape sequences, which you want
to see when you're online, but which will only garble your printout if
printed in a job.
In MPE V, if you recall, checking job/session mode was done this
way:
SOFTKEYSINIT << the logon UDC name >>
OPTION LOGON
SETJCW CIERROR=0
CONTINUE
RESUME
IF CIERROR<>978 THEN
<< initialize the softkeys >>
ENDIF
Very straightforward, isn't it? The :RESUME command, of course, is not
used for :RESUMEing at all; rather, we count on it to generate an
error condition -- error 978 if in batch, but a different error
(warning 1686) if online.
MPE XL makes this laughably simple:
SOFTKEYSINIT
OPTION LOGON
IF HPINTERACTIVE THEN
<< initialize the softkeys >>
ENDIF
Essentially, MPE XL automatically presets some variables to
interesting values -- HPINTERACTIVE, HPLDEVIN (your terminal number),
HPUSER (your logon user id), etc. This process actually started in MPE
V with the HPYEAR, HPMONTH, HPDATE, HPDAY, HPHOUR, and HPMINUTE JCWs,
but MPE XL has added a lot of new and useful ones.
Some more practical applications are readily apparent and others
(the best kind) aren't. For instance, a really nice typing-saver is:
:NEWUSER JACK;CAP=!HPUSERCAPF
"HPUSERCAPF" stands for "USER CAPabilities, Formatted". It's a
STRING variable that indicates which capabilities you currently have,
e.g. "AM,AL,GL,ND,SF,PH,DS,IA,BA". The "!" before the "HPUSERCAPF"
works much as it would before a UDC parameter -- it tells MPE to
substitute in the VALUE of the HPUSERCAPF variable in place of its
name.
Thus, the command might end up being:
:NEWUSER JACK;CAP=AM,AL,GL,ND,SF,PH,DS,IA,BA
You didn't have to type in all of those capabilities -- the
!HPUSERCAPF automatically put in all the ones you have.
You might even say
:NEWUSER JACK;CAP=![HPUSERCAPF-"AM,"]
Saying ![xxx] tells MPE: "Evaluate the expression xxx and substitute
in its result". Subtracting two strings in MPE XL removes the first
occurrence of the second string from the first -- thus, the :NEWUSER
command will become
:NEWUSER JACK;CAP=AL,GL,ND,SF,PH,DS,IA,BA
(since "AM,AL,GL,ND,SF,PH,DS,IA,BA"-"AM," is "AL,GL,...,BA").
Another nice example is:
:FILE SYSLIST=BK!HPYEAR!HPMONTH!HPDATE,NEW;DEV=DISC;SAVE
:STORE @.@.@; *T
This will do a system backup and send the listing to a disc file
IDENTIFIED BY THE BACKUP DATE. Thus, you can keep many of your backup
listings online (so you could easily tell which tape set and reel
number a file was on); each one will be stored in its own file. For
instance, on 20 November 1988, the above commands will be executed as:
:FILE SYSLIST=BK881120,NEW;DEV=DISC;SAVE
:STORE @.@.@; *T
Unfortunately, it's not quite this simple. (Almost, but not quite.)
What if we do the :FILE SYSLIST= on the 21st of January? Then, we'd
get
:FILE SYSLIST=BK88121;...
-- not quite what we want, since it could easily stand for the 1st of
December. We'd like the month and day to be zero-padded, so that the
file names will be more comprehensible and a :LISTF will show them in
the right order (i.e. not show BK88121 after BK881105). How can we do
this? Well, how about
:FILE SYSLIST=BK![10000*HPYEAR+100*HPMONTH+HPDATE];...
Instead of substituting the month and the day in directly, we
calculate the value 10000*HPYEAR+100*HPMONTH+HPDATE. Since this is
arithmetic, not textual substitution, "zero-padding" will occur -- the
21st of January of 1988 will yield 880121, the 9th of April of 1988
will yield 880409. Then, we textually substitute the resulting value
into the :FILE equation:
:FILE SYSLIST=BK880121;...
Even the additional power of MPE XL doesn't remove the need for a
little ingenuity.
Finally, one more useful little UDC:
HIPRI JOBNUM
ALTJOB #J!JOBNUM;INPRI=14
SETVAR OLDJOBLIMIT HPJOBLIMIT
LIMIT ![HPJOBCOUNT+1]
LIMIT !OLDJOBLIMIT
DELETEVAR OLDJOBLIMIT
Three guesses as to what this does? Give up? Well, you :STREAM a
job and find it at the bottom of the WAIT queue; you want it to
execute, but you don't want to let any of the other WAITing jobs
through.
This UDC:
* Alters the job to input priority 14 (the highest priority
possible).
* Saves the old job limit (indicated by the built-in variable
HPJOBLIMIT) in an MPE XL variable (OLDJOBLIMIT).
* Sets the job limit to HPJOBCOUNT -- the number of currently
executing jobs -- plus 1, thus letting the topmost WAITing job
(the one you just :ALTJOBed) through.
* Sets the job limit back to what it was before.
* Just for cleanliness, deletes the OLDJOBLIMIT variable.
Voila! The one problem I can see is that the UDC expects only a job
NUMBER, not the leading "#J" -- if a user types
HIPRI #J123
then the very first line will be
ALTJOB #J#J123;INPRI=14
-- MPE won't like this much. We'd like to let the user type either
HIPRI 123
or
HIPRI #J123
whichever he prefers.
The solution is again fairly simple, taking advantage of MPE XL's
provisions for strings and for string operators:
HIPRI JOBNUM
IF UPS(LFT("!JOBNUM",2))="#J" THEN
ALTJOB !JOBNUM;INPRI=14
ELSE
ALTJOB #J!JOBNUM;INPRI=14
ENDIF
SETVAR OLDJOBLIMIT HPJOBLIMIT
LIMIT ![HPJOBCOUNT+1]
LIMIT !OLDJOBLIMIT
DELETEVAR OLDJOBLIMIT
The key here is the :IF expression -- it extracts the leftmost 2
characters of the string containing JOBNUM (LFT("!JOBNUM",2)),
upshifts them (UPS(LFT("!JOBNUM",2))), and then compares them against
"#J". If the characters are equal to "#J", then we just do an :ALTJOB
!JOBNUM; if the characters are something else (presumably the start of
the job number), then we insert a #J in front of them.
#2. FILE INFORMATION
One of the most valuable new features of the MPE XL CI is the
ability to obtain FILE INFORMATION. Remember the old MPE trick of
finding out if a file exists or not?
SETJCW CIERROR=0
CONTINUE
LISTF MYFILE;$NULL
IF CIERROR=907 THEN
<< file doesn't exist >>
ELSE
<< file exists >>
ENDIF
Again, what we're doing here is executing a command (:LISTF) not for
its main purpose, but rather for a side effect -- if we give :LISTF a
file that doesn't exist, it'll set the CIERROR JCW to 907; if the file
exists, CIERROR will remain 0.
MPE XL is much more straightforward:
IF FINFO('MYFILE',0) THEN
<< file exists >>
ELSE
<< file doesn't exist >>
ENDIF
The FINFO function returns information about the file whose name is
passed as the first parameter. The second parameter tells FINFO which
information is to be gotten; 0 means a TRUE/FALSE flag indicating
whether or not the file exists. Other values ask for other things,
such as file code, EOF, FLIMIT, etc.
Applications for this abound. For instance, your job stream might
rename a file while preserving its lockword:
:RENAME OLDFILE/![FINFO('OLDFILE',33)],NEWFILE/![FINFO('OLDFILE',33)]
Similarly, a command like:
:IF FINFO('AP010S',-8)>FINFO('AP010P',-8) OR &
: FINFO('AP010S',-8)=FINFO('AP010P',-8) AND &
: FINFO('AP010S',-24)>FINFO('AP010P',-24) THEN
would check to see if AP010S was modified after AP010P -- if AP010S is
the source file and AP010P is the program, you might want to trigger
an automatic recompilation. Note how we're comparing FINFO (-8)s [the
last modify dates, expressed as YYYYMMDD integers] of the source and
the program; if the modify date of the source is greater, the
expression yields TRUE -- if the modify dates are equal, we then
compare FINFO (-24)s [the last modify times, expressed as HHMMSS
integers].
At first glance, one of the most powerful applications of FINFO
would seem to be something like this:
:IF FINFO('DATAFILE',19) > FINFO('DATAFILE',12)-100 THEN
: TELLOP File DATAFILE is &
: ![FINFO('DATAFILE',19)*100/FINFO('DATAFILE',12)]% full!
:ELSE
...
FINFO(xxx,19) returns xxx's EOF; FINFO(xxx,12) returns xxx's FLIMIT;
if EOF > FLIMIT-100, we send a message to the operator indicating how
full the file is (again, the wonders of expression substitution).
This would be very useful on an MPE/V system, where file overflows
are a real concern; however, on MPE/XL, files can be built with a very
high file limit without wasting much disc space. Thus, MPE/XL users
rarely need to worry about file EOFs and FLIMITs.
However, we might still want to, say, compare the number of entries
in an IMAGE dataset against its capacity; unfortunately, there's no
FINFO option that gets us this information.
There are, in fact, two pretty serious problems with FINFO:
* For one, there are still a number of things that FINFO just
doesn't provide. To name a few:
- The NUMBER OF SECTORS in a file. I found myself wanting to
write a command file that compared the number of sectors a
file occupied before and after a certain operation, but
there was no way of getting this information.
- The file's LAST ACCESS DATE/TIME and LAST RESTORE DATE/TIME
(FINFO gives us the creation date and the last modify date,
but not the last access date or the last restore date).
- The file's security information -- :RELEASEd/:SECUREd flag,
security matrix, etc. It would be quite nice, for instance,
to check the access you're allowed to a file before running
a program that might abort quite bizzarely if it isn't given
the access it wants.
- Whether or not the file is currently IN USE (and if it is,
in what mode).
- The NUMBER OF EXTENTS in a file, the number of user labels,
and others (IMAGE dataset information, etc.).
In fact, if you look at the FINFO option numbers, you'll find
that they're pretty much a subset of the option numbers of the
FLABELINFO intrinsic, which also lets you obtain file
information. Why a subset? Why not just implement all the
FLABELINFO options (though even that would still leave some
options out).
All the file attributes -- certainly all those listable with
:LISTF ,2 and MPE XL's new :LISTF ,3 -- should be easily
obtainable from the CI.
* Perhaps more important than the omitted functions is the fact
that
ALL THE FINFO OPTIONS ARE "MAGIC NUMBERS".
When you saw the command
:IF FINFO('DATAFILE',19) > FINFO('DATAFILE',12)-100 THEN
was it clear to you what FINFO(xxx,19) and FINFO(xxx,12) did? If
HP is going to implement file access functions, why not have an
FFLIMIT('DATAFILE'), an FEOF('DATAFILE'), an
FFILECODE('DATAFILE') and so on? Or, if you want a single
function, why not let the user say
FINFO('DATAFILE','FLIMIT')
or
FINFO('DATAFILE','EOF')
Sure, it would take a little bit of extra time to parse, but
think of the advantages in clarity.
Of course, you can remedy this problem yourself by setting up
(probably in a logon UDC) variables or JCWs that are set to the
the appropriate FINFO values, e.g.
SETVAR FIFILECODE 9
SETVAR FIFLIMIT 12
SETVAR FIEOF 19
...
You'd probably have to set either 14 or 18 of these variables,
and then you could say
:IF FINFO('AP010S',-FIMODDATE)>FINFO('AP010P',-FIMODDATE) OR &
: FINFO('AP010S',-FIMODDATE)=FINFO('AP010P',-FIMODDATE) AND &
: FINFO('AP010S',-FIMODTIME)>FINFO('AP010P',-FIMODTIME) THEN
or
:IF FINFO('DATAFILE',FIEOF)>FINFO('DATAFILE',FIFLIMIT)-100
THEN
Unfortunately, you and I both know most people won't do this --
they'll use the "magic numbers" and let you try to figure out
what's going on.
Even if you set up all the variables and use them consistently,
you'll lose one of the greatest advantages of command files:
their stand-alone nature. Your "MPE programs" will now rely on
your logon UDC and its SETVARs -- if it gets deleted, they'll
stop working. If you want to copy your job stream or other MPE
program onto some other machine, you'll have to be sure that the
other machine has the same logon UDCs. The point is that HP
shouldn't have made you (or let you) use "magic numbers" in the
first place.
This might seem like looking a gift horse in the mouth -- for
fifteen years, we had nothing, and now, when they give us something,
we want more. However, it seems almost a shame that HP, having made
the CI so much more powerful, didn't implement such reasonable and
useful features.
#3. INPUT AND OUTPUT
A major shortcoming of MPE V was the absence of any general output
command. Why, to output a simple message, you had to have a UDC like
DISPLAY !STUFF
OPTION LIST
COMMENT !STUFF
The OPTION LIST would cause the UDC body -- in this case COMMENT
followed by the DISPLAY parameters -- to be output; to output any
message, you'd say
DISPLAY "HI THERE!"
Unfortunately, this would display not HI THERE!, but rather
COMMENT HI THERE!
To avoid the output of the "COMMENT ", you had to output special
escape sequences to backspace the cursor and clear the line -- of
course, this wouldn't work on a printing terminal. All this bother
just to display some text!
MPE XL does things the right way -- it simply has an MPE command to
do the job. Just say
ECHO HI THERE!
and that's it. The only thing I can complain about is the command name
-- ECHO's pretty unintuitive. UNIX, of course, calls its command ECHO
(along with calling its PURGE command RM and its text search command
GREP), and MPE XL borrowed the name. I'd rather HP called it DISPLAY
or TYPE or OUTPUT or something like that, but it's hardly a big deal.
Of course, outputting variables and expressions can be easily done
with the ECHO command -- just use the !xxx and ![xxx] syntaxes:
ECHO YOU'RE SIGNED ON AS !HPUSER.!HPACCOUNT, X = ![UPS(X)]
In addition to the :ECHO command for output, MPE XL also has an
input command, fortunately called :INPUT. For instance, you might have
a UDC that says:
MOVE FROMFILE, TOFILE
SETJCW CIERROR=0
IF FINFO("!TOFILE",0) THEN
COMMENT Target file already exists!
INPUT PROMPT="OK to purge !TOFILE? "; NAME=PURGEFLAG
IF UPS(STR(PURGEFLAG,1,1))="Y" THEN
PURGE !TOFILE
ENDIF
ENDIF
RENAME !FROMFILE, !TOFILE
If TOFILE already exists, the UDC will ask the user if it's OK to
purge it. UPS(STR(PURGEFLAG,1,1)) merely means "the upshifted first
character of PURGEFLAG" -- this way, Y, YES, and YOYO will all be
accepted as a YES answer.
Actually, there's one pretty big temptation with the :INPUT command
that should be resisted. You should think twice (or more) before using
the :INPUT command to prompt for UDC (or command file) PARAMETERS. For
instance, a UDC such as
MOVEP
INPUT PROMPT="From file? "; NAME=FROMFILE
INPUT PROMPT="To file? "; NAME=TOFILE
SETJCW CIERROR=0
IF FINFO("!TOFILE",0) THEN
COMMENT Target file already exists!
INPUT PROMPT="OK to purge !TOFILE? "; NAME=PURGEFLAG
IF UPS(STR(PURGEFLAG,1,1))="Y" THEN
PURGE !TOFILE
ENDIF
ENDIF
RENAME !FROMFILE, !TOFILE
may not be a very good idea at all. Unlike the parameterized UDC we
showed above, this one can only be conveniently used directly from the
CI. Say that you want to write another UDC that runs a program and
renames one of its output files (LISTFILE) into LISTFILE.ARCHIVE. With
the parameterized MOVE UDC, we could say:
...
RUN MYPROG
MOVE LISTFILE, LISTFILE.ARCHIVE
...
and then have the MOVE UDC prompt the user if LISTFILE.ARCHIVE already
exists. The unparameterized MOVEP UDC can't be used here at all, since
it always prompts the user for the input and output files, which in
this case are fixed and should not be prompted for.
In other words, this is the same reason why the best
third-generation language procedures take their input values as
parameters rather than prompt for them -- a parameterized procedure is
much more reusable than a prompting one.
One very interesting use of the :INPUT command, though, might be in
cases such as this:
MOVE FROMFILE=" ", TOFILE=" "
IF "!FROMFILE"=" " THEN
INPUT PROMPT="From file? "; NAME=VARFROMFILE
ELSE
SETVAR VARFROMFILE "!FROMFILE"
ENDIF
IF "!TOFILE"=" " THEN
INPUT PROMPT="To file? "; NAME=VARTOFILE
ELSE
SETVAR VARTOFILE "!TOFILE"
ENDIF
SETJCW CIERROR=0
IF FINFO("!VARTOFILE",0) THEN
COMMENT Target file already exists!
INPUT PROMPT="OK to purge !VARTOFILE? "; NAME=PURGEFLAG
IF UPS(STR(PURGEFLAG,1,1))="Y" THEN
PURGE !VARTOFILE
ENDIF
ENDIF
RENAME !VARFROMFILE, !VARTOFILE
This UDC can accept its input either from its parameters or from the
terminal. If it's used from within another UDC or by a knowledgeable
user, it can be passed parameters -- if a novice user is using it, he
can just type
:MOVE
and be prompted for all the input (for instance, if he's unfamiliar
with what parameters the UDC takes). Actually, this may not be so
useful for a simple UDC like this, but a really complicated UDC with
many parameters can be made much more convenient with "dual-mode"
processing like this.
There are plenty of other uses for the :INPUT command -- menus,
error processing ("Abort UDC or continue? "), etc. There are also a
lot of rather devious, non-obvious uses for it, too (more about those
later). The only thing that bears keeping in mind is that :INPUTs
should not entirely take the place of parameter passing.
#4. :WHILE LOOPS
No programming language is really complete without some sort of
looping capability. In MPE V, you could sometimes make do with the
pseudo-looping capabilities of EDITOR/3000 (for things like taking the
output of one program and translating it into input for another) and
the ability of :STREAMs to stream other jobs. For instance, one thing
that we at VESOFT used to make multiple production tapes was a
tape-making job stream that at the end streamed itself, thus forming a
sort of "infinite loop". (This was before we implemented :WHILE and
other MPE XL functions in our STREAMX Version 2.0, which makes things
much easier.)
In one respect, MPE XL's :WHILE command gives you all the looping
that you need (any loop, including the FOR x:=y TO z and the REPEAT
... UNTIL constructs, can be emulated with a :WHILE); however, as
we'll discuss later, it falls tantalizingly short in some areas.
First the good news:
SETVAR JOBNUM 138
WHILE JOBNUM<=174 DO
ABORTJOB #J!JOBNUM
SETVAR JOBNUM JOBNUM+1
ENDWHILE
This is an example of how the :WHILE loop can iterate through a
set of integers.
This simply aborts a whole range of jobs, from #J138 to #J174.
(Seems useless?
Try submitting fifty jobs in one shot -- all of them with the same
silly error!
I did this the day before I wrote the paper; the :WHILE loop sure
came in handy.)
Similar things can be done in some other cases -- for instance, you
can use this to purge LOG####.PUB.SYS system log files IF you know
the starting and ending log file numbers (unless you're willing to
start at LOG0001 and work your way up).
Another example, taken roughly from Jeff Vance and John Korondy's
excellent paper "DESIGN FEATURES OF THE MPE XL USER INTERFACE"
(INTEREX Las Vegas 1987 Proceedings):
PRT F1, F2="", F3="", F4="", F5="", F6=""
COMMENT Prints F1, F2, F3, F4, F5, and F6 to the line printer
FILE OUT;DEV=LP
SETVAR I 1
SETVAR F7 "" << to terminate the loop >>
WHILE '!"F!I"' <> ''
IF FINFO('!"F!I"',0) THEN
ECHO PRINTING !"F!I"
PRINT !"F!I",*OUT
ELSE
ECHO ERROR: !F"!I" NOT FOUND, SKIPPED.
ENDIF
SETVAR I I+1
ENDWHILE
The WHILE loop here iterates through the 6 UDC parameters, making it
unnecessary to repeat its contents once for each one.
The construct !"F!I" is actually rather interesting.
If I is 3, it gets translated into !"F3", which in turn gets
replaced by the value of the F3 parameter.
Another example might be checking a parameter to make sure that
it's, say, entirely alphabetic (in preparation for passing it to some
program that will abort strangely and unpleasantly if there are any
non-alphabetic characters in it):
SETVAR I 1
WHILE I<=LEN(PARM) AND UPS(STR(PARM,I,1))>="A" AND &
UPS(STR(PARM,I,1))<="Z" DO
SETVAR I I+1
ENDWHILE
IF I>LEN(PARM) THEN
COMMENT Hit the end of the string without finding a non-alpha
RUN MYPROG;INFO="!PARM"
ELSE
ECHO Error! Non-alphabetic character found:
CALC "!PARM"
SETVAR BLANKS ""
SETVAR J 1
WHILE J<I
SETVAR BLANKS BLANKS+" "
SETVAR J J+1
ENDWHILE
CALC BLANKS+"^"
ENDIF
Note the little "bell-and-whistle" -- if there's a non-alphabetic
character, we use a :WHILE loop to concatenate together several
blanks and an "^", so the output looks like:
Error! Non-alphabetic character found:
FOOBAR.XYZZY
^
Many parsing operations can actually be done more simply with the
POS function (which finds the first occurrence of one string in
another); however, some complicated operations (such as the ones we
just showed) may require :WHILE loops.
Finally, one other place where :WHILE should find a lot of use
is the :INPUT command:
INPUT PROMPT="OK to proceed (Y/N)? "; NAME=ANSWER
WHILE UPS(ANSWER)<>"Y" AND UPS(ANSWER)<>"YES" AND &
UPS(ANSWER)<>"N" AND UPS(ANSWER)<>"NO" DO
ECHO Error: Expected YES or NO.
INPUT PROMPT="OK to proceed (Y/N)? "; NAME=ANSWER
ENDWHILE
Most good UDCs and command files that use :INPUT should have some
sort of input error checking, and this kind of :WHILE loop is a
convenient way of doing it.
(Of course, you could get rid of the four UPS(ANSWER)s by doing a
"SETVAR ANSWER UPS(ANSWER)", but you'd have to do it after each
INPUT command.)
With all this power, what's there to complain about?
After all, with an :IF and a :WHILE any language is theoretically
complete -- any algorithm can be implemented.
Well, not quite.
Control structures can get you only as far as the data access
primitives are able to take you.
Take some of the iterative operations that you'd REALLY want to
implement:
* WHILE there are files in a fileset, DO something to them.
* WHILE there are jobs left, ABORT them (in preparation for a
backup).
* WHILE there are records in a fileset, DO some processing on
them -- perhaps write some of the records into another file,
or pass them as input to some other program.
You can't do any of this (straightforwardly) because MPE XL doesn't
provide you any functions to read files, to handle filesets, to find
all jobs, etc.
You'd like to be able to say:
:WHILE FRECORD('MYFILE',RECNUM)<>''
...
:ENDWHILE
where FRECORD would return you a particular record of the specified
file; unfortunately, no FRECORD primitive exists.
The :WHILE command is only as powerful as the conditions you can
specify; unfortunately, at the moment, this seems mostly limited to
numeric iteration and to checking command success/failure.
Another thing you'd like to be able to do with :WHILE is to repeat
a particular command every given number of seconds or minutes --
for instance, to have a job stream wait until a particular file
is built or becomes accessible.
Unless you're willing to spend lots of CPU time in the
loop, you need to have some way of pausing for a given amount of time,
e.g.
:WHILE NOT FINFO('MYFILE',0) DO
: PAUSE 600 << 600 seconds >>
:ENDWHILE
Unfortunately (as of MPE/XL Release 1.1), there is no :PAUSE command
or PAUSE function provided by MPE XL (although as we'll see shortly,
there are some tricks you could do...).
#5. COMMAND FILES
Command files were implemented more for convenience than for
additional power; however, they can be convenient indeed.
Simply put, a command file is a replacement for a UDC.
If you want to implement a new command called DBSC to run DBSCHEMA,
you used to have to write a UDC:
DBSC TEXT="$STDIN", LIST="$STDLIST"
FILE DBSTEXT=!TEXT
FILE DBLIST=!LIST
RUN DBSCHEMA.PUB.SYS;PARM=3
You'd add this UDC to your system UDC file, :SETCATALOG the file,
and presto! you have a new command.
In MPE XL, you could use a command file to do the same thing.
You could build a file called DBSC.PUB.SYS that contains the text:
PARM TEXT="$STDIN", LIST="$STDLIST"
FILE DBSTEXT=!TEXT
FILE DBLIST=!LIST
RUN DBSCHEMA.PUB.SYS;PARM=3
(Note that the word "DBSC" in the first line of the UDC was replaced
by the word "PARM" in the command file.)
Then, the very presence of the DBSC.PUB.SYS file will implement the
new command -- no need to :SETCATALOG it.
You can just say
DBSC MYSCHEMA, *LP
and MPE will check to see if DBSC.PUB.SYS exists, find that it does,
and execute it much like it would have a :SETCATALOGed UDC.
Why is this so nice?
Well, remember all the nonsense you had to go through to change a
:SETCATALOGed UDC file?
You had to build a new file with a different name, :SETCATALOG it in
the old one's place, and even then it wouldn't take effect for
another session until it logged off and logged back on!
Most people ended up having several versions of the system UDC file,
since you couldn't purge the old file until everybody who had been
using it was logged off.
With command files, simply build the file, and there you have it.
No need to worry about whether the UDC file is in use (unless the
command is actually being executed at that very moment, it won't be
in use); no need to choose a new name for the file; no need to
remember to re-specify all the other UDC files on the :SETCATALOG.
In fact, the MPE XL compiler commands are actually implemented
this way -- :PASXL, for instance, is just a command file
(PASXL.PUB.SYS) that sets up several file equations and runs
PASCALXL.PUB.SYS (the actual compiler program file -- you still need
programs for something!).
Whenever I give an example in this paper that involves UDCs,
chances are very good that it will work with command files, too
(actually, you'd probably want to do it with command files).
I only use UDCs in the examples to keep things as familiar as
possible.
You could also implement account-wide commands by just putting the
command files into your PUB group, and group-wide commands by putting
them into your own groups.
As we mentioned earlier, MPE XL has a special variable called HPPATH
that indicates where it is to search for command files; by default,
HPPATH is set to "!HPGROUP,PUB,PUB.SYS", i.e. "search your group
(!HPGROUP) first, then the PUB group, then the PUB.SYS group".
You could actually change it to something else, e.g.
:SETVAR HPPATH "!HPGROUP,PUB,PUB.VESOFT,CMD.UTIL,PUB.SYS"
In fact, it's probably a good idea to keep your own command files
not in PUB.SYS (where they'll just get lost among all the other files)
but in a special group, say CMD.UTIL.
This way, a simple
:LISTF @.CMD.UTIL
will show you all the system-wide command files that you've set up.
Of course, you'll have to have a system-wide logon UDC that sets up
the HPPATH variable to include CMD.UTIL.
A similar feature of MPE XL is "implied run".
Just entering a program file name will AUTOMATICALLY cause that
program to be run; e.g.
:DBUTIL
will automatically do a
:RUN DBUTIL.PUB.SYS
WITHOUT your having to have a UDC or a command file for this
purpose.
You can also specify a parameter, which gets passed as the ;INFO=
string to the program being run:
:MYPROG FOO
:PROG2 "TESTING ONE TWO THREE"
and also a second parameter, which gets passed as the ;PARM=:
:MYPROG ,10
:MYPROG FOOBAR,5
(Other parameters -- ;LIB=, ;STDIN=, ;STDLIST=, etc. can not be
passed; you have to do a real :RUN for that.)
Also note that MPE XL looks for the program file in exactly the
same places in which it looks for a command file: all those groups
listed in the HPPATH variable.
These features are all very convenient, and can save you a good
deal of effort and some typing.
There is, however, one problem with both command files and implied
:RUNs (and also UDCs) that limits their usefulness:
* THERE'S NO WAY FOR PASSING THE *ENTIRE REMAINDER OF THE COMMAND
LINE* TO EITHER A COMMAND FILE, AN IMPLIED :RUN, OR A UDC.
For example, say that I want to implement a new command called
:CHGUSER that executes my own CHGUSER.PUB.SYS command file.
I want it to look much like MPE's :NEWUSER and :ALTUSER -- I'd like
to let people say
:CHGUSER XYZZY;CAP=-BA,+DS,+PM;PASS=$RANDOM
The CHGUSER.PUB.SYS command file could then take the entire remainder
of the line as a single parameter, and then perhaps pass it to some
program that would process it.
Unfortunately, this simply can't be done!
Since the parameter list includes ";"s, ","s, and "="s, MPE XL views
them as delimiters (it would view blanks as delimiters, too); there's
no way of specifying in the command file that delimiter checking is
to be turned OFF, and that the entire remainder of the command
is to be passed as one parameter.
Of course, you could require the user to enclose the parameter in
quotes, but you'd rather not do that.
(If you're thinking that declaring CAP=, PASS=, etc. as keywords to
the command file will work, it won't -- look at the ","s in the CAP=
parameter.)
In fact, MPE's own :FCOPY command couldn't be implemented as an
auto-RUN or as a command file for this very reason -- each :FCOPY
command always includes delimiters, and that won't work.
I can see why HP doesn't like delimiters in an implied :RUN (so that
the ;PARM= value can be specified as well as the ;INFO=), but why not
have some sort of option for command files?
Personally, I'd rather be able to pass the entire remainder of the
command as one parameter than be able to specify a ;PARM= value.
In fact, UNIX does have a way of treating the parameter list (of
either a program or a command file) as either a sequence of
individual parameters or as one single string;
UNIX programmers frequently use this feature.
Again, this may be looking a gift horse in the mouth, but it would
have been so easy for HP to implement something like this.
TRICKS
We've pretty much covered all the things you can do
straightforwardly with MPE XL.
Of course, if this was all I had to say, I'd never have written this
paper.
People who know me know that I NEVER do things straightforwardly...
MPE V had the (small) set of things you can do easily and
the far larger set of things you could do if you really stood the
system on its head.
Similarly, MPE XL has the larger set of things you can do easily, and
the bigger still number of things you can do with a little bit of
trickery.
This is where the fun begins.
#1. PAUSING FOR X SECONDS
At a certain point in your job stream, a particular file may be
in use.
You don't want this to abort the job -- rather, you want the job to
suspend until the file is no longer in use.
A first attempt at this might be:
WHILE FINFO('MYFILE',fileisinuseflag) DO
PAUSE one minute
ENDWHILE
While the file is in use (surely there must be an FINFO option
for this!), pause for a minute, and then check again.
This shouldn't be too much of a load on the system (though without
the :PAUSE this would be a heavy CPU hog indeed!).
Of course, you face two problems.
First of all, there is no FINFO option to check to see if the file
is in use or not.
(OK, everybody, submit those SRs!)
Old MPE programming hands, however, shouldn't despair:
FILE CHECKER=MYFILE;ACC=OUTKEEP;SAVE;EXC
SETJCW CIERROR=0
CONTINUE
PURGE *CHECKER
WHILE CIERROR=384 DO
PAUSE one minute
SETJCW CIERROR=0
CONTINUE
PURGE *CHECKER
ENDWHILE
See what we're doing?
The :FILE equation tells the file system to open the file with
;ACC=OUTKEEP (so the data won't get deleted) and close it with
disposition ;SAVE (so the file itself won't get purged) -- the :PURGE
command will thus not purge the file at all, but just try to open it
with the exclusive option.
As long as the :PURGE is failing, we know that the file is in use
(unless, of course, it doesn't exist or we're getting a security
violation).
We do this check once before the :WHILE loop; then, if CIERROR=384
(indicating that :PURGE couldn't open the file exclusively),
we pause for a minute, do the check again, and keep going until the
check succeeds.
The only problem that remains is, of course, that MPE XL
(as of Release 1.1) has no :PAUSE command -- without it, the entire
exercise is academic.
What can we do?
Well, one solution is to write a program.
Call it PAUSE.PUB.SYS -- it'll take a ;PARM= value, convert it to
a real number, and call the PAUSE intrinsic.
Then, any of your command files could say
:RUN PAUSE.PUB.SYS;PARM=60
or just use the implied :RUN, as in
:PAUSE ,60
I don't like this.
I don't like it for several reasons:
* The program, though not by any means difficult, is not trivial
to write.
If you know SPL, it's only a few lines; what if you only know
COBOL?
(It's a nightmare to call the PAUSE intrinsic from COBOL, in
which handling real numbers requires a lot more work than one
would care to do.)
From FORTRAN, you could call PAUSE, but you also need to call
the GETINFO intrinsic (quick! do you know its parameter
sequence?).
What if you had to write a program that checked to see if the
file was in use?
You'd have to call FOPEN, figure out the right foptions and
aoptions bits (%1 and %100, if you're curious), and then use
an intrinsic to set a JCW appropriately.
* Once you write it, you have to keep track of it.
You put its object code into PAUSE.PUB.SYS -- where do you keep
the source code?
What if you lose it?
Will you write documentation for it, or add a HELP option?
* Finally, the more external programs you use, the less
self-contained the job stream will be.
What if you move the job to one of your machines?
You'll have to move the PAUSE program, too, and probably its
source code and documentation, just to be safe.
For vendors like VESOFT, the problem becomes even greater --
our installation job stream has to be able to run on a system
where NONE of our software currently exists.
We can't rely on your PAUSE.PUB.SYS or what have you.
You might agree with me or you might not.
It's quite possible that the only problem with an external program
file is that it somehow affects some silly esthetic sense of mine --
that my mind is too twisted to appreciate a simple, straightforward
solution.
In any event, here's my answer to the problem:
:BUILD MSGFILE;TEMP;MSG
:FILE MSGFILE,OLDTEMP
:RUN FCOPY.PUB.SYS;STDIN=*MSGFILE;INFO=":INPUT DUMMY;WAIT=60"
Nice, eh?
I build a temporary message file called MSGFILE, and then I run
FCOPY with ;STDIN= redirected to it.
Then, I tell FCOPY to execute an :INPUT command, telling it to WAIT
for 60 seconds for input!
(Of course, the only reason I use FCOPY here is to have it execute
the MPE XL command ":INPUT DUMMY;WAIT=60" -- FCOPY's convenient for
this because we can pass the command to it as an INFO= string.)
Of course, the input will never come, since MSGFILE is empty;
and, I must admit that the :INPUT ;WAIT= parameter was almost
certainly intended to wait for TERMINAL input.
However, it also works perfectly well when the input is coming from
a $STDIN file that was redirected to a message file.
When the 60 seconds are up, the :INPUT command will terminate and
return control to FCOPY, which will then return back to the CI.
Now, our job stream is complete:
:BUILD MSGFILE;TEMP;MSG
:FILE MSGFILE,OLDTEMP
:FILE CHECKER=MYFILE;ACC=OUTKEEP;SAVE;EXC
:SETJCW CIERROR=0
:CONTINUE
:PURGE *CHECKER
:WHILE CIERROR=384 DO
: RUN FCOPY.PUB.SYS;STDIN=*MSGFILE;INFO=":INPUT DUMMY;WAIT=60"
: SETJCW CIERROR=0
: CONTINUE
: PURGE *CHECKER
:ENDWHILE
Complete, of course, except for the many :COMMENTs that I'm sure that
you, as a conscientious programmer, will certainly include...
Some may say that only a computer freak can think that the above
solution is simpler than just running a program that loops doing
FOPENs and PAUSEs.
They may be right.
#2. READING A FILE
The :REPORT command nicely shows you all the disc space used by
each account on the system (actually, on MPE XL 1.0 the disc space
:REPORTed is sometimes erroneous, but I'm sure that'll be fixed soon).
Unfortunately, it doesn't show you the total disc space used in the
entire system, which is a useful piece of information.
The :REPORT command can send its output to a file, which is good.
But what can you do to read the file?
Well, let's start at the beginning.
First, let's do a :REPORT into a disc file:
:FILE REPOUT;REC=-80,16,F,ASCII;NOCCTL;TEMP
:CONTINUE
:REPORT XXXXXXXX.@,*REPOUT
What's the XXXXXXXX.@ for?
The :REPORT command usually outputs information on accounts and on
groups; in our case, we don't want to have any group information at
all.
By specifying a group that we know doesn't exist in any account
(I hope that you don't have a group called XXXXXXXX) we can make
MPE output only the account information and no group information.
It'll also print an error (NONEXISTENT GROUP), but that's OK.
Now, we have a temporary file called REPOUT, which contains two
header lines and one line for each account.
We'd like to extract the number of sectors used
from each account line and add everything up.
This is where the real trickery comes in.
One thing we might do is use EDITOR.
The principle here is that we'll take the :REPORT listing,
which looks like
ADMIN 15502 ** 1046 ** 8372 **
CUST 3062 ** 0 ** 0 **
DEV 7080 ** 18 ** 8 **
...
and "massage" it into a sequence of MPE XL commands:
:SETVAR TOTALSPACE TOTALSPACE+ 15502
:SETVAR TOTALSPACE TOTALSPACE+ 3062
:SETVAR TOTALSPACE TOTALSPACE+ 7080
...
We can then execute all these commands, and TOTALSPACE will be the
total used disc space count.
Doing this is simple (?):
:PURGE REPOUT,TEMP
:FILE REPOUT;REC=-80,16,F,ASCII;NOCCTL;TEMP
:CONTINUE
:REPORT XXXXXXXX.@,*REPOUT
:SETVAR TOTALSPACE 0
:EDITOR
/TEXT REPOUT
/DELETE 1/2 << delete the header lines >>
/CHANGE 23/72,"",ALL << delete everything right of the count >>
/CHANGE 1/8,":SETVAR TOTALSPACE TOTALSPACE+" << delete the left >>
<< now, each line looks like: >>
<< :SETVAR TOTALSPACE TOTALSPACE+ 15502 >>
/KEEP REPUSE,UNN
/USE REPUSE << execute the :SETVARs >>
/EXIT
Now, the TOTALSPACE variable is set to the total disc space!
This is very much like what we did in pre-MPE XL "MPE PROGRAMMING"
-- we used EDITOR as a means of taking a program's or a command's
output and making it another program's (in this case, also EDITOR's)
input.
In fact, UNIX's "sed" editor is very frequently used for this purpose
by UNIX programmers (although it's much more adapted to this than
EDITOR/3000 is).
The trouble with this solution is that it's inherently limited to
plain textual substitution.
What if we wanted to sum the disc space of all accounts that used
more than 20,000 sectors?
EDITOR has no command that can easily check the value of a particular
field in a line.
What we'd really like to do is use all the power of MPE XL's :WHILE
loop and expressions to process the :REPORT listing one line at a
time.
As I mentioned before, MPE XL unfortunately has no "get a record
from a file" function.
However, not all is lost.
Let's set up two command files.
One (TOTSPACE) will look like this:
FILE REPOUT;REC=-80,16,F,ASCII;NOCCTL;TEMP
SETVAR OLDMSGFENCE HPMSGFENCE
SETVAR HPMSGFENCE 2
PURGE REPOUT,TEMP
CONTINUE
REPORT XXXXXXXX.@,*REPOUT
SETVAR HPMSGFENCE OLDMSGFENCE
FILE REPOUT,OLDTEMP
CONTINUE
RUN CI.PUB.SYS;PARM=3;INFO="TOTSPAC2";STDIN=*REPOUT;STDLIST=$NULL
ECHO TOTAL USED DISC SPACE = !TOTALSPACE
There are two new things here.
One is
SETVAR OLDMSGFENCE HPMSGFENCE
SETVAR HPMSGFENCE 2
CONTINUE
REPORT XXXXXXXX.@,*REPOUT
SETVAR HPMSGFENCE OLDMSGFENCE
What's all this HPMSGFENCE stuff?
Well, remember that the REPORT XXXXXXXX.@,*REPOUT command will almost
certainly output an error message (NONEXISTENT GROUP).
This is to be expected, and we don't want the user to have to see
this.
So, we set the HPMSGFENCE variable to 2, indicating that error
messages are not to be displayed (setting it to 1 would inhibit
display of warnings, but still print errors).
However, since we want to reset HPMSGFENCE to its old value later,
we save the old value of HPMSGFENCE, set the value to 1, do the
command, and then reset the old value.
Personally, I think that this is a bit more effort than required.
In MPEX, I simply added a new command called %NOMSG; saying
%NOMSG REPORT XXXXXXXX.@,*REPOUT
makes MPEX execute the :REPORT command without printing any messages.
Similarly, HP could have had a :NOMSG command (for suppressing errors
and warnings) and a :NOWARN command (for suppressing only warnings).
This would have saved all the bother of the saving of the old
HPMSGFENCE, setting it, and resetting it.
In fact, to be really clean, I should even do a
:DELETEVAR OLDMSGFENCE
after doing the :SETVAR HPMSGFENCE OLDMSGFENCE.
In any case, the HPMSGFENCE solution is better than no solution
at all -- in MPE V, the warning message would always be displayed,
and users might get quite confused by it.
The only other little trick (in this command file) is
RUN CI.PUB.SYS;PARM=3;INFO="TOTSPAC2";STDIN=*REPOUT;STDLIST=$NULL
What on earth does this mean?
In MPE XL, the CI is not some special piece of code kept in the
system SL.
Rather, it's a normal program file called CI.PUB.SYS -- when a job
or a session starts up, the system creates a new CI.PUB.SYS process
on the job/session's behalf.
However, CI.PUB.SYS is also :RUNable just like any other program;
you can run it interactively by saying
:RUN CI.PUB.SYS
or just
:CI
Alternatively, you can run it and tell it to execute exactly one
command:
:RUN CI.PUB.SYS;PARM=3;INFO="command to be executed"
(;PARM=3 tells the CI not to display the :WELCOME message and to
only process the ;INFO= command, rather than prompt for more
commands -- other ;PARM= values do different things.)
In our case, we're running CI.PUB.SYS with ;INFO="TOTSPAC2"
(telling it to execute our TOTSPAC2 command file), and with ;STDIN=
redirected to our :REPORT command output file.
We redirect ;STDLIST= to $NULL, since the CI will otherwise echo
its ;INFO= command -- ":TOTSPAC2" -- before executing it.
Now we can see what TOTSPAC2 contains:
INPUT DUMMY << to skip the first header line >>
INPUT DUMMY << to skip the second header line >>
SETVAR TOTALSPACE 0
SETVAR HPMSGFENCE 2 << to ignore any error messages >>
WHILE TRUE DO << loop until we get an error >>
INPUT REPORTLINE << get a :REPORT detail line >>
<< extract the disc space -- 15 columns starting with >>
<< column 9 -- and add it to TOTALSPACE >>
SETVAR TOTALSPACE TOTALSPACE + ![STR(REPORTLINE,9,15)]
ENDWHILE
See the trick?
CI.PUB.SYS's ;STDIN= is redirected to a disc file, so all :INPUT
commands will read from that disc file.
For each line we read in, we extract the account disc space
(STR(REPORTLINE,9,15)), and do a
:SETVAR TOTALSPACE TOTALSPACE + extracted_account_disc_space
When we run out of input lines, the :INPUT command will get an EOF
condition, and the command file will stop executing.
TOTALSPACE is now set to the total disc space.
Both the EDITOR and the two-command-files solution can be used
online, though both require two files
(the first approach would require a disc file that contains all the
required EDITOR commands).
In a job, the EDITOR approach can be completely self-contained, since
the EDITOR commands can just be put into the job stream; the second
approach can also be self-contained if you create the TOTSPAC2 command
file within the job (by using EDITOR or FCOPY).
Finally, one more variation on the same theme:
FILE REPOUT;REC=-248,,V,ASCII;NOCCTL;MSG;TEMP
SETVAR OLDMSGFENCE HPMSGFENCE
SETVAR HPMSGFENCE 2
CONTINUE
PURGE REPOUT,TEMP
CONTINUE
REPORT XXXXXXXX.@,*REPOUT
FILE REPOUT,OLDTEMP
CONTINUE
RUN CI.PUB.SYS;PARM=3;INFO="INPUT DUMMY";STDIN=*REPOUT;STDLIST=$NULL
RUN CI.PUB.SYS;PARM=3;INFO="INPUT DUMMY";STDIN=*REPOUT;STDLIST=$NULL
SETVAR TOTALSPACE 0
WHILE FINFO('*REPOUT',19)>0 DO
RUN CI.PUB.SYS;PARM=3;INFO="INPUT REPORTLINE";STDIN=*REPOUT;&
STDLIST=$NULL
SETVAR TOTALSPACE TOTALSPACE + ![STR(REPORTLINE,9,15)]
ENDWHILE
SETVAR HPMSGFENCE OLDMSGFENCE
ECHO TOTAL USED DISC SPACE = !TOTALSPACE
Intuitively obvious, eh?
* The :REPORT command output is sent to a MESSAGE FILE.
* To read a line from the file, we say
RUN CI.PUB.SYS;PARM=3;INFO="INPUT REPORTLINE";STDIN=*REPOUT;&
STDLIST=$NULL
This essentially tells the CI to read into REPORTLINE the first
record from *REPOUT -- since it's a message file, the record
will be read and deleted;
the next read will read the next record.
* We loop while FINFO('*REPOUT',19) -- REPOUT's end of file --
is greater than 0.
When the file is emptied out, we stop.
This is entirely self-contained, and in some respects more
versatile (we can, for instance, prompt the user for input in the
middle of the :WHILE loop, since our $STDIN is not redirected).
The output-to-a-message-file and run-the-CI-to-get-each-record
constructs are essentially a poor man's FREAD function.
On the other hand, this approach runs CI.PUB.SYS once for each file
-- even on a Spectrum this'll take some time!
One other glitch: each one of those :RUNs would normally print
one of those pesky "END OF PROGRAM" messages.
In MPE XL, you can actually avoid them -- as long as you use an
implied :RUN rather than an explicit :RUN command.
We can't use an implied :RUN because we need to redirect the
STDIN and STDLIST.
Setting HPMSGFENCE to 2 almost fixes the problem, since it
inhibits the printing of the "END OF PROGRAM".
HOWEVER, each RUN still outputs two blank lines -- thus, the above
script would print a couple of screenfuls of blank lines before
calculating the result.
This is another good argument for using the two-command-file
solution, which does only one :RUN and thus prints out only one
END OF PROGRAM message and one pair of blank lines.
#3. A PSCREEN COMMAND FILE
One of the most useful contributed programs for the HP3000 is
PSCREEN, which copies the contents of your screen to the line printer.
It works by outputting an ESCAPE-d sequence to the terminal,
which causes almost any HP terminal to send back (as input) the
contents of the current line on the screen.
PSCREEN sends one ESCAPE-d for each line, picks up the output
transmitted by the terminal, and prints it to the line printer.
Now, PSCREEN is already up and running, so there's really no
reason to implement it as a command file; however, it's quite
interesting to try it, both as an example of the power of MPE XL
and of the trickery you need to resort to in order to work around
some restrictions on that power.
The process of reading the data from the terminal is actually
quite straightforward:
CALC CHR(27)+'H'
WHILE there are more lines on the screen DO
INPUT CURRENTLINE;PROMPT=![CHR(27)+"d"]
ENDWHILE
CHR(27) means a character with the ascii value 27 -- the escape
character.
"![CHR(27)+'d']" is the string ESCAPE-d, which when sent to the
terminal (by the ;PROMPT=) will cause the terminal to input
(into CURRENTLINE) the current line on the screen.
The CALC command outputs ESCAPE-H (home up) to send the cursor to
the top of the screen.
(Actually, it turns out that we can't just display the home up
sequence in the :CALC since :CALC will then output a carriage
return and line feed, and we'll skip the first line on the screen;
instead, we have to incorporate the ESCAPE-H into the first :INPUT
command prompt.)
The only twist here (one that the "real" PSCREEN has to deal
with, too) is finding out how many lines there are on the screen.
If we send an ESCAPE-d after we've already read the last data line,
the terminal will just send us a blank line, and will be happy to
do this forever.
There are two ways of solving this problem.
One is to output (at the very beginning) some sort of "marker" to
the terminal, e.g. "*** PSCREEN END OF MEMORY ***"; then, we can
keep INPUTing until we get this marker line, at which point we know
we're done.
(We should also then erase the tag line so that subsequent PSCREENs
won't run into it.)
Another solution is to ask the terminal itself.
If we say
INPUT PROMPT="![CHR(27)+'F'+CHR(27)+'a']";NAME=CURSORPOS
then the terminal will be sent an ESCAPE-F (HOME DOWN, i.e. go to
the end of memory) and an ESCAPE-a.
The ESCAPE-a will ask it to transmit information on the current cursor
position, in the format "!&a888c999R", where the "!" is an escape
character, the "888" is the column number, and the "999" is the row
number.
This string will be input into the variable CURSORPOS.
Then, the value of the expression
![STR(CURSORPOS,8,3)]
will be the row number of the bottom of the screen.
The old PSCREEN uses the first approach (write a marker),
probably because it's more resilient; I suspect that some old
terminal over some strange datacomm connection can't handle the
ESCAPE-a sequence right.
In any event, reading the data from the screen isn't that hard.
The question is: how can we output it to the printer?
As we showed in our previous discussion, it's quite hard to read
data from a file into a variable.
It's harder still to output the data from a variable to a file.
The solution lies in running CI.PUB.SYS with ;STDLIST= redirected,
thus letting the :ECHO command output to a file rather than to the
terminal.
(This is much like doing file input by running CI.PUB.SYS with
;STDIN= redirected.)
Here's what the full PSCREEN script actually looks like:
SETVAR PSCREENTERM "*** PSCREEN MARKER ***"
ECHO !PSCREENTERM
SETVAR PSCREENLINE 0
INPUT PSCREEN!PSCREENLINE;PROMPT="![CHR(27)+'H'+CHR(27)+'d']"
WHILE PSCREEN!PSCREENLINE <> PSCREENTERM DO
SETVAR PSCREENLINE PSCREENLINE+1
INPUT PSCREEN!PSCREENLINE;PROMPT="![CHR(27)+'d']"
ENDWHILE
CALC CHR(27)+"A"+CHR(27)+"K" << clear the PSCREEN MARKER line >>
FILE PSCROUT;DEV=LP
RUN CI.PUB.SYS;PARM=3;INFO="PSCREENX";STDLIST=*PSCROUT
RESET PSCROUT
DELETEVAR PSCREEN@
Note that we're reading all the lines into variables called PSCREEN0,
PSCREEN1, PSCREEN2, PSCREEN3, etc.
These variables will then be read by the PSCREENX command file,
which looks like this:
SETVAR PSCREENI 0
WHILE PSCREENI<PSCREENLINE DO
ECHO ![PSCREEN!PSCREENI]
SETVAR PSCREENI PSCREENI+1
ENDWHILE
There it is, in all its glory!
Again, the PSCREEN program works just fine -- probably even better
than these command files -- but this is just an example of the kind
of things you can do.
One little glitch you'll run into with these command files is that
the first line of every printout will read ":PSCREENX".
That's because CI.PUB.SYS will echo its ;INFO= command to the
;STDLIST= file.
For PSCREEN, this should be fairly harmless; however, what if you
simply want to write the contents of a variable to a disc file without
the echoing getting in the way?
The solution is this:
PURGE TEMPOUT,TEMP
BUILD TEMPOUT;NOCCTL;REC=-508,,V,ASCII;TEMP
FILE TEMPOUT,OLDTEMP;SHR;GMULTI;ACC=APPEND
RUN CI.PUB.SYS;INFO="ECHO !MYVAR";STDLIST=*TEMPOUT
FILE TEMPOUT,OLDTEMP
FILE DISCFILE;ACC=APPEND
PRINT *TEMPOUT;OUT=*DISCFILE;START=3
We run the CI and tell it to echo the variable MYVAR to a
temporary file called TEMPOUT.
Then we do a :PRINT command (a new feature of MPE XL) that appends
to DISCFILE the contents of TEMPOUT starting with record #3.
Record #1 is CI.PUB.SYS's echo of the ":" prompt; record #2 is its
echo of the "ECHO !MYVAR" command; record #3 is the actual contents
MYVAR variable.
What a bother, and relatively slow, too (that's why we ran the
CI only once in the PSCREEN script).
A built-in MPE XL FWRITE function would have been so much simpler...
#4. EXPRESSIONS AND PROGRAMS
One of the most interesting possibilities of the MPE XL command
interface has nothing to do with command files (or UDCs or job
streams) at all.
I've never seen it implemented before, so it might have a good deal
of practical problems; however, I think that it has a lot of
potential for power.
Consider a program that prints the contents of one of your
specially-formatted data files.
If it were a database, you could use QUERY, with its fairly
sophisticated selection conditions -- you could specify exactly what
records you want to select.
However, if you're writing a special custom-made program, how can
you let the user specify the records to be selected?
There are 1,000 records in the file (17 pages at 60 lines per page),
and the user only wants a few of them.
If you don't put in some sort of selection condition, the user won't
be happy;
if you put in the ability to select on one particular field, I'll bet
you that the user will start asking for selection on another field.
What about ANDs?
ORs?
Arithmetic expressions (SALARY<>BASERATE+BONUSRATE)?
Soon they'll be asking for you to write your own expression parser!
What you really want is a GENERALIZED EXPRESSION PARSER, usable
by any subsystem that wants to have user-specified selection
conditions (and user-specified output formats).
You could tell it about the variables that you have defined -- e.g.,
define one variable for each field in the file, plus some other
variables for some calculated values that the user may find handy.
Then, you tell it to evaluate a user-supplied expression.
Think of all the various programs that could use this!
* V/3000 could have used this for the input field validity checks
(rather than having its own parser);
* QUERY could have used this for the >FIND command (rather than
having its own parser, which, incidentally, can't handle
parenthesized expressions);
* MPE V could have used it for the :IF command logical expressions;
* LISTLOG could have used it to let you select log records;
* QUERY could have used it to output expression values in >REPORTs
(rather than have that silly assembly-language-style register
mechanism);
* EDITOR or FCOPY could have implemented a smart string search
mechanism (find all lines that contain "ABC" OR "DEF").
HP could have saved itself man-years of extra effort, while at the
same time standardizing those expression evaluators that exist AND
implementing expression evaluation in a lot of places that need it!
Not to mention the uses that you and I could put it to!
The point here is that with MPE XL you can -- in a way -- do this
yourself.
Take that file-reader-and-printer program of yours and prompt the
user for a selection condition.
Then, for each file record, use the HPCIPUTVAR intrinsic (or pass the
COMMAND intrinsic a :SETVAR command) to set AN MPE XL VARIABLE FOR
EACH FIELD IN THE RECORD.
Now, do a
:SETVAR SELECTIONRESULT expression_input_by_the_user
Finally, do an HPCIGETVAR to get the value of the SELECTIONRESULT
variable; if it's TRUE, the record should be selected -- if it's
FALSE, rejected.
In other words, you're using the :SETVAR commands expression
handling to do the work for you.
You set MPE XL variables for all the fields in your record, and the
user can then use those variables inside the selection condition.
The condition can use all the MPE XL functions -- =, <>, <, >, +, -,
STR, POS, UPS, etc.; it can reference integer, string, or boolean
variables.
A sample run of the program might be:
:RUN SELFILE
SELFILE Version 1.5 -- this program prints selected records from
the PS010 KSAM file; please enter your selection condition:
>UPS(STATUS)<>"XX" AND WORK_HOURS*HOURLY_SALARY>=10000
Meantime, the program is doing:
FOR each record from PS010 DO
BEGIN
:SETVAR STATUS value_of_status_field
:SETVAR NAME value_of_name_field
:SETVAR WORK_HOURS value_of_work_hours_field
:SETVAR HOURLY_SALARY value_of_hourly_salary_field
:SETVAR DEPARTMENT value_of_department_field
...
:SETVAR SELECTIONRESULT &
UPS(STATUS)<>"XX" AND WORK_HOURS*HOURLY_SALARY>=10000
IF value of SELECTIONRESULT variable = TRUE THEN
output the record;
END;
(The :SETVAR commands in the pseudo-code should probably be calls to
the HPCIPUTVAR intrinsic.)
There are several non-trivial problems with this approach:
* You're restricted to INTEGER, STRING, and BOOLEAN variables --
no dates, reals, etc.
* You're restricted to those functions that MPE XL provides, which
are rather limited (though fairly powerful).
* Most importantly, all those intrinsic calls will take some time!
If you're reading through a 100,000 record file, you might
encounter some serious performance problems.
As I said, to the best of my knowledge nobody's ever implemented
this sort of facility -- for all I know, it may just not be
practically feasible.
However, I suspect that for quick-and-dirty query programs (and also
input checking, output formatting, etc.) where performance is not a
major consideration, it can be very powerful.
You can use it to give a lot of control to the user, with very little
programming effort on your own part.
CONCLUSION
The MPE XL user interface is much more powerful and much more
convenient than the "classic MPE" interface.
(I didn't even go into some features, like multi-line :REDO, which
are convenient indeed.)
It lets you easily do many things that used to require a lot of
effort; however, some key features are unfortunately missing.
Fortunately, with a little bit of ingenuity, even the apparently
"impossible" can be achieved -- I'd be happy if all this paper did
was let you know that there are possibilities to MPE XL beyond those
that are apparent at first glance.
We HP programmers did some pretty amazing things with the limited
capabilities that classic MPE offered us -- with MPE XL, we should
be able to write some very powerful stuff.
One thing that the new MPE XL features should do is whet the
appetites of all the poor people who still have to stick with MPE V
(or, heaven forbid, MPE/IV!) for some time in the future.
After seeing all those wonderful things on the new machines, how can
we bear to live with the old stuff?
There are actually two products out now that implement MPE XL
functionality on MPE V: one called Chameleon, from Taurus Software,
Inc., and VESOFT's own MPEX/3000.
Of course, MPEX does MPE XL emulation in addition to all the other
stuff that MPEX has always done -- fileset handling, %ALTFILE, new
%LISTF modes, the MPEX program hook facility, etc.
VESOFT's STREAMX also implements many MPE XL-like features
(including variables, :WHILE loops, expressions, etc.) for job
stream submission, an area unfortunately neglected by HP.
Personally, I think that variable input, expression evaluation,
input checking, etc. are even more useful at job stream SUBMISSION
time than they are in session mode and at job stream execution time.
Finally, there are several other papers available about MPE XL,
all of which I can recommend highly.
Jeff Vance & John Korondy of HP had the "Design Features of the
MPE XL User Interface" paper in the 1987 INTEREX Las Vegas
proceedings;
David T. Elward published the "Winning with MPE XL" paper in the
October and November 1988 The HP CHRONICLE.
Also, the MPE XL Commands Manual actually has a lot of useful
documentation on command files (including some very interesting
MPE XL Programming examples!) -- I've seen several versions, and it
seems that the most recent ones have the most information.
And, of course, the recently released "Beyond RISC!" book is an
indispensable tool for anybody who deals or will be dealing with
Precision Architecture machines.
Thanks to Rob Apgood of Strategic Systems, Inc., Gavin Scott
of American Data Industries, Stan Sieler and Steve Cooper of
Allegro Consultants, Jeff Vance and Kevin Cooper of HP, and Guy Smith
of Guy Smith Consulting for their input on this paper;
thanks especially to Gavin for letting me test out all the examples
on the computer in the two hours between the time I finished
writing it and the time I had to Federal Express it up to BARUG.