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.

Go to Adager's index of technical papers