VAX Professional: DCL Command RECALL Extensions


DCL Command RECALL Extensions


Hunter Goatley

Clyde Digital Systems

Since VAX/VMS V4.0, DCL has kept a “history” of your last 20 DCL commands, allowing you to easily retrieve past commands. You could either use your up-arrow and down-arrow keys to step forwards and backwards through the list, or you could use the DCL command RECALL to “recall” an old command. The RECALL command is quite flexible, allowing you to list the 20 commands and then select one by giving a number (“$ RECALL 5”), or allowing you to specify the first few characters of the command to be used in a search (e.g., “$ RECALL LINK” to retrieve the most recent LINK command. Many people have commented that one feature that is missing from DCL is a means of flushing the buffer (for security purposes). Last fall, I wrote a program that would flush the buffer, as well as allow you to save commands and restore them in a different interactive session.

The VMS terminal driver (TTDRIVER) has a last-command recall capability that DCL disables; DCL stores the commands in its own buffer in a user’s P1 address space. I started by using the DCL command EXAMINE to look around my P1 space to see if I could find the buffer DCL uses to store old commands. I succeeded in locating the buffer (luckily, it is not protected from user-mode reads) and set out trying to figure out how DCL stored them (I didn’t have access to the VMS micro-fiche, which may or may not have made my task easier!).

A 1024-byte buffer starting at virtual address 7FFE350F (hex) is used to store the DCL commands. Since the buffer is quite large, you will note that you probably have more commands stored there than you can access via the DCL RECALL command (depending on the length of your commands, as many as 100 or more could be stored). You can see this buffer by entering


Figure 1 is a partial dump of the command buffer (I used my own program to dump the bytes in a DUMP-like format—the DCL EXAMINE won’t allow you to see both the hexadecimal and ASCII representation of memory at the same time).

     $ ! The hex longwords should be read right to left.  The right-most
     $ ! longword on the first line is the address of the null byte following
     $ ! the last command entered (in this case, the MEM command below).
     $ !
     $ mem 7ffe350b:7ffe390f
     776F6873 0A000372 69640300 7FFE3608    7FFE350B
     49545F57 4F485309 000A7372 65737520   users...SHOW_TI    7FFE351B
     2F444D43 0800046C 69616D04 0009454D  ME...mail...CMD/    7FFE352B
     08455641 532F444D 43080008 5453494C  LIST...CMD/SAVE.    7FFE353B
     64756109 00085453 494C2F44 4D430800  ..CMD/LIST...aud    7FFE354B
     206E6F6D 756B7708 00096666 6F2F7469  it/off...wkumon     7FFE355B
                     ...........     ..........

     00000000 00000000 00000000 00000000  ................    7FFE38EB
     00000000 00000000 00000000 00000000  ................    7FFE38FB
     01000C00 00810350 03000000 00000000  ......<P>.......    7FFE390B
     $ !The buffer ends here ^   @7FFE390F

               Figure 1 - Partial dump of DCL command buffer

The commands are stored beginning at 7FFE350F and extending through 7FFE390F. The longword at location 7FFE350B is a pointer to the end of the last command stored in the buffer. Each command is preceded andfollowed by a count byte, with a NULL (0) byte separating commands (See Figure 2). The count byte (which holds the length of the command) is used to let DCL know how many bytes to show as you use your cursor keys to step through the list. When you press the up-arrow key, DCL uses the pointer (at 7FFE350B) as the starting location (the pointer is actually the address of the NULL byte following the last command). The pointer is decremented so that it points to the length of the last command, which is then used as the number of bytes to output to the terminal. After the command has been displayed, the command pointer is positioned at the NULL byte separating the last two commands. Then, depending on whether the up-arrow or down-arrow key is pressed, DCL moves forward or backward through the buffer. DCL stops stepping backwards after 20 commands have been RECALLed. Whenever you press RETURN to execute a command, the command string is stored in the buffer starting at the byte after the last NULL byte and the command pointer at 7FFE350B is updated to point to the byte past it. Got it? Good!

     . . C M  D / S A  V E . .  . C M D  / L I S  T . . .  audit/off...
     0008434D 442F5341 56450800 08434D44 2F4C4953 54080009
     ^ ^ ^                 ^ ^  ^ ^                 ^ ^ ^
     1 2 3                 4 1  2 3                 4 1 2

              1 - Null byte separating commands
              2 - Leading length of command
              3 - Start of command
              4 - Trailing length of command

         Figure 2 - Format of commands stored in the buffer

When the command buffer is full, DCL wraps the buffer so that it starts over again at 7FFE350F. Any resulting partial command is then zeroed out, so that DCL doesn’t get confused and output part of a command. In Figure 3, the first command gets wiped out by the wrapping of the command text (and the command pointer points to the end of the last command, which has been wrapped).

     $ mem 7ffe350b:7ffe390f
     66303933 65666637 3A623035 7FFE351C  L5..50b:7ffe390f    7FFE350B
     54030000 00000000 00000000 00000015  ...............T    7FFE351B
     206C6C61 2F657571 20687314 00035550 que/all     7FFE352B
                    ...........     ..........

     09000A41 544F5551 20574F48 530A000A  ...SHOW QUOTA...    7FFE38EB
     206D656D 15000945 4D49545F 574F4853  SHOW_TIME...mem     7FFE38FB
     01000C00 00810350 03000033 65666637  7ffe3..<P>.......    7FFE390B
     $ !The buffer ends here ^  and wraps back to 7FFE350F

       Figure 3 - Dump of DCL command buffer showing command wrap

                Note that the null bytes after 7FFE351C were
                written to wipe out the command previously
                stored there.

CMD.MAR is a MACRO program that will allow a user to flush his command buffer, list all the commands stored in the buffer, save the commands by writing them out to a file, and restore commands previously SAVEd. CMEXEC privilege (or CMKRNL) is required to FLUSH and RESTORE the command buffer, but LISTing and SAVE-ing can be done without privilege. The memory used as a buffer is not protected from user-mode reads, but is protected against user-mode and supervisor-mode writes. Therefore, you can see your old commands, but you can’t do anything with them unless you are running in a executive or kernel mode. CMD.EXE can be installed with CMEXEC privilege, if desired.

To use CMD.MAR, simply assemble and link the program and set up a foreign DCL command:

$ CMD :== $dev:[dir]CMD.EXE

A simple (but easy to code) command line parser allows you to specify which operation CMD is to perform. The valid qualifiers are:

/LIST     - List all commands stored
/FLUSH    - Flush the command buffer
/SAVE     - Write the buffer to a file for later restoration
/RESTORE  - Restore commands that were previously SAVEd
/BOTH     - SAVE, then FLUSH the command buffer

Only one qualifier may be specified, and at least 3 characters must follow the “/”.

To LIST old commands, CMD simply steps backwards through the buffer, displaying on the terminal all commands in the buffer (much as the DCL RECALL command does). To FLUSH the buffer, CMD zeroes out all 1024 bytes of the buffer, and resets the pointer at 7FFE350B to point to 7FFE350F. When SAVE-ing the commands, CMD writes the 1040-byte buffer starting at 7FFE3500 out to SYS$LOGIN:SAVE_CMDS.TXT. When you wish to RESTORE the commands, CMD simply reads the old buffer from SYS$LOGIN:SAVE_CMDS.TXT and copies all 1040 bytes to 7FFE3500, writing over the current DCL command buffer. After commands have been RESTOREd, your DCL RECALL buffer will be exactly as it was when it was SAVEd. See Figure 4 for an example of CMD’s actions.

        $ recall/all            ! Display the commands in the buffer
         1 audit/virt
         2 evespn
         3 Clock
         4 wait 00:00:13
         5 SHOW_TIME
         6 WKUMON
        $ CMD/SAVE              ! Now SAVE those commands
        $ CMD/FLUSH             ! ... and FLUSH the buffer
        $ recall/all            ! Display the buffer to show it was FLUSHed
        $ CMD/RESTORE           ! RESTORE the commands saved above
        $ recall/all            ! Display them now
         1 CMD/SAVE
         2 audit/virt
         3 evespn
         4 Clock
         5 wait 00:00:13
         6 SHOW_TIME
         7 WKUMON
        $ CMD/LIST              ! Use CMD/LIST to display them
        wait 00:00:13
        $ ! Voila!

                    Figure 4 -- Sample CMD usage

Please note that all of the addresses used by CMD were determined by observation alone and could change with any version of VMS. They are, however, correct for versions 4.1 through 4.5 of VMS. I probably have just overlooked the proper symbols defined for those addresses.

Biographical Information:

Hunter Goatley, a graduate in Computer Science from Western Kentucky University, is currently working as a programmer/analyst for Clyde Digital Systems, Orem, Utah.


Click here to download CMD.MAR

;=                                                                      =
;=      Programmer:     Hunter Goatley, Clyde Digital Systems, Orem, UT =
;=      Program:        CMD.MAR                                         =
;=      Purpose:        Save, restore, flush, & list DCL command buffer =
;=      Language:       VAX-11 MACRO32  assembly language               =
;=      System:         VAX/VMS v4.x                                    =
;=      Date:           October 8, 1986                                 =
;=                                                                      =
;=                                                                      =
;=      This  program  allows  the user to flush the command buffer,    =
;=      list all of the commands in  the buffer,  write the contents    =
;=      of the buffer to a file, and restore the buffer from a file.    =
;=                                                                      =
;=      To use this program, set up a foreign symbol, such as:          =
;=                                                                      =
;=                      $ CMD :== $dev:[dir]CMD.EXE                     =
;=                      $ CMD/qualifier                                 =
;=                                                                      =
;=      The valid qualifiers are:                                       =
;=                                                                      =
;=      /LIST   -  List all commands in the DCL command buffer          =
;=      /SAVE   -  Save the commands in SYS$LOGIN:SAVE_CMDS.TXT         =
;=      /RESTORE-  Restore commands previously SAVEd                    =
;=      /FLUSH  -  Flush the command buffer                             =
;=      /BOTH   -  SAVE, then FLUSH the DCL command buffer              =
;=                                                                      =
;  WARNING!!!!  These addresses were found by searching through P1 space --
;               they may be very VMS-version dependent.
CMD_BUF_BEG = ^X7FFE350F        ; First usable byte of the command data area
CMD_PTR     = ^X7FFE350B        ; Address of pointer to end of most recent cmd
CMD_BUF_END = ^X7FFE3910        ; One byte past cmd buffer (with autodecrement,
                                ; ...  will become addr of last byte of buffer)
CMD_BUFFER_ADDR = ^X7FFE3500            ; These symbols are based on observation
CMD_BUFFER_LEN  = 1040                  ; only!!  No guarantees

;  This macro compares the current address (in R10) with the beginning address
;  of the data area -- if they are equal, set the current address = to the end
;  of the data block (wrap).
        .MACRO  CHECK   ?HERE
        CMPL    R11,R10                 ; Have we gone past the beginning?
        BNEQ    HERE                    ; No -- skip next instruction
        MOVL    #CMD_BUF_END,R10        ; Make R10 point to one byte past the
                                        ; ... end of the data area.  With next
                                        ; ... autodecrement, it will point to
                                        ; ... the last byte of the table.
;  This macro compares the first four bytes of FOR_BUFF with a valid qualifier.
;  If there is a match, the appropriate routine is called and control branches
;  to return to VMS.
        CMPL    #^A\QUAL\,FOR_BUFF      ; Was /QUAL given?
        BNEQU   LABEL                   ; No - try next
        CALLS   #0,ROUTIN1              ; Call the proper routine
        .IF     DIF     0,ROUTIN2       ; Was a second routine given?
        BLBC    R0,BYE                  ; Error?
        CALLS   #0,ROUTIN2              ; No - call the second routine
        .ENDC                           ; ...
        BRW     BYE                     ; Return to VMS (only one qualifier
LABEL:  .ENDM   PRESENT                 ; ...  at a time!!!

        $SSDEF                          ; Include status symbols
        $RMSDEF                         ; Include RMS symbols
        $FABDEF                         ; Include $FAB symbols
        $RABDEF                         ; Include $RAB symbols
        $PRVDEF                         ; Include privilege symbols
CMDFAB: $FAB    FNM=<SYS$LOGIN:SAVE_CMDS.TXT&rt;, -        ; File name
                FAC=<GET,PUT&rt;, -                ; File ACcess (R/W only)
                MRS=1420, -                     ; Maximum Record Size
                RAT=CR, -                       ; Record ATtributes
                ORG=SEQ                         ; File Organization (sequential)
;***  Record Access Block for SAVE_CMDS.TXT
        $RAB    FAB=CMDFAB, -           ; Record Access Block
                RBF=CMDREC, -           ; If writing, write from CMDREC
                RSZ=CMD_BUFFER_LEN, -   ; Record size
                USZ=CMD_BUFFER_LEN, -   ; Input buffer is 1420 chars long
                UBF=CMDREC              ; Address of SAVE_CMDS record buffer
CMDREC: . = .+CMD_BUFFER_LEN            ; Buffer for DCL commands
MAXCMDLEN = 256                         ; Maximum length of a DCL command
CMDBUFFER_D:                            ; Descriptor for the command buffer
        .WORD   MAXCMDLEN               ; ...
        .BYTE   DSC$K_DTYPE_T           ; ...
        .BYTE   DSC$K_CLASS_S           ; ...
        .ADDRESS .+4                    ; ...
CMDBUFFER:                              ; The command buffer
        .BYTE   ^A/ /[MAXCMDLEN]        ; ...
FOR_BUFF_D:                             ; Descriptor for the command line
        .WORD   MAXCMDLEN               ; ...  returned by LIB$GET_FOREIGN
        .BYTE   DSC$K_DTYPE_T           ; ...
        .BYTE   DSC$K_CLASS_S           ; ...
        .ADDRESS .+4                    ; ...
FOR_BUFF:                               ; BUffer holding the command line
        .BLKB   MAXCMDLEN               ; ...
FOR_LEN:.LONG   0                       ; Length of command line returned
USAGE_D:                                ; Usage message
        .ASCID  -
 \Must give valid qualifier: /LIST, /FLUSH, /SAVE, /RESTORE, /BOTH (save,flush)\
CMEXEC: .QUAD   PRV$M_CMEXEC            ; Privilege needed to flush & restore
CMKRNL: .QUAD   PRV$M_CMKRNL            ; Alternative privilege needed
        .ENTRY  DCL_CMDS,^M<&rt;
; A simple command line parser follows.  The code simply checks for a "/"
; followed by at least 3 characters of the command qualifier.  Not very
; forgiving, but easy to code.
        PUSHAW  FOR_LEN                 ; Get any commands on command line
        PUSHL   #0                      ; Don't want to prompt for anything
        PUSHAQ  FOR_BUFF_D              ; Buffer for command line
        CALLS   #3,G^LIB$GET_FOREIGN    ; Get the command line
        TSTW    FOR_LEN                 ; Was anything given?
        BNEQ    10$                     ; Yes  - see what was
        PUSHAQ  USAGE_D                 ; No - print usage message
        CALLS   #1,G^LIB$PUT_OUTPUT     ; ...
        BRW     BYE                     ; ...
10$:    PRESENT </RES&rt;,RESTORE_CMDS             ; Was /RESTORE given?
        PRESENT </LIS&rt;,LIST_CMDS                ; Was /LIST?
        PRESENT </FLU&rt;,FLUSH_CMDS               ; Was /FLUSH?
        PRESENT </SAV&rt;,SAVE_CMDS                ; Was /SAVE?
        PRESENT </BOT&rt;,SAVE_CMDS,FLUSH_CMDS     ; Was /BOTH (save and flush)?

        PUSHAQ  USAGE_D                 ; If here, a valid qualifier was not
        CALLS   #1,G^LIB$PUT_OUTPUT     ; ...  given - print USAGE message
BYE:    $EXIT_S CODE=R0                 ; Exit to VMS
;   Subroutine SAVE_CMDS - Save the DCL commands in SYS$LOGIN:SAVE_CMDS.TXT
        .ENTRY  SAVE_CMDS,^M<&rt;
        $CREATE FAB=CMDFAB                      ; Create the output file
        BLBC    R0,100$                         ; Error?
        $CONNECT RAB=CMDRAB                     ; Connect the RAB
        BLBC    R0,100$                         ; Error?
        MOVC3   #CMD_BUFFER_LEN, -              ; Get the DCL previous command
                @#CMD_BUFFER_ADDR,CMDREC        ; ... buffer (not protected
                                                ; ... against user-mode reads)
        $PUT    RAB=CMDRAB                      ; Write the buffer to the file
        BLBC    R0,100$                         ; Error?
        $CLOSE  FAB=CMDFAB                      ; Close the file
100$:   RET                                     ; Return to caller
;   Subroutine RESTORE_CMDS - Restore DCL commands in SYS$LOGIN:SAVE_CMDS.TXT
;   Calls EXEC_RESTORE from EXECUTIVE mode
        .ENTRY  RESTORE_CMDS,^M<&rt;
        CALLS   #0,GETPRV                       ; Turn on the CMEXEC to do this
        BLBC    R0,100$                         ; If error (No priv), return
        $OPEN   FAB=CMDFAB                      ; Open SAVE_CMDS.TXT for reading
        BLBC    R0,100$                         ; Error?
        $CONNECT RAB=CMDRAB                     ; Connect the RAB
        BLBC    R0,100$                         ; Error?
        $GET    RAB=CMDRAB                      ; Read the only record in the
                                                ; ... file (the command buffer)
        BLBC    R0,100$                         ; Error?
        $CMEXEC_S -                             ; Go move the command buffer
                ROUTIN=EXEC_RESTORE             ; ... to process space
        $CLOSE  FAB=CMDFAB                      ; Close the file
100$:   RET                                     ; Return to caller
;****   This executive mode routine moves the previous commands from CMDREC
;****   to the CLI data area.
        .ENTRY  EXEC_RESTORE,^M<&rt;
        MOVAL   HANDLER,(FP)                    ; Unnecessary handler, but...
        MOVC3   #CMD_BUFFER_LEN,CMDREC, -       ; Move the prev command block
                @#CMD_BUFFER_ADDR               ; ...  to the CLI data area
        MOVZBL  #SS$_NORMAL,R0                  ; Set up a success return
        RET                                     ; Return to caller
        .ENTRY  HANDLER,^M<&rt;                    ; Exit handler in case of
        $EXIT_S                                 ; ... $ACCVIO (Kill process)
;   Subroutine LIST_CMDS - List all commands stored in DCL RECALL buffer
        .ENTRY  LIST_CMDS,^M<R2,R3,R4,R5,R6,R7,R8,R9,R10,R11&rt;
        MOVL    @#CMD_PTR,R10           ; Get addr of end of most recent cmd
        MOVL    R10,R9                  ; Make a copy of it
        MOVL    #CMD_BUF_BEG,R11        ; Get the beginning of the data block
LLOOP:  MOVAB   CMDBUFFER,R8            ; Get the buffer address
        MOVC5   #0,#0,#^A/ /,#MAXCMDLEN,(R8)
                                        ; Clear the buffer
        MOVZBL  -(R10),R7               ; Get the length of the first command
        BEQLU   LFIN                    ; 2 null bytes in a row? End
        MOVW    R7,CMDBUFFER_D          ; Move the length to the descriptor
        CHECK                           ; Check to see if this command wraps
                                        ; ...  to the end of the data area
        ADDL2   R7,R8                   ; Make R8 point to end of buffer
LLOOP1: MOVB    -(R10),-(R8)            ; Move the first byte
        CHECK                           ; Check our addresses
        SOBGTR  R7,LLOOP1               ; Loop until command is moved
        PUSHAQ  CMDBUFFER_D             ; Write it to the terminal
        CALLS   #1,G^LIB$PUT_OUTPUT     ; ....
        DECL    R10                     ; Should now point to prefix count
        CHECK                           ; Check our addresses
        DECL    R10                     ; Should now point to 0 byte
        CHECK                           ; Check our addresses
        CMPL    R10,R9                  ; If current addr = starting addr
        BEQL    LFIN                    ; ...  end of data
        BRB     LLOOP                   ; Loop until all commands printed
LFIN:   MOVL    #SS$_NORMAL,R0          ; Set return code
        RET                             ; Return to caller
;   Subroutine FLUSH_CMDS - Flush the DCL command RECALL buffer
        .ENTRY  FLUSH_CMDS,^M<&rt;
        CALLS   #0,GETPRV               ; Turn on the CMEXEC to do this
        BLBC    R0,5$                   ; If error (No priv), return
        $CMEXEC_S -                     ; Flush the DCL command buffer
                ROUTIN=EXEC_FLUSH       ; ...  (zero it out)
5$:     RET                             ; Return to caller
;  Zero out the DCL command recall buffer
        .ENTRY  EXEC_FLUSH,^M<R2,R3,R4,R5&rt;
        MOVC5   #0,#0,#0,#CMD_BUF_LEN,-         ; Zero out the DCL command
                @#CMD_BUF_BEG                   ; ...  recall buffer
        MOVL    #CMD_BUF_BEG,@#CMD_PTR          ; Reset DCL command pointer
        MOVL    #SS$_NORMAL,R0                  ; Set return status
        RET                                     ; Return to caller
;   Subroutine GETPRV - Turn on either CMEXEC or CMKRNL to do FLUSH and RESTORE
        .ENTRY  GETPRV,^M<&rt;
        $SETPRV_S -                     ; Turn on CMEXEC privilege
                ENBFLG=#1, -            ; ...
                PRVADR=CMEXEC           ; ...
        CMPL    #SS$_NORMAL,R0          ; Was CMEXEC turned on?
        BEQLU   5$                      ; Yes - return
        $SETPRV_S -                     ; If not, try to turn on CMKRNL
                ENBFLG=#1, -            ; ...
                PRVADR=CMKRNL           ; ...
        CMPL    #SS$_NORMAL,R0          ; Was CMEXEC turned on?
        BEQLU   5$                      ; Yes - return
        MOVL    #SS$_NOCMEXEC,R0        ; If not, tell that CMEXEC is needed
5$:     RET                             ; Return to caller

        .END    DCL_CMDS
 Posted by at 5:05 am