As a practical example of use of the MS-DOS EXEC function, I have included a small command interpreter called SHELL, with equivalent Microsoft C (Figure 12-4) and Microsoft Macro Assembler (Figure 12-5) source code. The source code for the assembly-language version is considerably more complex than the code for the C version, but the names and functionality of the various procedures are quite parallel.
/*
SHELL.C Simple extendable command interpreter
for MS-DOS versions 2.0 and later
Copyright 1988 Ray Duncan
Compile: C>CL SHELL.C
Usage: C>SHELL
*/
#include <stdio.h>
#include <process.h>
#include <stdlib.h>
#include <signal.h>
/* macro to return number of
elements in a structure */
#define dim(x) (sizeof(x) / sizeof(x[0]))
unsigned intrinsic(char *); /* function prototypes */
void extrinsic(char *);
void get_cmd(char *);
void get_comspec(char *);
void break_handler(void);
void cls_cmd(void);
void dos_cmd(void);
void exit_cmd(void);
struct cmd_table { /* intrinsic commands table */
char *cmd_name;
int (*cmd_fxn)();
} commands[] =
{ "CLS", cls_cmd,
"DOS", dos_cmd,
"EXIT", exit_cmd, };
static char com_spec[64]; /* COMMAND.COM filespec */
main(int argc, char *argv[])
{
char inp_buf[80]; /* keyboard input buffer */
get_comspec(com_spec); /* get COMMAND.COM filespec */
/* register new handler
for Ctrl-C interrupts */
if(signal(SIGINT, break_handler) == (int(*)()) -1)
{
fputs("Can't capture Control-C Interrupt", stderr);
exit(1);
}
while(1) /* main interpreter loop */
{
get_cmd(inp_buf); /* get a command */
if (! intrinsic(inp_buf) ) /* if it's intrinsic,
run its subroutine */
extrinsic(inp_buf); /* else pass to COMMAND.COM */
}
}
/*
Try to match user's command with intrinsic command
table. If a match is found, run the associated routine
and return true; else return false.
*/
unsigned intrinsic(char *input_string)
{
int i, j; /* some scratch variables */
/* scan off leading blanks */
while(*input_string == '\x20') input_string++ ;
/* search command table */
for(i=0; i < dim(commands); i++)
{
j = strcmp(commands[i].cmd_name, input_string);
if(j == 0) /* if match, run routine */
{
(*commands[i].cmd_fxn)();
return(1); /* and return true */
}
}
return(0); /* no match, return false */
}
/*
Process an extrinsic command by passing it
to an EXEC'd copy of COMMAND.COM.
*/
void extrinsic(char *input_string)
{
int status;
status = system(input_string); /* call EXEC function */
if(status) /* if failed, display
error message */
fputs("\nEXEC of COMMAND.COM failed\n", stderr);
}
/*
Issue prompt, get user's command from standard input,
fold it to uppercase.
*/
void get_cmd(char *buffer)
{
printf("\nsh: "); /* display prompt */
gets(buffer); /* get keyboard entry */
strupr(buffer); /* fold to uppercase */
}
/*
Get the full path and file specification for COMMAND.COM
from the COMSPEC variable in the environment.
*/
void get_comspec(char *buffer)
{
strcpy(buffer, getenv("COMSPEC"));
if(buffer[0] == NULL)
{
fputs("\nNo COMSPEC in environment\n", stderr);
exit(1);
}
}
/*
This Ctrl-C handler keeps SHELL from losing control.
It just reissues the prompt and returns.
*/
void break_handler(void)
{
signal(SIGINT, break_handler); /* reset handler */
printf("\nsh: "); /* display prompt */
}
/*
These are the subroutines for the intrinsic commands.
*/
void cls_cmd(void) /* CLS command */
{
printf("\033[2J"); /* ANSI escape sequence */
} /* to clear screen */
void dos_cmd(void) /* DOS command */
{
int status;
/* run COMMAND.COM */
status = spawnlp(P_WAIT, com_spec, com_spec, NULL);
if (status)
fputs("\nEXEC of COMMAND.COM failed\n",stderr);
}
void exit_cmd(void) /* EXIT command */
{
exit(0); /* terminate SHELL */
}
Figure 12-4. SHELL.C: A table-driven command interpreter written in Microsoft C.
name shell
page 55,132
title SHELL.ASM--simple MS-DOS shell
;
; SHELL.ASM Simple extendable command interpreter
; for MS-DOS versions 2.0 and later
;
; Copyright 1988 by Ray Duncan
;
; Build: C>MASM SHELL;
; C>LINK SHELL;
;
; Usage: C>SHELL;
;
stdin equ 0 ; standard input handle
stdout equ 1 ; standard output handle
stderr equ 2 ; standard error handle
cr equ 0dh ; ASCII carriage return
lf equ 0ah ; ASCII linefeed
blank equ 20h ; ASCII blank code
escape equ 01bh ; ASCII escape code
_TEXT segment word public 'CODE'
assume cs:_TEXT,ds:_DATA,ss:STACK
shell proc far ; at entry DS = ES = PSP
mov ax,_DATA ; make our data segment
mov ds,ax ; addressable
mov ax,es:[002ch] ; get environment segment
mov env_seg,ax ; from PSP and save it
; release unneeded memory...
; ES already = PSP segment
mov bx,100h ; BX = paragraphs needed
mov ah,4ah ; function 4ah = resize block
int 21h ; transfer to MS-DOS
jnc shell1 ; jump if resize OK
mov dx,offset msg1 ; resize failed, display
mov cx,msg1_length ; error message and exit
jmp shell4
shell1: call get_comspec ; get COMMAND.COM filespec
jnc shell2 ; jump if it was found
mov dx,offset msg3 ; COMSPEC not found in
mov cx,msg3_length ; environment, display error
jmp shell4 ; message and exit
shell2: mov dx,offset shell3 ; set Ctrl-C vector (int 23h)
mov ax,cs ; for this program's handler
mov ds,ax ; DS:DX = handler address
mov ax,2523h ; function 25h = set vector
int 21h ; transfer to MS-DOS
mov ax,_DATA ; make our data segment
mov ds,ax ; addressable again
mov es,ax
shell3: ; main interpreter loop
call get_cmd ; get a command from user
call intrinsic ; check if intrinsic function
jnc shell3 ; yes, it was processed
call extrinsic ; no, pass it to COMMAND.COM
jmp shell3 ; then get another command
shell4: ; come here if error detected
; DS:DX = message address
; CX = message length
mov bx,stderr ; BX = standard error handle
mov ah,40h ; function 40h = write
int 21h ; transfer to MS-DOS
mov ax,4c01h ; function 4ch = terminate with
; return code = 1
int 21h ; transfer to MS-DOS
shell endp
intrinsic proc near ; decode user entry against
; the table "COMMANDS"
; if match, run the routine,
; and return carry = false
; if no match, carry = true
; return carry = true
mov si,offset commands ; DS:SI = command table
intr1: cmp byte ptr [si],0 ; end of table?
je intr7 ; jump, end of table found
mov di,offset inp_buf ; no, let DI = addr of user input
intr2: cmp byte ptr [di],blank ; scan off any leading blanks
jne intr3
inc di ; found blank, go past it
jmp intr2
intr3: mov al,[si] ; next character from table
or al,al ; end of string?
jz intr4 ; jump, entire string matched
cmp al,[di] ; compare to input character
jnz intr6 ; jump, found mismatch
inc si ; advance string pointers
inc di
jmp intr3
intr4: cmp byte ptr [di],cr ; be sure user's entry
je intr5 ; is the same length...
cmp byte ptr [di],blank ; next character in entry
jne intr6 ; must be blank or return
intr5: call word ptr [si+1] ; run the command routine
clc ; return carry flag = false
ret ; as success flag
intr6: lodsb ; look for end of this
or al,al ; command string (null byte)
jnz intr6 ; not end yet, loop
add si,2 ; skip over routine address
jmp intr1 ; try to match next command
intr7: stc ; command not matched, exit
ret ; with carry = true
intrinsic endp
extrinsic proc near ; process extrinsic command
; by passing it to
; COMMAND.COM with a
; " /C " command tail
mov al,cr ; find length of command
mov cx,cmd_tail_length ; by scanning for carriage
mov di,offset cmd_tail+1 ; return
cld
repnz scasb
mov ax,di ; calculate command-tail
sub ax,offset cmd_tail+2 ; length without carriage
mov cmd_tail,al ; return, and store it
; set command-tail address
mov word ptr par_cmd,offset cmd_tail
call exec ; and run COMMAND.COM
ret
extrinsic endp
get_cmd proc near ; prompt user, get command
; display the shell prompt
mov dx,offset prompt ; DS:DX = message address
mov cx,prompt_length ; CX = message length
mov bx,stdout ; BX = standard output handle
mov ah,40h ; function 40h = write
int 21h ; transfer to MS-DOS
; get entry from user
mov dx,offset inp_buf ; DS:DX = input buffer
mov cx,inp_buf_length ; CX = max length to read
mov bx,stdin ; BX = standard input handle
mov ah,3fh ; function 3fh = read
int 21h ; transfer to MS-DOS
mov si,offset inp_buf ; fold lowercase characters
mov cx,inp_buf_length ; in entry to uppercase
gcmd1: cmp byte ptr [si],'a' ; check if 'a-z'
jb gcmd2 ; jump, not in range
cmp byte ptr [si],'z' ; check if 'a-z'
ja gcmd2 ; jump, not in range
sub byte ptr [si],'a'-'A' ; convert to uppercase
gcmd2: inc si ; advance through entry
loop gcmd1
ret ; back to caller
get_cmd endp
get_comspec proc near ; get location of COMMAND.COM
; from environment "COMSPEC="
; returns carry = false
; if COMSPEC found
; returns carry = true
; if no COMSPEC
mov si,offset com_var ; DS:SI = string to match...
call get_env ; search environment block
jc gcsp2 ; jump if COMSPEC not found
; ES:DI points past "="
mov si,offset com_spec ; DS:SI = local buffer
gcsp1: mov al,es:[di] ; copy COMSPEC variable
mov [si],al ; to local buffer
inc si
inc di
or al,al ; null char? (turns off carry)
jnz gcsp1 ; no, get next character
gcsp2: ret ; back to caller
get_comspec endp
get_env proc near ; search environment
; call DS:SI = "NAME="
; uses contents of "ENV_SEG"
; returns carry = false and ES:DI
; pointing to parameter if found,
; returns carry = true if no match
mov es,env_seg ; get environment segment
xor di,di ; initialize env offset
genv1: mov bx,si ; initialize pointer to name
cmp byte ptr es:[di],0 ; end of environment?
jne genv2 ; jump, end not found
stc ; no match, return carry set
ret
genv2: mov al,[bx] ; get character from name
or al,al ; end of name? (turns off carry)
jz genv3 ; yes, name matched
cmp al,es:[di] ; compare to environment
jne genv4 ; jump if match failed
inc bx ; advance environment
inc di ; and name pointers
jmp genv2
genv3: ; match found, carry = clear,
ret ; ES:DI = variable
genv4: xor al,al ; scan forward in environment
mov cx,-1 ; for zero byte
cld
repnz scasb
jmp genv1 ; go compare next string
get_env endp
exec proc near ; call MS-DOS EXEC function
; to run COMMAND.COM
mov stkseg,ss ; save stack pointer
mov stkptr,sp
; now run COMMAND.COM
mov dx,offset com_spec ; DS:DX = filename
mov bx,offset par_blk ; ES:BX = parameter block
mov ax,4b00h ; function 4bh = EXEC
; subfunction 0 =
; load and execute
int 21h ; transfer to MS-DOS
mov ax,_DATA ; make data segment
mov ds,ax ; addressable again
mov es,ax
cli ; (for bug in some 8088s)
mov ss,stkseg ; restore stack pointer
mov sp,stkptr
sti ; (for bug in some 8088s)
jnc exec1 ; jump if no errors
; display error message
mov dx,offset msg2 ; DS:DX = message address
mov cx,msg2_length ; CX = message length
mov bx,stderr ; BX = standard error handle
mov ah,40h ; function 40h = write
int 21h ; transfer to MS-DOS
exec1: ret ; back to caller
exec endp
cls_cmd proc near ; intrinsic CLS command
mov dx,offset cls_str ; send the ANSI escape
mov cx,cls_str_length ; sequence to clear
mov bx,stdout ; the screen
mov ah,40h
int 21h
ret
cls_cmd endp
dos_cmd proc near ; intrinsic DOS command
; set null command tail
mov word ptr par_cmd,offset nultail
call exec ; and run COMMAND.COM
ret
dos_cmd endp
exit_cmd proc near ; intrinsic EXIT command
mov ax,4c00h ; call MS-DOS terminate
int 21h ; function with
; return code of zero
exit_cmd endp
_TEXT ends
STACK segment para stack 'STACK' ; declare stack segment
dw 64 dup (?)
STACK ends
_DATA segment word public 'DATA'
commands equ $ ; "intrinsic" commands table
; each entry is ASCIIZ string
; followed by the offset
; of the procedure to be
; executed for that command
db 'CLS',0
dw cls_cmd
db 'DOS',0
dw dos_cmd
db 'EXIT',0
dw exit_cmd
db 0 ; end of table
com_var db 'COMSPEC=',0 ; environment variable
; COMMAND.COM filespec
com_spec db 80 dup (0) ; from environment COMSPEC=
nultail db 0,cr ; null command tail for
; invoking COMMAND.COM
; as another shell
cmd_tail db 0,' /C ' ; command tail for invoking
; COMMAND.COM as a transient
inp_buf db 80 dup (0) ; command line from standard input
inp_buf_length equ $-inp_buf
cmd_tail_length equ $-cmd_tail-1
prompt db cr,lf,'sh: ' ; SHELL's user prompt
prompt_length equ $-prompt
env_seg dw 0 ; segment of environment block
msg1 db cr,lf
db 'Unable to release memory.'
db cr,lf
msg1_length equ $-msg1
msg2 db cr,lf
db 'EXEC of COMMAND.COM failed.'
db cr,lf
msg2_length equ $-msg2
msg3 db cr,lf
db 'No COMSPEC variable in environment.'
db cr,lf
msg3_length equ $-msg3
cls_str db escape,'[2J' ; ANSI escape sequence
cls_str_length equ $-cls_str ; to clear the screen
; EXEC parameter block
par_blk dw 0 ; environment segment
par_cmd dd cmd_tail ; command line
dd fcb1 ; file control block #1
dd fcb2 ; file control block #2
fcb1 db 0 ; file control block #1
db 11 dup (' ')
db 25 dup (0)
fcb2 db 0 ; file control block #2
db 11 dup (' ')
db 25 dup (0)
stkseg dw 0 ; original SS contents
stkptr dw 0 ; original SP contents
_DATA ends
end shell
Figure 12-5. SHELL.ASM: A simple table-driven command interpreter written in Microsoft Macro Assembler.
The SHELL program is table driven and can easily be extended to provide a powerful customized user interface for almost any application. When SHELL takes control of the system, it displays the prompt
sh:
and waits for input from the user. After the user types a line terminated by a carriage return, SHELL tries to match the first token in the line against its table of internal (intrinsic) commands. If it finds a match, it calls the appropriate subroutine. If it does not find a match, it calls the MS-DOS EXEC function and passes the user's input to COMMAND.COM with the /C switch, essentially using COMMAND.COM as a transient command processor under its own control.
As supplied in these listings, SHELL "knows" exactly three internal commands:
Command Action
CLS Uses the ANSI standard control sequence to clear the
display screen and home the cursor.
DOS Runs a copy of COMMAND.COM.
EXIT Exits SHELL, returning control of the system to the
next lower command interpreter.
You can quickly add new intrinsic commands to either the C version or the assembly-language version of SHELL. Simply code a procedure with the appropriate action and insert the name of that procedure, along with the text string that defines the command, into the table COMMANDS. In addition, you can easily prevent SHELL from passing certain "dangerous" commands (such as MKDIR or ERASE) to COMMAND.COM simply by putting the names of the commands to be screened out into the intrinsic command table with the address of a subroutine that prints an error message.
To summarize, the basic flow of both versions of the SHELL program is as follows:
1.The program calls MS-DOS Int 21H Function 4AH (Resize Memory Block) to shrink its memory allocation, so that the maximum possible space will be available for COMMAND.COM if it is run as an overlay. (This is explicit in the assembly-language version only. To keep the example code simple, the number of paragraphs to be reserved is coded as a generous literal value, rather than being figured out at runtime from the size and location of the various program segments.)
2.The program searches the environment for the COMSPEC variable, which defines the location of an executable copy of COMMAND.COM. If it can't find the COMSPEC variable, it prints an error message and exits.
3.The program puts the address of its own handler in the Ctrl-C vector (Int 23H) so that it won't lose control if the user enters a Ctrl-C or a Ctrl-Break.
4.The program issues a prompt to the standard output device.
5.The program reads a buffered line from the standard input device to get the user's command.
6.The program matches the first blank-delimited token in the line against its table of intrinsic commands. If it finds a match, it executes the associated procedure.
7.If the program does not find a match in the table of intrinsic commands, it synthesizes a command-line tail by appending the user's input to the /C switch and then EXECs a copy of COMMAND.COM, passing the address of the synthesized command tail in the EXEC parameter block.
8.The program repeats steps 4 through 7 until the user enters the command EXIT, which is one of the intrinsic commands, and which causes SHELL to terminate execution.
In its present form, SHELL allows COMMAND.COM to inherit a full copy of the current environment. However, in some applications it may be helpful, or safer, to pass a modified copy of the environment block so that the secondary copy of COMMAND.COM will not have access to certain information.